[];
packageManager?: string;
packageRegistry?: string;
registry?: schema.CoreSchemaRegistry;
diff --git a/etc/cli.angular.io/.firebaserc b/etc/cli.angular.io/.firebaserc
deleted file mode 100644
index 3f1a5bf73284..000000000000
--- a/etc/cli.angular.io/.firebaserc
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "projects": {
- "default": "cli-angular-io"
- }
-}
diff --git a/etc/cli.angular.io/README.md b/etc/cli.angular.io/README.md
deleted file mode 100644
index 5e0190d83dc3..000000000000
--- a/etc/cli.angular.io/README.md
+++ /dev/null
@@ -1,14 +0,0 @@
-
-# Angular CLI microsite
-
-This folder contains all the static files used for the Angular CLI microsite
-(http://cli.angular.io).
-
-To make changes on the frontend, just update the files here, and ask the
-caretaker to deploy the new site when the commit is merged. Your commit should
-be of scope `docs:` (**NOT** `fix` or `feat`).
-
-## Deploy
-
-To deploy, use your firebase credentials to login, then use `firebase deploy`
-from this folder. There is currently no build step.
diff --git a/etc/cli.angular.io/angular-logo-with-text.svg b/etc/cli.angular.io/angular-logo-with-text.svg
deleted file mode 100644
index 51b8487acc9a..000000000000
--- a/etc/cli.angular.io/angular-logo-with-text.svg
+++ /dev/null
@@ -1,32 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/etc/cli.angular.io/angular-logo.svg b/etc/cli.angular.io/angular-logo.svg
deleted file mode 100644
index fc8b6e94fc32..000000000000
--- a/etc/cli.angular.io/angular-logo.svg
+++ /dev/null
@@ -1,19 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/etc/cli.angular.io/favicon.ico b/etc/cli.angular.io/favicon.ico
deleted file mode 100644
index 4dace951fab2..000000000000
Binary files a/etc/cli.angular.io/favicon.ico and /dev/null differ
diff --git a/etc/cli.angular.io/firebase.json b/etc/cli.angular.io/firebase.json
deleted file mode 100644
index b55622a85956..000000000000
--- a/etc/cli.angular.io/firebase.json
+++ /dev/null
@@ -1,16 +0,0 @@
-{
- "hosting": {
- "public": "",
- "ignore": [
- "firebase.json",
- "README.md",
- "**/.*"
- ],
- "rewrites": [
- {
- "source": "/**/!(*.@(js|ts|html|css|json|svg|png|jpg|jpeg))",
- "destination": "/index.html"
- }
- ]
- }
-}
diff --git a/etc/cli.angular.io/index.html b/etc/cli.angular.io/index.html
deleted file mode 100644
index 71f55c419027..000000000000
--- a/etc/cli.angular.io/index.html
+++ /dev/null
@@ -1,147 +0,0 @@
-
-
-
-
-
-
-
-
- Angular CLI
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/etc/cli.angular.io/license.html b/etc/cli.angular.io/license.html
deleted file mode 100644
index 0131cc303c40..000000000000
--- a/etc/cli.angular.io/license.html
+++ /dev/null
@@ -1,23 +0,0 @@
-The MIT License
-
-Copyright (c) Google, Inc. All Rights Reserved.
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in
-all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-THE SOFTWARE.
-
-
diff --git a/etc/cli.angular.io/main.css b/etc/cli.angular.io/main.css
deleted file mode 100644
index 61b0cb066543..000000000000
--- a/etc/cli.angular.io/main.css
+++ /dev/null
@@ -1 +0,0 @@
-body{font-family:"Roboto",Helvetica,sans-serif}h4,h5{font-size:30px;font-weight:400;line-height:40px;margin-bottom:15px;margin-top:15px}h5{font-size:16px;font-weight:300;line-height:28px;margin-bottom:25px;max-width:300px}.mdl-demo section.section--center{max-width:920px}.mdl-grid--no-spacing>.mdl-cell{width:100%}.mdl-layout--fixed-drawer>.mdl-layout__content{margin-left:0}.mdl-layout__header{background-color:#f44336;box-shadow:0 2px 5px 0 rgba(0,0,0,.26)}.mdl-layout__header a{color:#fff;text-decoration:none}.mdl-layout--fixed-drawer.is-upgraded:not(.is-small-screen)>.mdl-layout__header{margin-left:0;width:100%}.mdl-layout--fixed-drawer>.mdl-layout__header .mdl-layout__header-row{padding-left:25px;padding-right:0}.mdl-layout__drawer-button,.top-nav-wrapper label{display:none}@media (max-width:1024px){.mdl-layout__drawer-button{display:inline-block}}.mdl-layout__drawer{margin-top:65px;height:calc(100% - 65px)}@media (max-width:1024px){.mdl-layout__drawer{margin-top:0;height:100%}}.mdl-layout-title,.mdl-layout__title{font-size:16px;line-height:28px;letter-spacing:.02em}.microsite-name{display:inline-block;font-size:20px;margin-left:8px;margin-right:30px;text-transform:uppercase;-webkit-transform:translateY(3px);transform:translateY(3px)}.mdl-navigation__link{font-size:16px;text-transform:uppercase;text-decoration:none}.mdl-navigation__link:hover,.top-nav-wrapper label:hover{background-color:#d32f2f}.top-nav-wrapper{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row;-webkit-box-flex:1;-ms-flex-positive:1;flex-grow:1}@media (max-width:800px){.top-nav-wrapper{display:block;position:absolute;right:0;top:0;width:100%}.top-nav-wrapper label{cursor:pointer;display:block;float:right;line-height:56px;padding:0 16px}.top-nav-wrapper nav{background:#d32f2f;clear:both;display:none;height:auto!important}.top-nav-wrapper nav a{display:block}.top-nav-wrapper .mdl-layout-spacer{display:none}input:checked+.top-nav-wrapper label{background:#d32f2f}input:checked+.top-nav-wrapper nav{display:block}}.hero-background{background:-webkit-linear-gradient(#d32f2f ,#f44336);background:linear-gradient(#d32f2f ,#f44336);color:#fff;margin-bottom:60px}.mdl-grid,.mdl-mega-footer--bottom-section .mdl-cell--9-col{-webkit-box-align:center;-ms-flex-align:center;align-items:center}.hero-container{padding:56px 0!important}@media (max-width:830px){.hero-container{text-align:center}}.logo-container{overflow:hidden;text-align:center}@media (max-width:840px){.tagline{max-width:100%}}.mdl-button{height:45px;line-height:45px;min-width:140px;padding:0 30px}.mdl-button--primary.mdl-button--primary.mdl-button--fab,.mdl-button--primary.mdl-button--primary.mdl-button--raised{background-color:#fff;color:#b71c1c}.features-list{width:920px;margin:0 0 23px;padding:15px 200px 15px 15px}@media (max-width:840px){.features-list{padding-right:15px}}.features-list h4{color:#37474f;font-size:28px;font-weight:500;line-height:32px;margin:0 0 16px;opacity:.87}.features-list p,footer ul a{font-size:16px;line-height:30px;opacity:.87}.button-container{margin-bottom:24px!important;text-align:center}.mdl-button--accent.mdl-button--accent.mdl-button--fab,.mdl-button--accent.mdl-button--accent.mdl-button--raised{background-color:#f44336;color:#fff}.mdl-mega-footer--bottom-section .mdl-cell--9-col{-webkit-box-pack:end;-ms-flex-pack:end;justify-content:flex-end;display:-webkit-box;display:-ms-flexbox;display:flex}.mdl-mega-footer--bottom-section,.mdl-mega-footer__bottom-section{background-color:#263238;bottom:0;color:#fff;padding-top:0;right:0}footer ul{font-size:14px;font-weight:400;letter-spacing:0;line-height:24px;list-style:none;padding:0}footer ul a{color:#fff;line-height:28px;padding:0;text-decoration:none}footer ul a:hover{text-decoration:underline}@media (max-width:830px){footer ul{background-color:rgba(0,0,0,.12);padding:8px;text-align:center}}.mdl-mega-footer--bottom-section{margin-bottom:0}.mdl-mega-footer--bottom-section p{font-size:12px;margin:0;opacity:.54}.mdl-mega-footer--bottom-section a{color:#fff;font-weight:400;padding:0;text-decoration:none}.power-text{text-align:right}@media (max-width:830px){.power-text{text-align:center;width:calc(100% - 16px)}}.mdl-base{height:100vh}
diff --git a/etc/cli.angular.io/material.min.css b/etc/cli.angular.io/material.min.css
deleted file mode 100644
index e750f8137205..000000000000
--- a/etc/cli.angular.io/material.min.css
+++ /dev/null
@@ -1,9 +0,0 @@
-/**
- * material-design-lite - Material Design Components in CSS, JS and HTML
- * @version v1.1.3
- * @license Apache-2.0
- * @copyright 2015 Google, Inc.
- * @link https://github.com/google/material-design-lite
- */
-@charset "UTF-8";html{color:rgba(0,0,0,.87)}::-moz-selection{background:#b3d4fc;text-shadow:none}::selection{background:#b3d4fc;text-shadow:none}hr{display:block;height:1px;border:0;border-top:1px solid #ccc;margin:1em 0;padding:0}audio,canvas,iframe,img,svg,video{vertical-align:middle}fieldset{border:0;margin:0;padding:0}textarea{resize:vertical}.browserupgrade{margin:.2em 0;background:#ccc;color:#000;padding:.2em 0}.hidden{display:none!important}.visuallyhidden{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.visuallyhidden.focusable:active,.visuallyhidden.focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}.invisible{visibility:hidden}.clearfix:before,.clearfix:after{content:" ";display:table}.clearfix:after{clear:both}@media print{*,*:before,*:after,*:first-letter{background:transparent!important;color:#000!important;box-shadow:none!important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href)")"}abbr[title]:after{content:" (" attr(title)")"}a[href^="#"]:after,a[href^="javascript:"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100%!important}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}}a,.mdl-accordion,.mdl-button,.mdl-card,.mdl-checkbox,.mdl-dropdown-menu,.mdl-icon-toggle,.mdl-item,.mdl-radio,.mdl-slider,.mdl-switch,.mdl-tabs__tab{-webkit-tap-highlight-color:transparent;-webkit-tap-highlight-color:rgba(255,255,255,0)}html{width:100%;height:100%;-ms-touch-action:manipulation;touch-action:manipulation}body{width:100%;min-height:100%;margin:0}main{display:block}*[hidden]{display:none!important}html,body{font-family:"Helvetica","Arial",sans-serif;font-size:14px;font-weight:400;line-height:20px}h1,h2,h3,h4,h5,h6,p{padding:0}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small{font-family:"Roboto","Helvetica","Arial",sans-serif;font-weight:400;line-height:1.35;letter-spacing:-.02em;opacity:.54;font-size:.6em}h1{font-size:56px;line-height:1.35;letter-spacing:-.02em;margin:24px 0}h1,h2{font-family:"Roboto","Helvetica","Arial",sans-serif;font-weight:400}h2{font-size:45px;line-height:48px}h2,h3{margin:24px 0}h3{font-size:34px;line-height:40px}h3,h4{font-family:"Roboto","Helvetica","Arial",sans-serif;font-weight:400}h4{font-size:24px;line-height:32px;-moz-osx-font-smoothing:grayscale;margin:24px 0 16px}h5{font-size:20px;font-weight:500;line-height:1;letter-spacing:.02em}h5,h6{font-family:"Roboto","Helvetica","Arial",sans-serif;margin:24px 0 16px}h6{font-size:16px;letter-spacing:.04em}h6,p{font-weight:400;line-height:24px}p{font-size:14px;letter-spacing:0;margin:0 0 16px}a{color:#ff4081;font-weight:500}blockquote{font-family:"Roboto","Helvetica","Arial",sans-serif;position:relative;font-size:24px;font-weight:300;font-style:italic;line-height:1.35;letter-spacing:.08em}blockquote:before{position:absolute;left:-.5em;content:'“'}blockquote:after{content:'”';margin-left:-.05em}mark{background-color:#f4ff81}dt{font-weight:700}address{font-size:12px;line-height:1;font-style:normal}address,ul,ol{font-weight:400;letter-spacing:0}ul,ol{font-size:14px;line-height:24px}.mdl-typography--display-4,.mdl-typography--display-4-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:112px;font-weight:300;line-height:1;letter-spacing:-.04em}.mdl-typography--display-4-color-contrast{opacity:.54}.mdl-typography--display-3,.mdl-typography--display-3-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:56px;font-weight:400;line-height:1.35;letter-spacing:-.02em}.mdl-typography--display-3-color-contrast{opacity:.54}.mdl-typography--display-2,.mdl-typography--display-2-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:45px;font-weight:400;line-height:48px}.mdl-typography--display-2-color-contrast{opacity:.54}.mdl-typography--display-1,.mdl-typography--display-1-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:34px;font-weight:400;line-height:40px}.mdl-typography--display-1-color-contrast{opacity:.54}.mdl-typography--headline,.mdl-typography--headline-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:24px;font-weight:400;line-height:32px;-moz-osx-font-smoothing:grayscale}.mdl-typography--headline-color-contrast{opacity:.87}.mdl-typography--title,.mdl-typography--title-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:20px;font-weight:500;line-height:1;letter-spacing:.02em}.mdl-typography--title-color-contrast{opacity:.87}.mdl-typography--subhead,.mdl-typography--subhead-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:16px;font-weight:400;line-height:24px;letter-spacing:.04em}.mdl-typography--subhead-color-contrast{opacity:.87}.mdl-typography--body-2,.mdl-typography--body-2-color-contrast{font-size:14px;font-weight:700;line-height:24px;letter-spacing:0}.mdl-typography--body-2-color-contrast{opacity:.87}.mdl-typography--body-1,.mdl-typography--body-1-color-contrast{font-size:14px;font-weight:400;line-height:24px;letter-spacing:0}.mdl-typography--body-1-color-contrast{opacity:.87}.mdl-typography--body-2-force-preferred-font,.mdl-typography--body-2-force-preferred-font-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:14px;font-weight:500;line-height:24px;letter-spacing:0}.mdl-typography--body-2-force-preferred-font-color-contrast{opacity:.87}.mdl-typography--body-1-force-preferred-font,.mdl-typography--body-1-force-preferred-font-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:14px;font-weight:400;line-height:24px;letter-spacing:0}.mdl-typography--body-1-force-preferred-font-color-contrast{opacity:.87}.mdl-typography--caption,.mdl-typography--caption-force-preferred-font{font-size:12px;font-weight:400;line-height:1;letter-spacing:0}.mdl-typography--caption-force-preferred-font{font-family:"Roboto","Helvetica","Arial",sans-serif}.mdl-typography--caption-color-contrast,.mdl-typography--caption-force-preferred-font-color-contrast{font-size:12px;font-weight:400;line-height:1;letter-spacing:0;opacity:.54}.mdl-typography--caption-force-preferred-font-color-contrast,.mdl-typography--menu{font-family:"Roboto","Helvetica","Arial",sans-serif}.mdl-typography--menu{font-size:14px;font-weight:500;line-height:1;letter-spacing:0}.mdl-typography--menu-color-contrast{opacity:.87}.mdl-typography--menu-color-contrast,.mdl-typography--button,.mdl-typography--button-color-contrast{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:14px;font-weight:500;line-height:1;letter-spacing:0}.mdl-typography--button,.mdl-typography--button-color-contrast{text-transform:uppercase}.mdl-typography--button-color-contrast{opacity:.87}.mdl-typography--text-left{text-align:left}.mdl-typography--text-right{text-align:right}.mdl-typography--text-center{text-align:center}.mdl-typography--text-justify{text-align:justify}.mdl-typography--text-nowrap{white-space:nowrap}.mdl-typography--text-lowercase{text-transform:lowercase}.mdl-typography--text-uppercase{text-transform:uppercase}.mdl-typography--text-capitalize{text-transform:capitalize}.mdl-typography--font-thin{font-weight:200!important}.mdl-typography--font-light{font-weight:300!important}.mdl-typography--font-regular{font-weight:400!important}.mdl-typography--font-medium{font-weight:500!important}.mdl-typography--font-bold{font-weight:700!important}.mdl-typography--font-black{font-weight:900!important}.material-icons{font-family:'Material Icons';font-weight:400;font-style:normal;font-size:24px;line-height:1;letter-spacing:normal;text-transform:none;display:inline-block;word-wrap:normal;-moz-font-feature-settings:'liga';font-feature-settings:'liga';-webkit-font-feature-settings:'liga';-webkit-font-smoothing:antialiased}.mdl-color-text--red{color:#f44336 !important}.mdl-color--red{background-color:#f44336 !important}.mdl-color-text--red-50{color:#ffebee !important}.mdl-color--red-50{background-color:#ffebee !important}.mdl-color-text--red-100{color:#ffcdd2 !important}.mdl-color--red-100{background-color:#ffcdd2 !important}.mdl-color-text--red-200{color:#ef9a9a !important}.mdl-color--red-200{background-color:#ef9a9a !important}.mdl-color-text--red-300{color:#e57373 !important}.mdl-color--red-300{background-color:#e57373 !important}.mdl-color-text--red-400{color:#ef5350 !important}.mdl-color--red-400{background-color:#ef5350 !important}.mdl-color-text--red-500{color:#f44336 !important}.mdl-color--red-500{background-color:#f44336 !important}.mdl-color-text--red-600{color:#e53935 !important}.mdl-color--red-600{background-color:#e53935 !important}.mdl-color-text--red-700{color:#d32f2f !important}.mdl-color--red-700{background-color:#d32f2f !important}.mdl-color-text--red-800{color:#c62828 !important}.mdl-color--red-800{background-color:#c62828 !important}.mdl-color-text--red-900{color:#b71c1c !important}.mdl-color--red-900{background-color:#b71c1c !important}.mdl-color-text--red-A100{color:#ff8a80 !important}.mdl-color--red-A100{background-color:#ff8a80 !important}.mdl-color-text--red-A200{color:#ff5252 !important}.mdl-color--red-A200{background-color:#ff5252 !important}.mdl-color-text--red-A400{color:#ff1744 !important}.mdl-color--red-A400{background-color:#ff1744 !important}.mdl-color-text--red-A700{color:#d50000 !important}.mdl-color--red-A700{background-color:#d50000 !important}.mdl-color-text--pink{color:#e91e63 !important}.mdl-color--pink{background-color:#e91e63 !important}.mdl-color-text--pink-50{color:#fce4ec !important}.mdl-color--pink-50{background-color:#fce4ec !important}.mdl-color-text--pink-100{color:#f8bbd0 !important}.mdl-color--pink-100{background-color:#f8bbd0 !important}.mdl-color-text--pink-200{color:#f48fb1 !important}.mdl-color--pink-200{background-color:#f48fb1 !important}.mdl-color-text--pink-300{color:#f06292 !important}.mdl-color--pink-300{background-color:#f06292 !important}.mdl-color-text--pink-400{color:#ec407a !important}.mdl-color--pink-400{background-color:#ec407a !important}.mdl-color-text--pink-500{color:#e91e63 !important}.mdl-color--pink-500{background-color:#e91e63 !important}.mdl-color-text--pink-600{color:#d81b60 !important}.mdl-color--pink-600{background-color:#d81b60 !important}.mdl-color-text--pink-700{color:#c2185b !important}.mdl-color--pink-700{background-color:#c2185b !important}.mdl-color-text--pink-800{color:#ad1457 !important}.mdl-color--pink-800{background-color:#ad1457 !important}.mdl-color-text--pink-900{color:#880e4f !important}.mdl-color--pink-900{background-color:#880e4f !important}.mdl-color-text--pink-A100{color:#ff80ab !important}.mdl-color--pink-A100{background-color:#ff80ab !important}.mdl-color-text--pink-A200{color:#ff4081 !important}.mdl-color--pink-A200{background-color:#ff4081 !important}.mdl-color-text--pink-A400{color:#f50057 !important}.mdl-color--pink-A400{background-color:#f50057 !important}.mdl-color-text--pink-A700{color:#c51162 !important}.mdl-color--pink-A700{background-color:#c51162 !important}.mdl-color-text--purple{color:#9c27b0 !important}.mdl-color--purple{background-color:#9c27b0 !important}.mdl-color-text--purple-50{color:#f3e5f5 !important}.mdl-color--purple-50{background-color:#f3e5f5 !important}.mdl-color-text--purple-100{color:#e1bee7 !important}.mdl-color--purple-100{background-color:#e1bee7 !important}.mdl-color-text--purple-200{color:#ce93d8 !important}.mdl-color--purple-200{background-color:#ce93d8 !important}.mdl-color-text--purple-300{color:#ba68c8 !important}.mdl-color--purple-300{background-color:#ba68c8 !important}.mdl-color-text--purple-400{color:#ab47bc !important}.mdl-color--purple-400{background-color:#ab47bc !important}.mdl-color-text--purple-500{color:#9c27b0 !important}.mdl-color--purple-500{background-color:#9c27b0 !important}.mdl-color-text--purple-600{color:#8e24aa !important}.mdl-color--purple-600{background-color:#8e24aa !important}.mdl-color-text--purple-700{color:#7b1fa2 !important}.mdl-color--purple-700{background-color:#7b1fa2 !important}.mdl-color-text--purple-800{color:#6a1b9a !important}.mdl-color--purple-800{background-color:#6a1b9a !important}.mdl-color-text--purple-900{color:#4a148c !important}.mdl-color--purple-900{background-color:#4a148c !important}.mdl-color-text--purple-A100{color:#ea80fc !important}.mdl-color--purple-A100{background-color:#ea80fc !important}.mdl-color-text--purple-A200{color:#e040fb !important}.mdl-color--purple-A200{background-color:#e040fb !important}.mdl-color-text--purple-A400{color:#d500f9 !important}.mdl-color--purple-A400{background-color:#d500f9 !important}.mdl-color-text--purple-A700{color:#a0f !important}.mdl-color--purple-A700{background-color:#a0f !important}.mdl-color-text--deep-purple{color:#673ab7 !important}.mdl-color--deep-purple{background-color:#673ab7 !important}.mdl-color-text--deep-purple-50{color:#ede7f6 !important}.mdl-color--deep-purple-50{background-color:#ede7f6 !important}.mdl-color-text--deep-purple-100{color:#d1c4e9 !important}.mdl-color--deep-purple-100{background-color:#d1c4e9 !important}.mdl-color-text--deep-purple-200{color:#b39ddb !important}.mdl-color--deep-purple-200{background-color:#b39ddb !important}.mdl-color-text--deep-purple-300{color:#9575cd !important}.mdl-color--deep-purple-300{background-color:#9575cd !important}.mdl-color-text--deep-purple-400{color:#7e57c2 !important}.mdl-color--deep-purple-400{background-color:#7e57c2 !important}.mdl-color-text--deep-purple-500{color:#673ab7 !important}.mdl-color--deep-purple-500{background-color:#673ab7 !important}.mdl-color-text--deep-purple-600{color:#5e35b1 !important}.mdl-color--deep-purple-600{background-color:#5e35b1 !important}.mdl-color-text--deep-purple-700{color:#512da8 !important}.mdl-color--deep-purple-700{background-color:#512da8 !important}.mdl-color-text--deep-purple-800{color:#4527a0 !important}.mdl-color--deep-purple-800{background-color:#4527a0 !important}.mdl-color-text--deep-purple-900{color:#311b92 !important}.mdl-color--deep-purple-900{background-color:#311b92 !important}.mdl-color-text--deep-purple-A100{color:#b388ff !important}.mdl-color--deep-purple-A100{background-color:#b388ff !important}.mdl-color-text--deep-purple-A200{color:#7c4dff !important}.mdl-color--deep-purple-A200{background-color:#7c4dff !important}.mdl-color-text--deep-purple-A400{color:#651fff !important}.mdl-color--deep-purple-A400{background-color:#651fff !important}.mdl-color-text--deep-purple-A700{color:#6200ea !important}.mdl-color--deep-purple-A700{background-color:#6200ea !important}.mdl-color-text--indigo{color:#3f51b5 !important}.mdl-color--indigo{background-color:#3f51b5 !important}.mdl-color-text--indigo-50{color:#e8eaf6 !important}.mdl-color--indigo-50{background-color:#e8eaf6 !important}.mdl-color-text--indigo-100{color:#c5cae9 !important}.mdl-color--indigo-100{background-color:#c5cae9 !important}.mdl-color-text--indigo-200{color:#9fa8da !important}.mdl-color--indigo-200{background-color:#9fa8da !important}.mdl-color-text--indigo-300{color:#7986cb !important}.mdl-color--indigo-300{background-color:#7986cb !important}.mdl-color-text--indigo-400{color:#5c6bc0 !important}.mdl-color--indigo-400{background-color:#5c6bc0 !important}.mdl-color-text--indigo-500{color:#3f51b5 !important}.mdl-color--indigo-500{background-color:#3f51b5 !important}.mdl-color-text--indigo-600{color:#3949ab !important}.mdl-color--indigo-600{background-color:#3949ab !important}.mdl-color-text--indigo-700{color:#303f9f !important}.mdl-color--indigo-700{background-color:#303f9f !important}.mdl-color-text--indigo-800{color:#283593 !important}.mdl-color--indigo-800{background-color:#283593 !important}.mdl-color-text--indigo-900{color:#1a237e !important}.mdl-color--indigo-900{background-color:#1a237e !important}.mdl-color-text--indigo-A100{color:#8c9eff !important}.mdl-color--indigo-A100{background-color:#8c9eff !important}.mdl-color-text--indigo-A200{color:#536dfe !important}.mdl-color--indigo-A200{background-color:#536dfe !important}.mdl-color-text--indigo-A400{color:#3d5afe !important}.mdl-color--indigo-A400{background-color:#3d5afe !important}.mdl-color-text--indigo-A700{color:#304ffe !important}.mdl-color--indigo-A700{background-color:#304ffe !important}.mdl-color-text--blue{color:#2196f3 !important}.mdl-color--blue{background-color:#2196f3 !important}.mdl-color-text--blue-50{color:#e3f2fd !important}.mdl-color--blue-50{background-color:#e3f2fd !important}.mdl-color-text--blue-100{color:#bbdefb !important}.mdl-color--blue-100{background-color:#bbdefb !important}.mdl-color-text--blue-200{color:#90caf9 !important}.mdl-color--blue-200{background-color:#90caf9 !important}.mdl-color-text--blue-300{color:#64b5f6 !important}.mdl-color--blue-300{background-color:#64b5f6 !important}.mdl-color-text--blue-400{color:#42a5f5 !important}.mdl-color--blue-400{background-color:#42a5f5 !important}.mdl-color-text--blue-500{color:#2196f3 !important}.mdl-color--blue-500{background-color:#2196f3 !important}.mdl-color-text--blue-600{color:#1e88e5 !important}.mdl-color--blue-600{background-color:#1e88e5 !important}.mdl-color-text--blue-700{color:#1976d2 !important}.mdl-color--blue-700{background-color:#1976d2 !important}.mdl-color-text--blue-800{color:#1565c0 !important}.mdl-color--blue-800{background-color:#1565c0 !important}.mdl-color-text--blue-900{color:#0d47a1 !important}.mdl-color--blue-900{background-color:#0d47a1 !important}.mdl-color-text--blue-A100{color:#82b1ff !important}.mdl-color--blue-A100{background-color:#82b1ff !important}.mdl-color-text--blue-A200{color:#448aff !important}.mdl-color--blue-A200{background-color:#448aff !important}.mdl-color-text--blue-A400{color:#2979ff !important}.mdl-color--blue-A400{background-color:#2979ff !important}.mdl-color-text--blue-A700{color:#2962ff !important}.mdl-color--blue-A700{background-color:#2962ff !important}.mdl-color-text--light-blue{color:#03a9f4 !important}.mdl-color--light-blue{background-color:#03a9f4 !important}.mdl-color-text--light-blue-50{color:#e1f5fe !important}.mdl-color--light-blue-50{background-color:#e1f5fe !important}.mdl-color-text--light-blue-100{color:#b3e5fc !important}.mdl-color--light-blue-100{background-color:#b3e5fc !important}.mdl-color-text--light-blue-200{color:#81d4fa !important}.mdl-color--light-blue-200{background-color:#81d4fa !important}.mdl-color-text--light-blue-300{color:#4fc3f7 !important}.mdl-color--light-blue-300{background-color:#4fc3f7 !important}.mdl-color-text--light-blue-400{color:#29b6f6 !important}.mdl-color--light-blue-400{background-color:#29b6f6 !important}.mdl-color-text--light-blue-500{color:#03a9f4 !important}.mdl-color--light-blue-500{background-color:#03a9f4 !important}.mdl-color-text--light-blue-600{color:#039be5 !important}.mdl-color--light-blue-600{background-color:#039be5 !important}.mdl-color-text--light-blue-700{color:#0288d1 !important}.mdl-color--light-blue-700{background-color:#0288d1 !important}.mdl-color-text--light-blue-800{color:#0277bd !important}.mdl-color--light-blue-800{background-color:#0277bd !important}.mdl-color-text--light-blue-900{color:#01579b !important}.mdl-color--light-blue-900{background-color:#01579b !important}.mdl-color-text--light-blue-A100{color:#80d8ff !important}.mdl-color--light-blue-A100{background-color:#80d8ff !important}.mdl-color-text--light-blue-A200{color:#40c4ff !important}.mdl-color--light-blue-A200{background-color:#40c4ff !important}.mdl-color-text--light-blue-A400{color:#00b0ff !important}.mdl-color--light-blue-A400{background-color:#00b0ff !important}.mdl-color-text--light-blue-A700{color:#0091ea !important}.mdl-color--light-blue-A700{background-color:#0091ea !important}.mdl-color-text--cyan{color:#00bcd4 !important}.mdl-color--cyan{background-color:#00bcd4 !important}.mdl-color-text--cyan-50{color:#e0f7fa !important}.mdl-color--cyan-50{background-color:#e0f7fa !important}.mdl-color-text--cyan-100{color:#b2ebf2 !important}.mdl-color--cyan-100{background-color:#b2ebf2 !important}.mdl-color-text--cyan-200{color:#80deea !important}.mdl-color--cyan-200{background-color:#80deea !important}.mdl-color-text--cyan-300{color:#4dd0e1 !important}.mdl-color--cyan-300{background-color:#4dd0e1 !important}.mdl-color-text--cyan-400{color:#26c6da !important}.mdl-color--cyan-400{background-color:#26c6da !important}.mdl-color-text--cyan-500{color:#00bcd4 !important}.mdl-color--cyan-500{background-color:#00bcd4 !important}.mdl-color-text--cyan-600{color:#00acc1 !important}.mdl-color--cyan-600{background-color:#00acc1 !important}.mdl-color-text--cyan-700{color:#0097a7 !important}.mdl-color--cyan-700{background-color:#0097a7 !important}.mdl-color-text--cyan-800{color:#00838f !important}.mdl-color--cyan-800{background-color:#00838f !important}.mdl-color-text--cyan-900{color:#006064 !important}.mdl-color--cyan-900{background-color:#006064 !important}.mdl-color-text--cyan-A100{color:#84ffff !important}.mdl-color--cyan-A100{background-color:#84ffff !important}.mdl-color-text--cyan-A200{color:#18ffff !important}.mdl-color--cyan-A200{background-color:#18ffff !important}.mdl-color-text--cyan-A400{color:#00e5ff !important}.mdl-color--cyan-A400{background-color:#00e5ff !important}.mdl-color-text--cyan-A700{color:#00b8d4 !important}.mdl-color--cyan-A700{background-color:#00b8d4 !important}.mdl-color-text--teal{color:#009688 !important}.mdl-color--teal{background-color:#009688 !important}.mdl-color-text--teal-50{color:#e0f2f1 !important}.mdl-color--teal-50{background-color:#e0f2f1 !important}.mdl-color-text--teal-100{color:#b2dfdb !important}.mdl-color--teal-100{background-color:#b2dfdb !important}.mdl-color-text--teal-200{color:#80cbc4 !important}.mdl-color--teal-200{background-color:#80cbc4 !important}.mdl-color-text--teal-300{color:#4db6ac !important}.mdl-color--teal-300{background-color:#4db6ac !important}.mdl-color-text--teal-400{color:#26a69a !important}.mdl-color--teal-400{background-color:#26a69a !important}.mdl-color-text--teal-500{color:#009688 !important}.mdl-color--teal-500{background-color:#009688 !important}.mdl-color-text--teal-600{color:#00897b !important}.mdl-color--teal-600{background-color:#00897b !important}.mdl-color-text--teal-700{color:#00796b !important}.mdl-color--teal-700{background-color:#00796b !important}.mdl-color-text--teal-800{color:#00695c !important}.mdl-color--teal-800{background-color:#00695c !important}.mdl-color-text--teal-900{color:#004d40 !important}.mdl-color--teal-900{background-color:#004d40 !important}.mdl-color-text--teal-A100{color:#a7ffeb !important}.mdl-color--teal-A100{background-color:#a7ffeb !important}.mdl-color-text--teal-A200{color:#64ffda !important}.mdl-color--teal-A200{background-color:#64ffda !important}.mdl-color-text--teal-A400{color:#1de9b6 !important}.mdl-color--teal-A400{background-color:#1de9b6 !important}.mdl-color-text--teal-A700{color:#00bfa5 !important}.mdl-color--teal-A700{background-color:#00bfa5 !important}.mdl-color-text--green{color:#4caf50 !important}.mdl-color--green{background-color:#4caf50 !important}.mdl-color-text--green-50{color:#e8f5e9 !important}.mdl-color--green-50{background-color:#e8f5e9 !important}.mdl-color-text--green-100{color:#c8e6c9 !important}.mdl-color--green-100{background-color:#c8e6c9 !important}.mdl-color-text--green-200{color:#a5d6a7 !important}.mdl-color--green-200{background-color:#a5d6a7 !important}.mdl-color-text--green-300{color:#81c784 !important}.mdl-color--green-300{background-color:#81c784 !important}.mdl-color-text--green-400{color:#66bb6a !important}.mdl-color--green-400{background-color:#66bb6a !important}.mdl-color-text--green-500{color:#4caf50 !important}.mdl-color--green-500{background-color:#4caf50 !important}.mdl-color-text--green-600{color:#43a047 !important}.mdl-color--green-600{background-color:#43a047 !important}.mdl-color-text--green-700{color:#388e3c !important}.mdl-color--green-700{background-color:#388e3c !important}.mdl-color-text--green-800{color:#2e7d32 !important}.mdl-color--green-800{background-color:#2e7d32 !important}.mdl-color-text--green-900{color:#1b5e20 !important}.mdl-color--green-900{background-color:#1b5e20 !important}.mdl-color-text--green-A100{color:#b9f6ca !important}.mdl-color--green-A100{background-color:#b9f6ca !important}.mdl-color-text--green-A200{color:#69f0ae !important}.mdl-color--green-A200{background-color:#69f0ae !important}.mdl-color-text--green-A400{color:#00e676 !important}.mdl-color--green-A400{background-color:#00e676 !important}.mdl-color-text--green-A700{color:#00c853 !important}.mdl-color--green-A700{background-color:#00c853 !important}.mdl-color-text--light-green{color:#8bc34a !important}.mdl-color--light-green{background-color:#8bc34a !important}.mdl-color-text--light-green-50{color:#f1f8e9 !important}.mdl-color--light-green-50{background-color:#f1f8e9 !important}.mdl-color-text--light-green-100{color:#dcedc8 !important}.mdl-color--light-green-100{background-color:#dcedc8 !important}.mdl-color-text--light-green-200{color:#c5e1a5 !important}.mdl-color--light-green-200{background-color:#c5e1a5 !important}.mdl-color-text--light-green-300{color:#aed581 !important}.mdl-color--light-green-300{background-color:#aed581 !important}.mdl-color-text--light-green-400{color:#9ccc65 !important}.mdl-color--light-green-400{background-color:#9ccc65 !important}.mdl-color-text--light-green-500{color:#8bc34a !important}.mdl-color--light-green-500{background-color:#8bc34a !important}.mdl-color-text--light-green-600{color:#7cb342 !important}.mdl-color--light-green-600{background-color:#7cb342 !important}.mdl-color-text--light-green-700{color:#689f38 !important}.mdl-color--light-green-700{background-color:#689f38 !important}.mdl-color-text--light-green-800{color:#558b2f !important}.mdl-color--light-green-800{background-color:#558b2f !important}.mdl-color-text--light-green-900{color:#33691e !important}.mdl-color--light-green-900{background-color:#33691e !important}.mdl-color-text--light-green-A100{color:#ccff90 !important}.mdl-color--light-green-A100{background-color:#ccff90 !important}.mdl-color-text--light-green-A200{color:#b2ff59 !important}.mdl-color--light-green-A200{background-color:#b2ff59 !important}.mdl-color-text--light-green-A400{color:#76ff03 !important}.mdl-color--light-green-A400{background-color:#76ff03 !important}.mdl-color-text--light-green-A700{color:#64dd17 !important}.mdl-color--light-green-A700{background-color:#64dd17 !important}.mdl-color-text--lime{color:#cddc39 !important}.mdl-color--lime{background-color:#cddc39 !important}.mdl-color-text--lime-50{color:#f9fbe7 !important}.mdl-color--lime-50{background-color:#f9fbe7 !important}.mdl-color-text--lime-100{color:#f0f4c3 !important}.mdl-color--lime-100{background-color:#f0f4c3 !important}.mdl-color-text--lime-200{color:#e6ee9c !important}.mdl-color--lime-200{background-color:#e6ee9c !important}.mdl-color-text--lime-300{color:#dce775 !important}.mdl-color--lime-300{background-color:#dce775 !important}.mdl-color-text--lime-400{color:#d4e157 !important}.mdl-color--lime-400{background-color:#d4e157 !important}.mdl-color-text--lime-500{color:#cddc39 !important}.mdl-color--lime-500{background-color:#cddc39 !important}.mdl-color-text--lime-600{color:#c0ca33 !important}.mdl-color--lime-600{background-color:#c0ca33 !important}.mdl-color-text--lime-700{color:#afb42b !important}.mdl-color--lime-700{background-color:#afb42b !important}.mdl-color-text--lime-800{color:#9e9d24 !important}.mdl-color--lime-800{background-color:#9e9d24 !important}.mdl-color-text--lime-900{color:#827717 !important}.mdl-color--lime-900{background-color:#827717 !important}.mdl-color-text--lime-A100{color:#f4ff81 !important}.mdl-color--lime-A100{background-color:#f4ff81 !important}.mdl-color-text--lime-A200{color:#eeff41 !important}.mdl-color--lime-A200{background-color:#eeff41 !important}.mdl-color-text--lime-A400{color:#c6ff00 !important}.mdl-color--lime-A400{background-color:#c6ff00 !important}.mdl-color-text--lime-A700{color:#aeea00 !important}.mdl-color--lime-A700{background-color:#aeea00 !important}.mdl-color-text--yellow{color:#ffeb3b !important}.mdl-color--yellow{background-color:#ffeb3b !important}.mdl-color-text--yellow-50{color:#fffde7 !important}.mdl-color--yellow-50{background-color:#fffde7 !important}.mdl-color-text--yellow-100{color:#fff9c4 !important}.mdl-color--yellow-100{background-color:#fff9c4 !important}.mdl-color-text--yellow-200{color:#fff59d !important}.mdl-color--yellow-200{background-color:#fff59d !important}.mdl-color-text--yellow-300{color:#fff176 !important}.mdl-color--yellow-300{background-color:#fff176 !important}.mdl-color-text--yellow-400{color:#ffee58 !important}.mdl-color--yellow-400{background-color:#ffee58 !important}.mdl-color-text--yellow-500{color:#ffeb3b !important}.mdl-color--yellow-500{background-color:#ffeb3b !important}.mdl-color-text--yellow-600{color:#fdd835 !important}.mdl-color--yellow-600{background-color:#fdd835 !important}.mdl-color-text--yellow-700{color:#fbc02d !important}.mdl-color--yellow-700{background-color:#fbc02d !important}.mdl-color-text--yellow-800{color:#f9a825 !important}.mdl-color--yellow-800{background-color:#f9a825 !important}.mdl-color-text--yellow-900{color:#f57f17 !important}.mdl-color--yellow-900{background-color:#f57f17 !important}.mdl-color-text--yellow-A100{color:#ffff8d !important}.mdl-color--yellow-A100{background-color:#ffff8d !important}.mdl-color-text--yellow-A200{color:#ff0 !important}.mdl-color--yellow-A200{background-color:#ff0 !important}.mdl-color-text--yellow-A400{color:#ffea00 !important}.mdl-color--yellow-A400{background-color:#ffea00 !important}.mdl-color-text--yellow-A700{color:#ffd600 !important}.mdl-color--yellow-A700{background-color:#ffd600 !important}.mdl-color-text--amber{color:#ffc107 !important}.mdl-color--amber{background-color:#ffc107 !important}.mdl-color-text--amber-50{color:#fff8e1 !important}.mdl-color--amber-50{background-color:#fff8e1 !important}.mdl-color-text--amber-100{color:#ffecb3 !important}.mdl-color--amber-100{background-color:#ffecb3 !important}.mdl-color-text--amber-200{color:#ffe082 !important}.mdl-color--amber-200{background-color:#ffe082 !important}.mdl-color-text--amber-300{color:#ffd54f !important}.mdl-color--amber-300{background-color:#ffd54f !important}.mdl-color-text--amber-400{color:#ffca28 !important}.mdl-color--amber-400{background-color:#ffca28 !important}.mdl-color-text--amber-500{color:#ffc107 !important}.mdl-color--amber-500{background-color:#ffc107 !important}.mdl-color-text--amber-600{color:#ffb300 !important}.mdl-color--amber-600{background-color:#ffb300 !important}.mdl-color-text--amber-700{color:#ffa000 !important}.mdl-color--amber-700{background-color:#ffa000 !important}.mdl-color-text--amber-800{color:#ff8f00 !important}.mdl-color--amber-800{background-color:#ff8f00 !important}.mdl-color-text--amber-900{color:#ff6f00 !important}.mdl-color--amber-900{background-color:#ff6f00 !important}.mdl-color-text--amber-A100{color:#ffe57f !important}.mdl-color--amber-A100{background-color:#ffe57f !important}.mdl-color-text--amber-A200{color:#ffd740 !important}.mdl-color--amber-A200{background-color:#ffd740 !important}.mdl-color-text--amber-A400{color:#ffc400 !important}.mdl-color--amber-A400{background-color:#ffc400 !important}.mdl-color-text--amber-A700{color:#ffab00 !important}.mdl-color--amber-A700{background-color:#ffab00 !important}.mdl-color-text--orange{color:#ff9800 !important}.mdl-color--orange{background-color:#ff9800 !important}.mdl-color-text--orange-50{color:#fff3e0 !important}.mdl-color--orange-50{background-color:#fff3e0 !important}.mdl-color-text--orange-100{color:#ffe0b2 !important}.mdl-color--orange-100{background-color:#ffe0b2 !important}.mdl-color-text--orange-200{color:#ffcc80 !important}.mdl-color--orange-200{background-color:#ffcc80 !important}.mdl-color-text--orange-300{color:#ffb74d !important}.mdl-color--orange-300{background-color:#ffb74d !important}.mdl-color-text--orange-400{color:#ffa726 !important}.mdl-color--orange-400{background-color:#ffa726 !important}.mdl-color-text--orange-500{color:#ff9800 !important}.mdl-color--orange-500{background-color:#ff9800 !important}.mdl-color-text--orange-600{color:#fb8c00 !important}.mdl-color--orange-600{background-color:#fb8c00 !important}.mdl-color-text--orange-700{color:#f57c00 !important}.mdl-color--orange-700{background-color:#f57c00 !important}.mdl-color-text--orange-800{color:#ef6c00 !important}.mdl-color--orange-800{background-color:#ef6c00 !important}.mdl-color-text--orange-900{color:#e65100 !important}.mdl-color--orange-900{background-color:#e65100 !important}.mdl-color-text--orange-A100{color:#ffd180 !important}.mdl-color--orange-A100{background-color:#ffd180 !important}.mdl-color-text--orange-A200{color:#ffab40 !important}.mdl-color--orange-A200{background-color:#ffab40 !important}.mdl-color-text--orange-A400{color:#ff9100 !important}.mdl-color--orange-A400{background-color:#ff9100 !important}.mdl-color-text--orange-A700{color:#ff6d00 !important}.mdl-color--orange-A700{background-color:#ff6d00 !important}.mdl-color-text--deep-orange{color:#ff5722 !important}.mdl-color--deep-orange{background-color:#ff5722 !important}.mdl-color-text--deep-orange-50{color:#fbe9e7 !important}.mdl-color--deep-orange-50{background-color:#fbe9e7 !important}.mdl-color-text--deep-orange-100{color:#ffccbc !important}.mdl-color--deep-orange-100{background-color:#ffccbc !important}.mdl-color-text--deep-orange-200{color:#ffab91 !important}.mdl-color--deep-orange-200{background-color:#ffab91 !important}.mdl-color-text--deep-orange-300{color:#ff8a65 !important}.mdl-color--deep-orange-300{background-color:#ff8a65 !important}.mdl-color-text--deep-orange-400{color:#ff7043 !important}.mdl-color--deep-orange-400{background-color:#ff7043 !important}.mdl-color-text--deep-orange-500{color:#ff5722 !important}.mdl-color--deep-orange-500{background-color:#ff5722 !important}.mdl-color-text--deep-orange-600{color:#f4511e !important}.mdl-color--deep-orange-600{background-color:#f4511e !important}.mdl-color-text--deep-orange-700{color:#e64a19 !important}.mdl-color--deep-orange-700{background-color:#e64a19 !important}.mdl-color-text--deep-orange-800{color:#d84315 !important}.mdl-color--deep-orange-800{background-color:#d84315 !important}.mdl-color-text--deep-orange-900{color:#bf360c !important}.mdl-color--deep-orange-900{background-color:#bf360c !important}.mdl-color-text--deep-orange-A100{color:#ff9e80 !important}.mdl-color--deep-orange-A100{background-color:#ff9e80 !important}.mdl-color-text--deep-orange-A200{color:#ff6e40 !important}.mdl-color--deep-orange-A200{background-color:#ff6e40 !important}.mdl-color-text--deep-orange-A400{color:#ff3d00 !important}.mdl-color--deep-orange-A400{background-color:#ff3d00 !important}.mdl-color-text--deep-orange-A700{color:#dd2c00 !important}.mdl-color--deep-orange-A700{background-color:#dd2c00 !important}.mdl-color-text--brown{color:#795548 !important}.mdl-color--brown{background-color:#795548 !important}.mdl-color-text--brown-50{color:#efebe9 !important}.mdl-color--brown-50{background-color:#efebe9 !important}.mdl-color-text--brown-100{color:#d7ccc8 !important}.mdl-color--brown-100{background-color:#d7ccc8 !important}.mdl-color-text--brown-200{color:#bcaaa4 !important}.mdl-color--brown-200{background-color:#bcaaa4 !important}.mdl-color-text--brown-300{color:#a1887f !important}.mdl-color--brown-300{background-color:#a1887f !important}.mdl-color-text--brown-400{color:#8d6e63 !important}.mdl-color--brown-400{background-color:#8d6e63 !important}.mdl-color-text--brown-500{color:#795548 !important}.mdl-color--brown-500{background-color:#795548 !important}.mdl-color-text--brown-600{color:#6d4c41 !important}.mdl-color--brown-600{background-color:#6d4c41 !important}.mdl-color-text--brown-700{color:#5d4037 !important}.mdl-color--brown-700{background-color:#5d4037 !important}.mdl-color-text--brown-800{color:#4e342e !important}.mdl-color--brown-800{background-color:#4e342e !important}.mdl-color-text--brown-900{color:#3e2723 !important}.mdl-color--brown-900{background-color:#3e2723 !important}.mdl-color-text--grey{color:#9e9e9e !important}.mdl-color--grey{background-color:#9e9e9e !important}.mdl-color-text--grey-50{color:#fafafa !important}.mdl-color--grey-50{background-color:#fafafa !important}.mdl-color-text--grey-100{color:#f5f5f5 !important}.mdl-color--grey-100{background-color:#f5f5f5 !important}.mdl-color-text--grey-200{color:#eee !important}.mdl-color--grey-200{background-color:#eee !important}.mdl-color-text--grey-300{color:#e0e0e0 !important}.mdl-color--grey-300{background-color:#e0e0e0 !important}.mdl-color-text--grey-400{color:#bdbdbd !important}.mdl-color--grey-400{background-color:#bdbdbd !important}.mdl-color-text--grey-500{color:#9e9e9e !important}.mdl-color--grey-500{background-color:#9e9e9e !important}.mdl-color-text--grey-600{color:#757575 !important}.mdl-color--grey-600{background-color:#757575 !important}.mdl-color-text--grey-700{color:#616161 !important}.mdl-color--grey-700{background-color:#616161 !important}.mdl-color-text--grey-800{color:#424242 !important}.mdl-color--grey-800{background-color:#424242 !important}.mdl-color-text--grey-900{color:#212121 !important}.mdl-color--grey-900{background-color:#212121 !important}.mdl-color-text--blue-grey{color:#607d8b !important}.mdl-color--blue-grey{background-color:#607d8b !important}.mdl-color-text--blue-grey-50{color:#eceff1 !important}.mdl-color--blue-grey-50{background-color:#eceff1 !important}.mdl-color-text--blue-grey-100{color:#cfd8dc !important}.mdl-color--blue-grey-100{background-color:#cfd8dc !important}.mdl-color-text--blue-grey-200{color:#b0bec5 !important}.mdl-color--blue-grey-200{background-color:#b0bec5 !important}.mdl-color-text--blue-grey-300{color:#90a4ae !important}.mdl-color--blue-grey-300{background-color:#90a4ae !important}.mdl-color-text--blue-grey-400{color:#78909c !important}.mdl-color--blue-grey-400{background-color:#78909c !important}.mdl-color-text--blue-grey-500{color:#607d8b !important}.mdl-color--blue-grey-500{background-color:#607d8b !important}.mdl-color-text--blue-grey-600{color:#546e7a !important}.mdl-color--blue-grey-600{background-color:#546e7a !important}.mdl-color-text--blue-grey-700{color:#455a64 !important}.mdl-color--blue-grey-700{background-color:#455a64 !important}.mdl-color-text--blue-grey-800{color:#37474f !important}.mdl-color--blue-grey-800{background-color:#37474f !important}.mdl-color-text--blue-grey-900{color:#263238 !important}.mdl-color--blue-grey-900{background-color:#263238 !important}.mdl-color--black{background-color:#000 !important}.mdl-color-text--black{color:#000 !important}.mdl-color--white{background-color:#fff !important}.mdl-color-text--white{color:#fff !important}.mdl-color--primary{background-color:#3f51b5 !important}.mdl-color--primary-contrast{background-color:#fff !important}.mdl-color--primary-dark{background-color:#303f9f !important}.mdl-color--accent{background-color:#ff4081 !important}.mdl-color--accent-contrast{background-color:#fff !important}.mdl-color-text--primary{color:#3f51b5 !important}.mdl-color-text--primary-contrast{color:#fff !important}.mdl-color-text--primary-dark{color:#303f9f !important}.mdl-color-text--accent{color:#ff4081 !important}.mdl-color-text--accent-contrast{color:#fff !important}.mdl-ripple{background:#000;border-radius:50%;height:50px;left:0;opacity:0;pointer-events:none;position:absolute;top:0;-webkit-transform:translate(-50%,-50%);transform:translate(-50%,-50%);width:50px;overflow:hidden}.mdl-ripple.is-animating{transition:transform .3s cubic-bezier(0,0,.2,1),width .3s cubic-bezier(0,0,.2,1),height .3s cubic-bezier(0,0,.2,1),opacity .6s cubic-bezier(0,0,.2,1);transition:transform .3s cubic-bezier(0,0,.2,1),width .3s cubic-bezier(0,0,.2,1),height .3s cubic-bezier(0,0,.2,1),opacity .6s cubic-bezier(0,0,.2,1),-webkit-transform .3s cubic-bezier(0,0,.2,1)}.mdl-ripple.is-visible{opacity:.3}.mdl-animation--default,.mdl-animation--fast-out-slow-in{transition-timing-function:cubic-bezier(.4,0,.2,1)}.mdl-animation--linear-out-slow-in{transition-timing-function:cubic-bezier(0,0,.2,1)}.mdl-animation--fast-out-linear-in{transition-timing-function:cubic-bezier(.4,0,1,1)}.mdl-badge{position:relative;white-space:nowrap;margin-right:24px}.mdl-badge:not([data-badge]){margin-right:auto}.mdl-badge[data-badge]:after{content:attr(data-badge);display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-flex-wrap:wrap;-ms-flex-wrap:wrap;flex-wrap:wrap;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;-webkit-align-content:center;-ms-flex-line-pack:center;align-content:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;position:absolute;top:-11px;right:-24px;font-family:"Roboto","Helvetica","Arial",sans-serif;font-weight:600;font-size:12px;width:22px;height:22px;border-radius:50%;background:#ff4081;color:#fff}.mdl-button .mdl-badge[data-badge]:after{top:-10px;right:-5px}.mdl-badge.mdl-badge--no-background[data-badge]:after{color:#ff4081;background:rgba(255,255,255,.2);box-shadow:0 0 1px gray}.mdl-badge.mdl-badge--overlap{margin-right:10px}.mdl-badge.mdl-badge--overlap:after{right:-10px}.mdl-button{background:0 0;border:none;border-radius:2px;color:#000;position:relative;height:36px;margin:0;min-width:64px;padding:0 16px;display:inline-block;font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:14px;font-weight:500;text-transform:uppercase;letter-spacing:0;overflow:hidden;will-change:box-shadow;transition:box-shadow .2s cubic-bezier(.4,0,1,1),background-color .2s cubic-bezier(.4,0,.2,1),color .2s cubic-bezier(.4,0,.2,1);outline:none;cursor:pointer;text-decoration:none;text-align:center;line-height:36px;vertical-align:middle}.mdl-button::-moz-focus-inner{border:0}.mdl-button:hover{background-color:rgba(158,158,158,.2)}.mdl-button:focus:not(:active){background-color:rgba(0,0,0,.12)}.mdl-button:active{background-color:rgba(158,158,158,.4)}.mdl-button.mdl-button--colored{color:#3f51b5}.mdl-button.mdl-button--colored:focus:not(:active){background-color:rgba(0,0,0,.12)}input.mdl-button[type="submit"]{-webkit-appearance:none}.mdl-button--raised{background:rgba(158,158,158,.2);box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12)}.mdl-button--raised:active{box-shadow:0 4px 5px 0 rgba(0,0,0,.14),0 1px 10px 0 rgba(0,0,0,.12),0 2px 4px -1px rgba(0,0,0,.2);background-color:rgba(158,158,158,.4)}.mdl-button--raised:focus:not(:active){box-shadow:0 0 8px rgba(0,0,0,.18),0 8px 16px rgba(0,0,0,.36);background-color:rgba(158,158,158,.4)}.mdl-button--raised.mdl-button--colored{background:#3f51b5;color:#fff}.mdl-button--raised.mdl-button--colored:hover{background-color:#3f51b5}.mdl-button--raised.mdl-button--colored:active{background-color:#3f51b5}.mdl-button--raised.mdl-button--colored:focus:not(:active){background-color:#3f51b5}.mdl-button--raised.mdl-button--colored .mdl-ripple{background:#fff}.mdl-button--fab{border-radius:50%;font-size:24px;height:56px;margin:auto;min-width:56px;width:56px;padding:0;overflow:hidden;background:rgba(158,158,158,.2);box-shadow:0 1px 1.5px 0 rgba(0,0,0,.12),0 1px 1px 0 rgba(0,0,0,.24);position:relative;line-height:normal}.mdl-button--fab .material-icons{position:absolute;top:50%;left:50%;-webkit-transform:translate(-12px,-12px);transform:translate(-12px,-12px);line-height:24px;width:24px}.mdl-button--fab.mdl-button--mini-fab{height:40px;min-width:40px;width:40px}.mdl-button--fab .mdl-button__ripple-container{border-radius:50%;-webkit-mask-image:-webkit-radial-gradient(circle,#fff,#000)}.mdl-button--fab:active{box-shadow:0 4px 5px 0 rgba(0,0,0,.14),0 1px 10px 0 rgba(0,0,0,.12),0 2px 4px -1px rgba(0,0,0,.2);background-color:rgba(158,158,158,.4)}.mdl-button--fab:focus:not(:active){box-shadow:0 0 8px rgba(0,0,0,.18),0 8px 16px rgba(0,0,0,.36);background-color:rgba(158,158,158,.4)}.mdl-button--fab.mdl-button--colored{background:#ff4081;color:#fff}.mdl-button--fab.mdl-button--colored:hover{background-color:#ff4081}.mdl-button--fab.mdl-button--colored:focus:not(:active){background-color:#ff4081}.mdl-button--fab.mdl-button--colored:active{background-color:#ff4081}.mdl-button--fab.mdl-button--colored .mdl-ripple{background:#fff}.mdl-button--icon{border-radius:50%;font-size:24px;height:32px;margin-left:0;margin-right:0;min-width:32px;width:32px;padding:0;overflow:hidden;color:inherit;line-height:normal}.mdl-button--icon .material-icons{position:absolute;top:50%;left:50%;-webkit-transform:translate(-12px,-12px);transform:translate(-12px,-12px);line-height:24px;width:24px}.mdl-button--icon.mdl-button--mini-icon{height:24px;min-width:24px;width:24px}.mdl-button--icon.mdl-button--mini-icon .material-icons{top:0;left:0}.mdl-button--icon .mdl-button__ripple-container{border-radius:50%;-webkit-mask-image:-webkit-radial-gradient(circle,#fff,#000)}.mdl-button__ripple-container{display:block;height:100%;left:0;position:absolute;top:0;width:100%;z-index:0;overflow:hidden}.mdl-button[disabled] .mdl-button__ripple-container .mdl-ripple,.mdl-button.mdl-button--disabled .mdl-button__ripple-container .mdl-ripple{background-color:transparent}.mdl-button--primary.mdl-button--primary{color:#3f51b5}.mdl-button--primary.mdl-button--primary .mdl-ripple{background:#fff}.mdl-button--primary.mdl-button--primary.mdl-button--raised,.mdl-button--primary.mdl-button--primary.mdl-button--fab{color:#fff;background-color:#3f51b5}.mdl-button--accent.mdl-button--accent{color:#ff4081}.mdl-button--accent.mdl-button--accent .mdl-ripple{background:#fff}.mdl-button--accent.mdl-button--accent.mdl-button--raised,.mdl-button--accent.mdl-button--accent.mdl-button--fab{color:#fff;background-color:#ff4081}.mdl-button[disabled][disabled],.mdl-button.mdl-button--disabled.mdl-button--disabled{color:rgba(0,0,0,.26);cursor:default;background-color:transparent}.mdl-button--fab[disabled][disabled],.mdl-button--fab.mdl-button--disabled.mdl-button--disabled{background-color:rgba(0,0,0,.12);color:rgba(0,0,0,.26)}.mdl-button--raised[disabled][disabled],.mdl-button--raised.mdl-button--disabled.mdl-button--disabled{background-color:rgba(0,0,0,.12);color:rgba(0,0,0,.26);box-shadow:none}.mdl-button--colored[disabled][disabled],.mdl-button--colored.mdl-button--disabled.mdl-button--disabled{color:rgba(0,0,0,.26)}.mdl-button .material-icons{vertical-align:middle}.mdl-card{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;font-size:16px;font-weight:400;min-height:200px;overflow:hidden;width:330px;z-index:1;position:relative;background:#fff;border-radius:2px;box-sizing:border-box}.mdl-card__media{background-color:#ff4081;background-repeat:repeat;background-position:50% 50%;background-size:cover;background-origin:padding-box;background-attachment:scroll;box-sizing:border-box}.mdl-card__title{-webkit-align-items:center;-ms-flex-align:center;align-items:center;color:#000;display:block;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-justify-content:stretch;-ms-flex-pack:stretch;justify-content:stretch;line-height:normal;padding:16px;-webkit-perspective-origin:165px 56px;perspective-origin:165px 56px;-webkit-transform-origin:165px 56px;transform-origin:165px 56px;box-sizing:border-box}.mdl-card__title.mdl-card--border{border-bottom:1px solid rgba(0,0,0,.1)}.mdl-card__title-text{-webkit-align-self:flex-end;-ms-flex-item-align:end;align-self:flex-end;color:inherit;display:block;display:-webkit-flex;display:-ms-flexbox;display:flex;font-size:24px;font-weight:300;line-height:normal;overflow:hidden;-webkit-transform-origin:149px 48px;transform-origin:149px 48px;margin:0}.mdl-card__subtitle-text{font-size:14px;color:rgba(0,0,0,.54);margin:0}.mdl-card__supporting-text{color:rgba(0,0,0,.54);font-size:1rem;line-height:18px;overflow:hidden;padding:16px;width:90%}.mdl-card__actions{font-size:16px;line-height:normal;width:100%;background-color:transparent;padding:8px;box-sizing:border-box}.mdl-card__actions.mdl-card--border{border-top:1px solid rgba(0,0,0,.1)}.mdl-card--expand{-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1}.mdl-card__menu{position:absolute;right:16px;top:16px}.mdl-checkbox{position:relative;z-index:1;vertical-align:middle;display:inline-block;box-sizing:border-box;width:100%;height:24px;margin:0;padding:0}.mdl-checkbox.is-upgraded{padding-left:24px}.mdl-checkbox__input{line-height:24px}.mdl-checkbox.is-upgraded .mdl-checkbox__input{position:absolute;width:0;height:0;margin:0;padding:0;opacity:0;-ms-appearance:none;-moz-appearance:none;-webkit-appearance:none;appearance:none;border:none}.mdl-checkbox__box-outline{position:absolute;top:3px;left:0;display:inline-block;box-sizing:border-box;width:16px;height:16px;margin:0;cursor:pointer;overflow:hidden;border:2px solid rgba(0,0,0,.54);border-radius:2px;z-index:2}.mdl-checkbox.is-checked .mdl-checkbox__box-outline{border:2px solid #3f51b5}fieldset[disabled] .mdl-checkbox .mdl-checkbox__box-outline,.mdl-checkbox.is-disabled .mdl-checkbox__box-outline{border:2px solid rgba(0,0,0,.26);cursor:auto}.mdl-checkbox__focus-helper{position:absolute;top:3px;left:0;display:inline-block;box-sizing:border-box;width:16px;height:16px;border-radius:50%;background-color:transparent}.mdl-checkbox.is-focused .mdl-checkbox__focus-helper{box-shadow:0 0 0 8px rgba(0,0,0,.1);background-color:rgba(0,0,0,.1)}.mdl-checkbox.is-focused.is-checked .mdl-checkbox__focus-helper{box-shadow:0 0 0 8px rgba(63,81,181,.26);background-color:rgba(63,81,181,.26)}.mdl-checkbox__tick-outline{position:absolute;top:0;left:0;height:100%;width:100%;-webkit-mask:url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgdmVyc2lvbj0iMS4xIgogICB2aWV3Qm94PSIwIDAgMSAxIgogICBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWluWU1pbiBtZWV0Ij4KICA8ZGVmcz4KICAgIDxjbGlwUGF0aCBpZD0iY2xpcCI+CiAgICAgIDxwYXRoCiAgICAgICAgIGQ9Ik0gMCwwIDAsMSAxLDEgMSwwIDAsMCB6IE0gMC44NTM0Mzc1LDAuMTY3MTg3NSAwLjk1OTY4NzUsMC4yNzMxMjUgMC40MjkzNzUsMC44MDM0Mzc1IDAuMzIzMTI1LDAuOTA5Njg3NSAwLjIxNzE4NzUsMC44MDM0Mzc1IDAuMDQwMzEyNSwwLjYyNjg3NSAwLjE0NjU2MjUsMC41MjA2MjUgMC4zMjMxMjUsMC42OTc1IDAuODUzNDM3NSwwLjE2NzE4NzUgeiIKICAgICAgICAgc3R5bGU9ImZpbGw6I2ZmZmZmZjtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZSIgLz4KICAgIDwvY2xpcFBhdGg+CiAgICA8bWFzayBpZD0ibWFzayIgbWFza1VuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgbWFza0NvbnRlbnRVbml0cz0ib2JqZWN0Qm91bmRpbmdCb3giPgogICAgICA8cGF0aAogICAgICAgICBkPSJNIDAsMCAwLDEgMSwxIDEsMCAwLDAgeiBNIDAuODUzNDM3NSwwLjE2NzE4NzUgMC45NTk2ODc1LDAuMjczMTI1IDAuNDI5Mzc1LDAuODAzNDM3NSAwLjMyMzEyNSwwLjkwOTY4NzUgMC4yMTcxODc1LDAuODAzNDM3NSAwLjA0MDMxMjUsMC42MjY4NzUgMC4xNDY1NjI1LDAuNTIwNjI1IDAuMzIzMTI1LDAuNjk3NSAwLjg1MzQzNzUsMC4xNjcxODc1IHoiCiAgICAgICAgIHN0eWxlPSJmaWxsOiNmZmZmZmY7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmUiIC8+CiAgICA8L21hc2s+CiAgPC9kZWZzPgogIDxyZWN0CiAgICAgd2lkdGg9IjEiCiAgICAgaGVpZ2h0PSIxIgogICAgIHg9IjAiCiAgICAgeT0iMCIKICAgICBjbGlwLXBhdGg9InVybCgjY2xpcCkiCiAgICAgc3R5bGU9ImZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZSIgLz4KPC9zdmc+Cg==");mask:url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgdmVyc2lvbj0iMS4xIgogICB2aWV3Qm94PSIwIDAgMSAxIgogICBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWluWU1pbiBtZWV0Ij4KICA8ZGVmcz4KICAgIDxjbGlwUGF0aCBpZD0iY2xpcCI+CiAgICAgIDxwYXRoCiAgICAgICAgIGQ9Ik0gMCwwIDAsMSAxLDEgMSwwIDAsMCB6IE0gMC44NTM0Mzc1LDAuMTY3MTg3NSAwLjk1OTY4NzUsMC4yNzMxMjUgMC40MjkzNzUsMC44MDM0Mzc1IDAuMzIzMTI1LDAuOTA5Njg3NSAwLjIxNzE4NzUsMC44MDM0Mzc1IDAuMDQwMzEyNSwwLjYyNjg3NSAwLjE0NjU2MjUsMC41MjA2MjUgMC4zMjMxMjUsMC42OTc1IDAuODUzNDM3NSwwLjE2NzE4NzUgeiIKICAgICAgICAgc3R5bGU9ImZpbGw6I2ZmZmZmZjtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZSIgLz4KICAgIDwvY2xpcFBhdGg+CiAgICA8bWFzayBpZD0ibWFzayIgbWFza1VuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgbWFza0NvbnRlbnRVbml0cz0ib2JqZWN0Qm91bmRpbmdCb3giPgogICAgICA8cGF0aAogICAgICAgICBkPSJNIDAsMCAwLDEgMSwxIDEsMCAwLDAgeiBNIDAuODUzNDM3NSwwLjE2NzE4NzUgMC45NTk2ODc1LDAuMjczMTI1IDAuNDI5Mzc1LDAuODAzNDM3NSAwLjMyMzEyNSwwLjkwOTY4NzUgMC4yMTcxODc1LDAuODAzNDM3NSAwLjA0MDMxMjUsMC42MjY4NzUgMC4xNDY1NjI1LDAuNTIwNjI1IDAuMzIzMTI1LDAuNjk3NSAwLjg1MzQzNzUsMC4xNjcxODc1IHoiCiAgICAgICAgIHN0eWxlPSJmaWxsOiNmZmZmZmY7ZmlsbC1vcGFjaXR5OjE7c3Ryb2tlOm5vbmUiIC8+CiAgICA8L21hc2s+CiAgPC9kZWZzPgogIDxyZWN0CiAgICAgd2lkdGg9IjEiCiAgICAgaGVpZ2h0PSIxIgogICAgIHg9IjAiCiAgICAgeT0iMCIKICAgICBjbGlwLXBhdGg9InVybCgjY2xpcCkiCiAgICAgc3R5bGU9ImZpbGw6IzAwMDAwMDtmaWxsLW9wYWNpdHk6MTtzdHJva2U6bm9uZSIgLz4KPC9zdmc+Cg==");background:0 0;transition-duration:.28s;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-property:background}.mdl-checkbox.is-checked .mdl-checkbox__tick-outline{background:#3f51b5 url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgdmVyc2lvbj0iMS4xIgogICB2aWV3Qm94PSIwIDAgMSAxIgogICBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWluWU1pbiBtZWV0Ij4KICA8cGF0aAogICAgIGQ9Ik0gMC4wNDAzODA1OSwwLjYyNjc3NjcgMC4xNDY0NDY2MSwwLjUyMDcxMDY4IDAuNDI5Mjg5MzIsMC44MDM1NTMzOSAwLjMyMzIyMzMsMC45MDk2MTk0MSB6IE0gMC4yMTcxNTcyOSwwLjgwMzU1MzM5IDAuODUzNTUzMzksMC4xNjcxNTcyOSAwLjk1OTYxOTQxLDAuMjczMjIzMyAwLjMyMzIyMzMsMC45MDk2MTk0MSB6IgogICAgIGlkPSJyZWN0Mzc4MCIKICAgICBzdHlsZT0iZmlsbDojZmZmZmZmO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lIiAvPgo8L3N2Zz4K")}fieldset[disabled] .mdl-checkbox.is-checked .mdl-checkbox__tick-outline,.mdl-checkbox.is-checked.is-disabled .mdl-checkbox__tick-outline{background:rgba(0,0,0,.26)url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgdmVyc2lvbj0iMS4xIgogICB2aWV3Qm94PSIwIDAgMSAxIgogICBwcmVzZXJ2ZUFzcGVjdFJhdGlvPSJ4TWluWU1pbiBtZWV0Ij4KICA8cGF0aAogICAgIGQ9Ik0gMC4wNDAzODA1OSwwLjYyNjc3NjcgMC4xNDY0NDY2MSwwLjUyMDcxMDY4IDAuNDI5Mjg5MzIsMC44MDM1NTMzOSAwLjMyMzIyMzMsMC45MDk2MTk0MSB6IE0gMC4yMTcxNTcyOSwwLjgwMzU1MzM5IDAuODUzNTUzMzksMC4xNjcxNTcyOSAwLjk1OTYxOTQxLDAuMjczMjIzMyAwLjMyMzIyMzMsMC45MDk2MTk0MSB6IgogICAgIGlkPSJyZWN0Mzc4MCIKICAgICBzdHlsZT0iZmlsbDojZmZmZmZmO2ZpbGwtb3BhY2l0eToxO3N0cm9rZTpub25lIiAvPgo8L3N2Zz4K")}.mdl-checkbox__label{position:relative;cursor:pointer;font-size:16px;line-height:24px;margin:0}fieldset[disabled] .mdl-checkbox .mdl-checkbox__label,.mdl-checkbox.is-disabled .mdl-checkbox__label{color:rgba(0,0,0,.26);cursor:auto}.mdl-checkbox__ripple-container{position:absolute;z-index:2;top:-6px;left:-10px;box-sizing:border-box;width:36px;height:36px;border-radius:50%;cursor:pointer;overflow:hidden;-webkit-mask-image:-webkit-radial-gradient(circle,#fff,#000)}.mdl-checkbox__ripple-container .mdl-ripple{background:#3f51b5}fieldset[disabled] .mdl-checkbox .mdl-checkbox__ripple-container,.mdl-checkbox.is-disabled .mdl-checkbox__ripple-container{cursor:auto}fieldset[disabled] .mdl-checkbox .mdl-checkbox__ripple-container .mdl-ripple,.mdl-checkbox.is-disabled .mdl-checkbox__ripple-container .mdl-ripple{background:0 0}.mdl-data-table{position:relative;border:1px solid rgba(0,0,0,.12);border-collapse:collapse;white-space:nowrap;font-size:13px;background-color:#fff}.mdl-data-table thead{padding-bottom:3px}.mdl-data-table thead .mdl-data-table__select{margin-top:0}.mdl-data-table tbody tr{position:relative;height:48px;transition-duration:.28s;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-property:background-color}.mdl-data-table tbody tr.is-selected{background-color:#e0e0e0}.mdl-data-table tbody tr:hover{background-color:#eee}.mdl-data-table td{text-align:right}.mdl-data-table th{padding:0 18px 12px 18px;text-align:right}.mdl-data-table td:first-of-type,.mdl-data-table th:first-of-type{padding-left:24px}.mdl-data-table td:last-of-type,.mdl-data-table th:last-of-type{padding-right:24px}.mdl-data-table td{position:relative;height:48px;border-top:1px solid rgba(0,0,0,.12);border-bottom:1px solid rgba(0,0,0,.12);padding:12px 18px;box-sizing:border-box}.mdl-data-table td,.mdl-data-table td .mdl-data-table__select{vertical-align:middle}.mdl-data-table th{position:relative;vertical-align:bottom;text-overflow:ellipsis;font-weight:700;line-height:24px;letter-spacing:0;height:48px;font-size:12px;color:rgba(0,0,0,.54);padding-bottom:8px;box-sizing:border-box}.mdl-data-table th.mdl-data-table__header--sorted-ascending,.mdl-data-table th.mdl-data-table__header--sorted-descending{color:rgba(0,0,0,.87)}.mdl-data-table th.mdl-data-table__header--sorted-ascending:before,.mdl-data-table th.mdl-data-table__header--sorted-descending:before{font-family:'Material Icons';font-weight:400;font-style:normal;line-height:1;letter-spacing:normal;text-transform:none;display:inline-block;word-wrap:normal;-moz-font-feature-settings:'liga';font-feature-settings:'liga';-webkit-font-feature-settings:'liga';-webkit-font-smoothing:antialiased;font-size:16px;content:"\e5d8";margin-right:5px;vertical-align:sub}.mdl-data-table th.mdl-data-table__header--sorted-ascending:hover,.mdl-data-table th.mdl-data-table__header--sorted-descending:hover{cursor:pointer}.mdl-data-table th.mdl-data-table__header--sorted-ascending:hover:before,.mdl-data-table th.mdl-data-table__header--sorted-descending:hover:before{color:rgba(0,0,0,.26)}.mdl-data-table th.mdl-data-table__header--sorted-descending:before{content:"\e5db"}.mdl-data-table__select{width:16px}.mdl-data-table__cell--non-numeric.mdl-data-table__cell--non-numeric{text-align:left}.mdl-dialog{border:none;box-shadow:0 9px 46px 8px rgba(0,0,0,.14),0 11px 15px -7px rgba(0,0,0,.12),0 24px 38px 3px rgba(0,0,0,.2);width:280px}.mdl-dialog__title{padding:24px 24px 0;margin:0;font-size:2.5rem}.mdl-dialog__actions{padding:8px 8px 8px 24px;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:row-reverse;-ms-flex-direction:row-reverse;flex-direction:row-reverse;-webkit-flex-wrap:wrap;-ms-flex-wrap:wrap;flex-wrap:wrap}.mdl-dialog__actions>*{margin-right:8px;height:36px}.mdl-dialog__actions>*:first-child{margin-right:0}.mdl-dialog__actions--full-width{padding:0 0 8px}.mdl-dialog__actions--full-width>*{height:48px;-webkit-flex:0 0 100%;-ms-flex:0 0 100%;flex:0 0 100%;padding-right:16px;margin-right:0;text-align:right}.mdl-dialog__content{padding:20px 24px 24px;color:rgba(0,0,0,.54)}.mdl-mega-footer{padding:16px 40px;color:#9e9e9e;background-color:#424242}.mdl-mega-footer--top-section:after,.mdl-mega-footer--middle-section:after,.mdl-mega-footer--bottom-section:after,.mdl-mega-footer__top-section:after,.mdl-mega-footer__middle-section:after,.mdl-mega-footer__bottom-section:after{content:'';display:block;clear:both}.mdl-mega-footer--left-section,.mdl-mega-footer__left-section,.mdl-mega-footer--right-section,.mdl-mega-footer__right-section{margin-bottom:16px}.mdl-mega-footer--right-section a,.mdl-mega-footer__right-section a{display:block;margin-bottom:16px;color:inherit;text-decoration:none}@media screen and (min-width:760px){.mdl-mega-footer--left-section,.mdl-mega-footer__left-section{float:left}.mdl-mega-footer--right-section,.mdl-mega-footer__right-section{float:right}.mdl-mega-footer--right-section a,.mdl-mega-footer__right-section a{display:inline-block;margin-left:16px;line-height:36px;vertical-align:middle}}.mdl-mega-footer--social-btn,.mdl-mega-footer__social-btn{width:36px;height:36px;padding:0;margin:0;background-color:#9e9e9e;border:none}.mdl-mega-footer--drop-down-section,.mdl-mega-footer__drop-down-section{display:block;position:relative}@media screen and (min-width:760px){.mdl-mega-footer--drop-down-section,.mdl-mega-footer__drop-down-section{width:33%}.mdl-mega-footer--drop-down-section:nth-child(1),.mdl-mega-footer--drop-down-section:nth-child(2),.mdl-mega-footer__drop-down-section:nth-child(1),.mdl-mega-footer__drop-down-section:nth-child(2){float:left}.mdl-mega-footer--drop-down-section:nth-child(3),.mdl-mega-footer__drop-down-section:nth-child(3){float:right}.mdl-mega-footer--drop-down-section:nth-child(3):after,.mdl-mega-footer__drop-down-section:nth-child(3):after{clear:right}.mdl-mega-footer--drop-down-section:nth-child(4),.mdl-mega-footer__drop-down-section:nth-child(4){clear:right;float:right}.mdl-mega-footer--middle-section:after,.mdl-mega-footer__middle-section:after{content:'';display:block;clear:both}.mdl-mega-footer--bottom-section,.mdl-mega-footer__bottom-section{padding-top:0}}@media screen and (min-width:1024px){.mdl-mega-footer--drop-down-section,.mdl-mega-footer--drop-down-section:nth-child(3),.mdl-mega-footer--drop-down-section:nth-child(4),.mdl-mega-footer__drop-down-section,.mdl-mega-footer__drop-down-section:nth-child(3),.mdl-mega-footer__drop-down-section:nth-child(4){width:24%;float:left}}.mdl-mega-footer--heading-checkbox,.mdl-mega-footer__heading-checkbox{position:absolute;width:100%;height:55.8px;padding:32px;margin:-16px 0 0;cursor:pointer;z-index:1;opacity:0}.mdl-mega-footer--heading-checkbox+.mdl-mega-footer--heading:after,.mdl-mega-footer--heading-checkbox+.mdl-mega-footer__heading:after,.mdl-mega-footer__heading-checkbox+.mdl-mega-footer--heading:after,.mdl-mega-footer__heading-checkbox+.mdl-mega-footer__heading:after{font-family:'Material Icons';content:'\E5CE'}.mdl-mega-footer--heading-checkbox:checked~.mdl-mega-footer--link-list,.mdl-mega-footer--heading-checkbox:checked~.mdl-mega-footer__link-list,.mdl-mega-footer--heading-checkbox:checked+.mdl-mega-footer--heading+.mdl-mega-footer--link-list,.mdl-mega-footer--heading-checkbox:checked+.mdl-mega-footer__heading+.mdl-mega-footer__link-list,.mdl-mega-footer__heading-checkbox:checked~.mdl-mega-footer--link-list,.mdl-mega-footer__heading-checkbox:checked~.mdl-mega-footer__link-list,.mdl-mega-footer__heading-checkbox:checked+.mdl-mega-footer--heading+.mdl-mega-footer--link-list,.mdl-mega-footer__heading-checkbox:checked+.mdl-mega-footer__heading+.mdl-mega-footer__link-list{display:none}.mdl-mega-footer--heading-checkbox:checked+.mdl-mega-footer--heading:after,.mdl-mega-footer--heading-checkbox:checked+.mdl-mega-footer__heading:after,.mdl-mega-footer__heading-checkbox:checked+.mdl-mega-footer--heading:after,.mdl-mega-footer__heading-checkbox:checked+.mdl-mega-footer__heading:after{font-family:'Material Icons';content:'\E5CF'}.mdl-mega-footer--heading,.mdl-mega-footer__heading{position:relative;width:100%;padding-right:39.8px;margin-bottom:16px;box-sizing:border-box;font-size:14px;line-height:23.8px;font-weight:500;white-space:nowrap;text-overflow:ellipsis;overflow:hidden;color:#e0e0e0}.mdl-mega-footer--heading:after,.mdl-mega-footer__heading:after{content:'';position:absolute;top:0;right:0;display:block;width:23.8px;height:23.8px;background-size:cover}.mdl-mega-footer--link-list,.mdl-mega-footer__link-list{list-style:none;padding:0;margin:0 0 32px}.mdl-mega-footer--link-list:after,.mdl-mega-footer__link-list:after{clear:both;display:block;content:''}.mdl-mega-footer--link-list li,.mdl-mega-footer__link-list li{font-size:14px;font-weight:400;letter-spacing:0;line-height:20px}.mdl-mega-footer--link-list a,.mdl-mega-footer__link-list a{color:inherit;text-decoration:none;white-space:nowrap}@media screen and (min-width:760px){.mdl-mega-footer--heading-checkbox,.mdl-mega-footer__heading-checkbox{display:none}.mdl-mega-footer--heading-checkbox+.mdl-mega-footer--heading:after,.mdl-mega-footer--heading-checkbox+.mdl-mega-footer__heading:after,.mdl-mega-footer__heading-checkbox+.mdl-mega-footer--heading:after,.mdl-mega-footer__heading-checkbox+.mdl-mega-footer__heading:after{content:''}.mdl-mega-footer--heading-checkbox:checked~.mdl-mega-footer--link-list,.mdl-mega-footer--heading-checkbox:checked~.mdl-mega-footer__link-list,.mdl-mega-footer--heading-checkbox:checked+.mdl-mega-footer__heading+.mdl-mega-footer__link-list,.mdl-mega-footer--heading-checkbox:checked+.mdl-mega-footer--heading+.mdl-mega-footer--link-list,.mdl-mega-footer__heading-checkbox:checked~.mdl-mega-footer--link-list,.mdl-mega-footer__heading-checkbox:checked~.mdl-mega-footer__link-list,.mdl-mega-footer__heading-checkbox:checked+.mdl-mega-footer__heading+.mdl-mega-footer__link-list,.mdl-mega-footer__heading-checkbox:checked+.mdl-mega-footer--heading+.mdl-mega-footer--link-list{display:block}.mdl-mega-footer--heading-checkbox:checked+.mdl-mega-footer--heading:after,.mdl-mega-footer--heading-checkbox:checked+.mdl-mega-footer__heading:after,.mdl-mega-footer__heading-checkbox:checked+.mdl-mega-footer--heading:after,.mdl-mega-footer__heading-checkbox:checked+.mdl-mega-footer__heading:after{content:''}}.mdl-mega-footer--bottom-section,.mdl-mega-footer__bottom-section{padding-top:16px;margin-bottom:16px}.mdl-logo{margin-bottom:16px;color:#fff}.mdl-mega-footer--bottom-section .mdl-mega-footer--link-list li,.mdl-mega-footer__bottom-section .mdl-mega-footer__link-list li{float:left;margin-bottom:0;margin-right:16px}@media screen and (min-width:760px){.mdl-logo{float:left;margin-bottom:0;margin-right:16px}}.mdl-mini-footer{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-flow:row wrap;-ms-flex-flow:row wrap;flex-flow:row wrap;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;padding:32px 16px;color:#9e9e9e;background-color:#424242}.mdl-mini-footer:after{content:'';display:block}.mdl-mini-footer .mdl-logo{line-height:36px}.mdl-mini-footer--link-list,.mdl-mini-footer__link-list{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-flow:row nowrap;-ms-flex-flow:row nowrap;flex-flow:row nowrap;list-style:none;margin:0;padding:0}.mdl-mini-footer--link-list li,.mdl-mini-footer__link-list li{margin-bottom:0;margin-right:16px}@media screen and (min-width:760px){.mdl-mini-footer--link-list li,.mdl-mini-footer__link-list li{line-height:36px}}.mdl-mini-footer--link-list a,.mdl-mini-footer__link-list a{color:inherit;text-decoration:none;white-space:nowrap}.mdl-mini-footer--left-section,.mdl-mini-footer__left-section{display:inline-block;-webkit-order:0;-ms-flex-order:0;order:0}.mdl-mini-footer--right-section,.mdl-mini-footer__right-section{display:inline-block;-webkit-order:1;-ms-flex-order:1;order:1}.mdl-mini-footer--social-btn,.mdl-mini-footer__social-btn{width:36px;height:36px;padding:0;margin:0;background-color:#9e9e9e;border:none}.mdl-icon-toggle{position:relative;z-index:1;vertical-align:middle;display:inline-block;height:32px;margin:0;padding:0}.mdl-icon-toggle__input{line-height:32px}.mdl-icon-toggle.is-upgraded .mdl-icon-toggle__input{position:absolute;width:0;height:0;margin:0;padding:0;opacity:0;-ms-appearance:none;-moz-appearance:none;-webkit-appearance:none;appearance:none;border:none}.mdl-icon-toggle__label{display:inline-block;position:relative;cursor:pointer;height:32px;width:32px;min-width:32px;color:#616161;border-radius:50%;padding:0;margin-left:0;margin-right:0;text-align:center;background-color:transparent;will-change:background-color;transition:background-color .2s cubic-bezier(.4,0,.2,1),color .2s cubic-bezier(.4,0,.2,1)}.mdl-icon-toggle__label.material-icons{line-height:32px;font-size:24px}.mdl-icon-toggle.is-checked .mdl-icon-toggle__label{color:#3f51b5}.mdl-icon-toggle.is-disabled .mdl-icon-toggle__label{color:rgba(0,0,0,.26);cursor:auto;transition:none}.mdl-icon-toggle.is-focused .mdl-icon-toggle__label{background-color:rgba(0,0,0,.12)}.mdl-icon-toggle.is-focused.is-checked .mdl-icon-toggle__label{background-color:rgba(63,81,181,.26)}.mdl-icon-toggle__ripple-container{position:absolute;z-index:2;top:-2px;left:-2px;box-sizing:border-box;width:36px;height:36px;border-radius:50%;cursor:pointer;overflow:hidden;-webkit-mask-image:-webkit-radial-gradient(circle,#fff,#000)}.mdl-icon-toggle__ripple-container .mdl-ripple{background:#616161}.mdl-icon-toggle.is-disabled .mdl-icon-toggle__ripple-container{cursor:auto}.mdl-icon-toggle.is-disabled .mdl-icon-toggle__ripple-container .mdl-ripple{background:0 0}.mdl-list{display:block;padding:8px 0;list-style:none}.mdl-list__item{font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:16px;font-weight:400;letter-spacing:.04em;line-height:1;min-height:48px;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;padding:16px;cursor:default;color:rgba(0,0,0,.87);overflow:hidden}.mdl-list__item,.mdl-list__item .mdl-list__item-primary-content{box-sizing:border-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.mdl-list__item .mdl-list__item-primary-content{-webkit-order:0;-ms-flex-order:0;order:0;-webkit-flex-grow:2;-ms-flex-positive:2;flex-grow:2;text-decoration:none}.mdl-list__item .mdl-list__item-primary-content .mdl-list__item-icon{margin-right:32px}.mdl-list__item .mdl-list__item-primary-content .mdl-list__item-avatar{margin-right:16px}.mdl-list__item .mdl-list__item-secondary-content{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-flow:column;-ms-flex-flow:column;flex-flow:column;-webkit-align-items:flex-end;-ms-flex-align:end;align-items:flex-end;margin-left:16px}.mdl-list__item .mdl-list__item-secondary-content .mdl-list__item-secondary-action label{display:inline}.mdl-list__item .mdl-list__item-secondary-content .mdl-list__item-secondary-info{font-size:12px;font-weight:400;line-height:1;letter-spacing:0;color:rgba(0,0,0,.54)}.mdl-list__item .mdl-list__item-secondary-content .mdl-list__item-sub-header{padding:0 0 0 16px}.mdl-list__item-icon,.mdl-list__item-icon.material-icons{height:24px;width:24px;font-size:24px;box-sizing:border-box;color:#757575}.mdl-list__item-avatar,.mdl-list__item-avatar.material-icons{height:40px;width:40px;box-sizing:border-box;border-radius:50%;background-color:#757575;font-size:40px;color:#fff}.mdl-list__item--two-line{height:72px}.mdl-list__item--two-line .mdl-list__item-primary-content{height:36px;line-height:20px;display:block}.mdl-list__item--two-line .mdl-list__item-primary-content .mdl-list__item-avatar{float:left}.mdl-list__item--two-line .mdl-list__item-primary-content .mdl-list__item-icon{float:left;margin-top:6px}.mdl-list__item--two-line .mdl-list__item-primary-content .mdl-list__item-secondary-content{height:36px}.mdl-list__item--two-line .mdl-list__item-primary-content .mdl-list__item-sub-title{font-size:14px;font-weight:400;letter-spacing:0;line-height:18px;color:rgba(0,0,0,.54);display:block;padding:0}.mdl-list__item--three-line{height:88px}.mdl-list__item--three-line .mdl-list__item-primary-content{height:52px;line-height:20px;display:block}.mdl-list__item--three-line .mdl-list__item-primary-content .mdl-list__item-avatar,.mdl-list__item--three-line .mdl-list__item-primary-content .mdl-list__item-icon{float:left}.mdl-list__item--three-line .mdl-list__item-secondary-content{height:52px}.mdl-list__item--three-line .mdl-list__item-text-body{font-size:14px;font-weight:400;letter-spacing:0;line-height:18px;height:52px;color:rgba(0,0,0,.54);display:block;padding:0}.mdl-menu__container{display:block;margin:0;padding:0;border:none;position:absolute;overflow:visible;height:0;width:0;visibility:hidden;z-index:-1}.mdl-menu__container.is-visible,.mdl-menu__container.is-animating{z-index:999;visibility:visible}.mdl-menu__outline{display:block;background:#fff;margin:0;padding:0;border:none;border-radius:2px;position:absolute;top:0;left:0;overflow:hidden;opacity:0;-webkit-transform:scale(0);transform:scale(0);-webkit-transform-origin:0 0;transform-origin:0 0;box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12);will-change:transform;transition:transform .3s cubic-bezier(.4,0,.2,1),opacity .2s cubic-bezier(.4,0,.2,1);transition:transform .3s cubic-bezier(.4,0,.2,1),opacity .2s cubic-bezier(.4,0,.2,1),-webkit-transform .3s cubic-bezier(.4,0,.2,1);z-index:-1}.mdl-menu__container.is-visible .mdl-menu__outline{opacity:1;-webkit-transform:scale(1);transform:scale(1);z-index:999}.mdl-menu__outline.mdl-menu--bottom-right{-webkit-transform-origin:100% 0;transform-origin:100% 0}.mdl-menu__outline.mdl-menu--top-left{-webkit-transform-origin:0 100%;transform-origin:0 100%}.mdl-menu__outline.mdl-menu--top-right{-webkit-transform-origin:100% 100%;transform-origin:100% 100%}.mdl-menu{position:absolute;list-style:none;top:0;left:0;height:auto;width:auto;min-width:124px;padding:8px 0;margin:0;opacity:0;clip:rect(0 0 0 0);z-index:-1}.mdl-menu__container.is-visible .mdl-menu{opacity:1;z-index:999}.mdl-menu.is-animating{transition:opacity .2s cubic-bezier(.4,0,.2,1),clip .3s cubic-bezier(.4,0,.2,1)}.mdl-menu.mdl-menu--bottom-right{left:auto;right:0}.mdl-menu.mdl-menu--top-left{top:auto;bottom:0}.mdl-menu.mdl-menu--top-right{top:auto;left:auto;bottom:0;right:0}.mdl-menu.mdl-menu--unaligned{top:auto;left:auto}.mdl-menu__item{display:block;border:none;color:rgba(0,0,0,.87);background-color:transparent;text-align:left;margin:0;padding:0 16px;outline-color:#bdbdbd;position:relative;overflow:hidden;font-size:14px;font-weight:400;letter-spacing:0;text-decoration:none;cursor:pointer;height:48px;line-height:48px;white-space:nowrap;opacity:0;transition:opacity .2s cubic-bezier(.4,0,.2,1);-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.mdl-menu__container.is-visible .mdl-menu__item{opacity:1}.mdl-menu__item::-moz-focus-inner{border:0}.mdl-menu__item--full-bleed-divider{border-bottom:1px solid rgba(0,0,0,.12)}.mdl-menu__item[disabled],.mdl-menu__item[data-mdl-disabled]{color:#bdbdbd;background-color:transparent;cursor:auto}.mdl-menu__item[disabled]:hover,.mdl-menu__item[data-mdl-disabled]:hover{background-color:transparent}.mdl-menu__item[disabled]:focus,.mdl-menu__item[data-mdl-disabled]:focus{background-color:transparent}.mdl-menu__item[disabled] .mdl-ripple,.mdl-menu__item[data-mdl-disabled] .mdl-ripple{background:0 0}.mdl-menu__item:hover{background-color:#eee}.mdl-menu__item:focus{outline:none;background-color:#eee}.mdl-menu__item:active{background-color:#e0e0e0}.mdl-menu__item--ripple-container{display:block;height:100%;left:0;position:absolute;top:0;width:100%;z-index:0;overflow:hidden}.mdl-progress{display:block;position:relative;height:4px;width:500px;max-width:100%}.mdl-progress>.bar{display:block;position:absolute;top:0;bottom:0;width:0%;transition:width .2s cubic-bezier(.4,0,.2,1)}.mdl-progress>.progressbar{background-color:#3f51b5;z-index:1;left:0}.mdl-progress>.bufferbar{background-image:linear-gradient(to right,rgba(255,255,255,.7),rgba(255,255,255,.7)),linear-gradient(to right,#3f51b5 ,#3f51b5);z-index:0;left:0}.mdl-progress>.auxbar{right:0}@supports (-webkit-appearance:none){.mdl-progress:not(.mdl-progress--indeterminate):not(.mdl-progress--indeterminate)>.auxbar,.mdl-progress:not(.mdl-progress__indeterminate):not(.mdl-progress__indeterminate)>.auxbar{background-image:linear-gradient(to right,rgba(255,255,255,.7),rgba(255,255,255,.7)),linear-gradient(to right,#3f51b5 ,#3f51b5);-webkit-mask:url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIj8+Cjxzdmcgd2lkdGg9IjEyIiBoZWlnaHQ9IjQiIHZpZXdQb3J0PSIwIDAgMTIgNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogIDxlbGxpcHNlIGN4PSIyIiBjeT0iMiIgcng9IjIiIHJ5PSIyIj4KICAgIDxhbmltYXRlIGF0dHJpYnV0ZU5hbWU9ImN4IiBmcm9tPSIyIiB0bz0iLTEwIiBkdXI9IjAuNnMiIHJlcGVhdENvdW50PSJpbmRlZmluaXRlIiAvPgogIDwvZWxsaXBzZT4KICA8ZWxsaXBzZSBjeD0iMTQiIGN5PSIyIiByeD0iMiIgcnk9IjIiIGNsYXNzPSJsb2FkZXIiPgogICAgPGFuaW1hdGUgYXR0cmlidXRlTmFtZT0iY3giIGZyb209IjE0IiB0bz0iMiIgZHVyPSIwLjZzIiByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSIgLz4KICA8L2VsbGlwc2U+Cjwvc3ZnPgo=");mask:url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIj8+Cjxzdmcgd2lkdGg9IjEyIiBoZWlnaHQ9IjQiIHZpZXdQb3J0PSIwIDAgMTIgNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogIDxlbGxpcHNlIGN4PSIyIiBjeT0iMiIgcng9IjIiIHJ5PSIyIj4KICAgIDxhbmltYXRlIGF0dHJpYnV0ZU5hbWU9ImN4IiBmcm9tPSIyIiB0bz0iLTEwIiBkdXI9IjAuNnMiIHJlcGVhdENvdW50PSJpbmRlZmluaXRlIiAvPgogIDwvZWxsaXBzZT4KICA8ZWxsaXBzZSBjeD0iMTQiIGN5PSIyIiByeD0iMiIgcnk9IjIiIGNsYXNzPSJsb2FkZXIiPgogICAgPGFuaW1hdGUgYXR0cmlidXRlTmFtZT0iY3giIGZyb209IjE0IiB0bz0iMiIgZHVyPSIwLjZzIiByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSIgLz4KICA8L2VsbGlwc2U+Cjwvc3ZnPgo=")}}.mdl-progress:not(.mdl-progress--indeterminate)>.auxbar,.mdl-progress:not(.mdl-progress__indeterminate)>.auxbar{background-image:linear-gradient(to right,rgba(255,255,255,.9),rgba(255,255,255,.9)),linear-gradient(to right,#3f51b5 ,#3f51b5)}.mdl-progress.mdl-progress--indeterminate>.bar1,.mdl-progress.mdl-progress__indeterminate>.bar1{-webkit-animation-name:indeterminate1;animation-name:indeterminate1}.mdl-progress.mdl-progress--indeterminate>.bar1,.mdl-progress.mdl-progress__indeterminate>.bar1,.mdl-progress.mdl-progress--indeterminate>.bar3,.mdl-progress.mdl-progress__indeterminate>.bar3{background-color:#3f51b5;-webkit-animation-duration:2s;animation-duration:2s;-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite;-webkit-animation-timing-function:linear;animation-timing-function:linear}.mdl-progress.mdl-progress--indeterminate>.bar3,.mdl-progress.mdl-progress__indeterminate>.bar3{background-image:none;-webkit-animation-name:indeterminate2;animation-name:indeterminate2}@-webkit-keyframes indeterminate1{0%{left:0%;width:0%}50%{left:25%;width:75%}75%{left:100%;width:0%}}@keyframes indeterminate1{0%{left:0%;width:0%}50%{left:25%;width:75%}75%{left:100%;width:0%}}@-webkit-keyframes indeterminate2{0%,50%{left:0%;width:0%}75%{left:0%;width:25%}100%{left:100%;width:0%}}@keyframes indeterminate2{0%,50%{left:0%;width:0%}75%{left:0%;width:25%}100%{left:100%;width:0%}}.mdl-navigation{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;box-sizing:border-box}.mdl-navigation__link{color:#424242;text-decoration:none;margin:0;font-size:14px;font-weight:400;line-height:24px;letter-spacing:0;opacity:.87}.mdl-navigation__link .material-icons{vertical-align:middle}.mdl-layout{width:100%;height:100%;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;overflow-y:auto;overflow-x:hidden;position:relative;-webkit-overflow-scrolling:touch}.mdl-layout.is-small-screen .mdl-layout--large-screen-only{display:none}.mdl-layout:not(.is-small-screen) .mdl-layout--small-screen-only{display:none}.mdl-layout__container{position:absolute;width:100%;height:100%}.mdl-layout__title,.mdl-layout-title{display:block;position:relative;font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:20px;line-height:1;letter-spacing:.02em;font-weight:400;box-sizing:border-box}.mdl-layout-spacer{-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1}.mdl-layout__drawer{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;width:240px;height:100%;max-height:100%;position:absolute;top:0;left:0;box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12);box-sizing:border-box;border-right:1px solid #e0e0e0;background:#fafafa;-webkit-transform:translateX(-250px);transform:translateX(-250px);-webkit-transform-style:preserve-3d;transform-style:preserve-3d;will-change:transform;transition-duration:.2s;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-property:transform;transition-property:transform,-webkit-transform;color:#424242;overflow:visible;overflow-y:auto;z-index:5}.mdl-layout__drawer.is-visible{-webkit-transform:translateX(0);transform:translateX(0)}.mdl-layout__drawer.is-visible~.mdl-layout__content.mdl-layout__content{overflow:hidden}.mdl-layout__drawer>*{-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0}.mdl-layout__drawer>.mdl-layout__title,.mdl-layout__drawer>.mdl-layout-title{line-height:64px;padding-left:40px}@media screen and (max-width:1024px){.mdl-layout__drawer>.mdl-layout__title,.mdl-layout__drawer>.mdl-layout-title{line-height:56px;padding-left:16px}}.mdl-layout__drawer .mdl-navigation{-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;-webkit-align-items:stretch;-ms-flex-align:stretch;-ms-grid-row-align:stretch;align-items:stretch;padding-top:16px}.mdl-layout__drawer .mdl-navigation .mdl-navigation__link{display:block;-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0;padding:16px 40px;margin:0;color:#757575}@media screen and (max-width:1024px){.mdl-layout__drawer .mdl-navigation .mdl-navigation__link{padding:16px}}.mdl-layout__drawer .mdl-navigation .mdl-navigation__link:hover{background-color:#e0e0e0}.mdl-layout__drawer .mdl-navigation .mdl-navigation__link--current{background-color:#000;color:#e0e0e0}@media screen and (min-width:1025px){.mdl-layout--fixed-drawer>.mdl-layout__drawer{-webkit-transform:translateX(0);transform:translateX(0)}}.mdl-layout__drawer-button{display:block;position:absolute;height:48px;width:48px;border:0;-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0;overflow:hidden;text-align:center;cursor:pointer;font-size:26px;line-height:50px;font-family:Helvetica,Arial,sans-serif;margin:10px 12px;top:0;left:0;color:#fff;z-index:4}.mdl-layout__header .mdl-layout__drawer-button{position:absolute;color:#fff;background-color:inherit}@media screen and (max-width:1024px){.mdl-layout__header .mdl-layout__drawer-button{margin:4px}}@media screen and (max-width:1024px){.mdl-layout__drawer-button{margin:4px;color:rgba(0,0,0,.5)}}@media screen and (min-width:1025px){.mdl-layout--fixed-drawer>.mdl-layout__drawer-button,.mdl-layout--no-desktop-drawer-button .mdl-layout__drawer-button{display:none}}.mdl-layout--no-drawer-button .mdl-layout__drawer-button{display:none}.mdl-layout__header{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-justify-content:flex-start;-ms-flex-pack:start;justify-content:flex-start;box-sizing:border-box;-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0;width:100%;margin:0;padding:0;border:none;min-height:64px;max-height:1000px;z-index:3;background-color:#3f51b5;color:#fff;box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12);transition-duration:.2s;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-property:max-height,box-shadow}@media screen and (max-width:1024px){.mdl-layout__header{min-height:56px}}.mdl-layout--fixed-drawer.is-upgraded:not(.is-small-screen)>.mdl-layout__header{margin-left:240px;width:calc(100% - 240px)}@media screen and (min-width:1025px){.mdl-layout--fixed-drawer>.mdl-layout__header .mdl-layout__header-row{padding-left:40px}}.mdl-layout__header>.mdl-layout-icon{position:absolute;left:40px;top:16px;height:32px;width:32px;overflow:hidden;z-index:3;display:block}@media screen and (max-width:1024px){.mdl-layout__header>.mdl-layout-icon{left:16px;top:12px}}.mdl-layout.has-drawer .mdl-layout__header>.mdl-layout-icon{display:none}.mdl-layout__header.is-compact{max-height:64px}@media screen and (max-width:1024px){.mdl-layout__header.is-compact{max-height:56px}}.mdl-layout__header.is-compact.has-tabs{height:112px}@media screen and (max-width:1024px){.mdl-layout__header.is-compact.has-tabs{min-height:104px}}@media screen and (max-width:1024px){.mdl-layout__header{display:none}.mdl-layout--fixed-header>.mdl-layout__header{display:-webkit-flex;display:-ms-flexbox;display:flex}}.mdl-layout__header--transparent.mdl-layout__header--transparent{background-color:transparent;box-shadow:none}.mdl-layout__header--seamed,.mdl-layout__header--scroll{box-shadow:none}.mdl-layout__header--waterfall{box-shadow:none;overflow:hidden}.mdl-layout__header--waterfall.is-casting-shadow{box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12)}.mdl-layout__header--waterfall.mdl-layout__header--waterfall-hide-top{-webkit-justify-content:flex-end;-ms-flex-pack:end;justify-content:flex-end}.mdl-layout__header-row{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-flex-wrap:nowrap;-ms-flex-wrap:nowrap;flex-wrap:nowrap;-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0;box-sizing:border-box;-webkit-align-self:stretch;-ms-flex-item-align:stretch;align-self:stretch;-webkit-align-items:center;-ms-flex-align:center;align-items:center;height:64px;margin:0;padding:0 40px 0 80px}.mdl-layout--no-drawer-button .mdl-layout__header-row{padding-left:40px}@media screen and (min-width:1025px){.mdl-layout--no-desktop-drawer-button .mdl-layout__header-row{padding-left:40px}}@media screen and (max-width:1024px){.mdl-layout__header-row{height:56px;padding:0 16px 0 72px}.mdl-layout--no-drawer-button .mdl-layout__header-row{padding-left:16px}}.mdl-layout__header-row>*{-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0}.mdl-layout__header--scroll .mdl-layout__header-row{width:100%}.mdl-layout__header-row .mdl-navigation{margin:0;padding:0;height:64px;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-align-items:center;-ms-flex-align:center;-ms-grid-row-align:center;align-items:center}@media screen and (max-width:1024px){.mdl-layout__header-row .mdl-navigation{height:56px}}.mdl-layout__header-row .mdl-navigation__link{display:block;color:#fff;line-height:64px;padding:0 24px}@media screen and (max-width:1024px){.mdl-layout__header-row .mdl-navigation__link{line-height:56px;padding:0 16px}}.mdl-layout__obfuscator{background-color:transparent;position:absolute;top:0;left:0;height:100%;width:100%;z-index:4;visibility:hidden;transition-property:background-color;transition-duration:.2s;transition-timing-function:cubic-bezier(.4,0,.2,1)}.mdl-layout__obfuscator.is-visible{background-color:rgba(0,0,0,.5);visibility:visible}@supports (pointer-events:auto){.mdl-layout__obfuscator{background-color:rgba(0,0,0,.5);opacity:0;transition-property:opacity;visibility:visible;pointer-events:none}.mdl-layout__obfuscator.is-visible{pointer-events:auto;opacity:1}}.mdl-layout__content{-ms-flex:0 1 auto;position:relative;display:inline-block;overflow-y:auto;overflow-x:hidden;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;z-index:1;-webkit-overflow-scrolling:touch}.mdl-layout--fixed-drawer>.mdl-layout__content{margin-left:240px}.mdl-layout__container.has-scrolling-header .mdl-layout__content{overflow:visible}@media screen and (max-width:1024px){.mdl-layout--fixed-drawer>.mdl-layout__content{margin-left:0}.mdl-layout__container.has-scrolling-header .mdl-layout__content{overflow-y:auto;overflow-x:hidden}}.mdl-layout__tab-bar{height:96px;margin:0;width:calc(100% - 112px);padding:0 0 0 56px;display:-webkit-flex;display:-ms-flexbox;display:flex;background-color:#3f51b5;overflow-y:hidden;overflow-x:scroll}.mdl-layout__tab-bar::-webkit-scrollbar{display:none}.mdl-layout--no-drawer-button .mdl-layout__tab-bar{padding-left:16px;width:calc(100% - 32px)}@media screen and (min-width:1025px){.mdl-layout--no-desktop-drawer-button .mdl-layout__tab-bar{padding-left:16px;width:calc(100% - 32px)}}@media screen and (max-width:1024px){.mdl-layout__tab-bar{width:calc(100% - 60px);padding:0 0 0 60px}.mdl-layout--no-drawer-button .mdl-layout__tab-bar{width:calc(100% - 8px);padding-left:4px}}.mdl-layout--fixed-tabs .mdl-layout__tab-bar{padding:0;overflow:hidden;width:100%}.mdl-layout__tab-bar-container{position:relative;height:48px;width:100%;border:none;margin:0;z-index:2;-webkit-flex-grow:0;-ms-flex-positive:0;flex-grow:0;-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0;overflow:hidden}.mdl-layout__container>.mdl-layout__tab-bar-container{position:absolute;top:0;left:0}.mdl-layout__tab-bar-button{display:inline-block;position:absolute;top:0;height:48px;width:56px;z-index:4;text-align:center;background-color:#3f51b5;color:transparent;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.mdl-layout--no-desktop-drawer-button .mdl-layout__tab-bar-button,.mdl-layout--no-drawer-button .mdl-layout__tab-bar-button{width:16px}.mdl-layout--no-desktop-drawer-button .mdl-layout__tab-bar-button .material-icons,.mdl-layout--no-drawer-button .mdl-layout__tab-bar-button .material-icons{position:relative;left:-4px}@media screen and (max-width:1024px){.mdl-layout__tab-bar-button{display:none;width:60px}}.mdl-layout--fixed-tabs .mdl-layout__tab-bar-button{display:none}.mdl-layout__tab-bar-button .material-icons{line-height:48px}.mdl-layout__tab-bar-button.is-active{color:#fff}.mdl-layout__tab-bar-left-button{left:0}.mdl-layout__tab-bar-right-button{right:0}.mdl-layout__tab{margin:0;border:none;padding:0 24px;float:left;position:relative;display:block;-webkit-flex-grow:0;-ms-flex-positive:0;flex-grow:0;-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0;text-decoration:none;height:48px;line-height:48px;text-align:center;font-weight:500;font-size:14px;text-transform:uppercase;color:rgba(255,255,255,.6);overflow:hidden}@media screen and (max-width:1024px){.mdl-layout__tab{padding:0 12px}}.mdl-layout--fixed-tabs .mdl-layout__tab{float:none;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;padding:0}.mdl-layout.is-upgraded .mdl-layout__tab.is-active{color:#fff}.mdl-layout.is-upgraded .mdl-layout__tab.is-active::after{height:2px;width:100%;display:block;content:" ";bottom:0;left:0;position:absolute;background:#ff4081;-webkit-animation:border-expand .2s cubic-bezier(.4,0,.4,1).01s alternate forwards;animation:border-expand .2s cubic-bezier(.4,0,.4,1).01s alternate forwards;transition:all 1s cubic-bezier(.4,0,1,1)}.mdl-layout__tab .mdl-layout__tab-ripple-container{display:block;position:absolute;height:100%;width:100%;left:0;top:0;z-index:1;overflow:hidden}.mdl-layout__tab .mdl-layout__tab-ripple-container .mdl-ripple{background-color:#fff}.mdl-layout__tab-panel{display:block}.mdl-layout.is-upgraded .mdl-layout__tab-panel{display:none}.mdl-layout.is-upgraded .mdl-layout__tab-panel.is-active{display:block}.mdl-radio{position:relative;font-size:16px;line-height:24px;display:inline-block;box-sizing:border-box;margin:0;padding-left:0}.mdl-radio.is-upgraded{padding-left:24px}.mdl-radio__button{line-height:24px}.mdl-radio.is-upgraded .mdl-radio__button{position:absolute;width:0;height:0;margin:0;padding:0;opacity:0;-ms-appearance:none;-moz-appearance:none;-webkit-appearance:none;appearance:none;border:none}.mdl-radio__outer-circle{position:absolute;top:4px;left:0;display:inline-block;box-sizing:border-box;width:16px;height:16px;margin:0;cursor:pointer;border:2px solid rgba(0,0,0,.54);border-radius:50%;z-index:2}.mdl-radio.is-checked .mdl-radio__outer-circle{border:2px solid #3f51b5}.mdl-radio__outer-circle fieldset[disabled] .mdl-radio,.mdl-radio.is-disabled .mdl-radio__outer-circle{border:2px solid rgba(0,0,0,.26);cursor:auto}.mdl-radio__inner-circle{position:absolute;z-index:1;margin:0;top:8px;left:4px;box-sizing:border-box;width:8px;height:8px;cursor:pointer;transition-duration:.28s;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-property:transform;transition-property:transform,-webkit-transform;-webkit-transform:scale3d(0,0,0);transform:scale3d(0,0,0);border-radius:50%;background:#3f51b5}.mdl-radio.is-checked .mdl-radio__inner-circle{-webkit-transform:scale3d(1,1,1);transform:scale3d(1,1,1)}fieldset[disabled] .mdl-radio .mdl-radio__inner-circle,.mdl-radio.is-disabled .mdl-radio__inner-circle{background:rgba(0,0,0,.26);cursor:auto}.mdl-radio.is-focused .mdl-radio__inner-circle{box-shadow:0 0 0 10px rgba(0,0,0,.1)}.mdl-radio__label{cursor:pointer}fieldset[disabled] .mdl-radio .mdl-radio__label,.mdl-radio.is-disabled .mdl-radio__label{color:rgba(0,0,0,.26);cursor:auto}.mdl-radio__ripple-container{position:absolute;z-index:2;top:-9px;left:-13px;box-sizing:border-box;width:42px;height:42px;border-radius:50%;cursor:pointer;overflow:hidden;-webkit-mask-image:-webkit-radial-gradient(circle,#fff,#000)}.mdl-radio__ripple-container .mdl-ripple{background:#3f51b5}fieldset[disabled] .mdl-radio .mdl-radio__ripple-container,.mdl-radio.is-disabled .mdl-radio__ripple-container{cursor:auto}fieldset[disabled] .mdl-radio .mdl-radio__ripple-container .mdl-ripple,.mdl-radio.is-disabled .mdl-radio__ripple-container .mdl-ripple{background:0 0}_:-ms-input-placeholder,:root .mdl-slider.mdl-slider.is-upgraded{-ms-appearance:none;height:32px;margin:0}.mdl-slider{width:calc(100% - 40px);margin:0 20px}.mdl-slider.is-upgraded{-webkit-appearance:none;-moz-appearance:none;appearance:none;height:2px;background:0 0;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;outline:0;padding:0;color:#3f51b5;-webkit-align-self:center;-ms-flex-item-align:center;align-self:center;z-index:1;cursor:pointer}.mdl-slider.is-upgraded::-moz-focus-outer{border:0}.mdl-slider.is-upgraded::-ms-tooltip{display:none}.mdl-slider.is-upgraded::-webkit-slider-runnable-track{background:0 0}.mdl-slider.is-upgraded::-moz-range-track{background:0 0;border:none}.mdl-slider.is-upgraded::-ms-track{background:0 0;color:transparent;height:2px;width:100%;border:none}.mdl-slider.is-upgraded::-ms-fill-lower{padding:0;background:linear-gradient(to right,transparent,transparent 16px,#3f51b5 16px,#3f51b5 0)}.mdl-slider.is-upgraded::-ms-fill-upper{padding:0;background:linear-gradient(to left,transparent,transparent 16px,rgba(0,0,0,.26)16px,rgba(0,0,0,.26)0)}.mdl-slider.is-upgraded::-webkit-slider-thumb{-webkit-appearance:none;width:12px;height:12px;box-sizing:border-box;border-radius:50%;background:#3f51b5;border:none;transition:transform .18s cubic-bezier(.4,0,.2,1),border .18s cubic-bezier(.4,0,.2,1),box-shadow .18s cubic-bezier(.4,0,.2,1),background .28s cubic-bezier(.4,0,.2,1);transition:transform .18s cubic-bezier(.4,0,.2,1),border .18s cubic-bezier(.4,0,.2,1),box-shadow .18s cubic-bezier(.4,0,.2,1),background .28s cubic-bezier(.4,0,.2,1),-webkit-transform .18s cubic-bezier(.4,0,.2,1)}.mdl-slider.is-upgraded::-moz-range-thumb{-moz-appearance:none;width:12px;height:12px;box-sizing:border-box;border-radius:50%;background-image:none;background:#3f51b5;border:none}.mdl-slider.is-upgraded:focus:not(:active)::-webkit-slider-thumb{box-shadow:0 0 0 10px rgba(63,81,181,.26)}.mdl-slider.is-upgraded:focus:not(:active)::-moz-range-thumb{box-shadow:0 0 0 10px rgba(63,81,181,.26)}.mdl-slider.is-upgraded:active::-webkit-slider-thumb{background-image:none;background:#3f51b5;-webkit-transform:scale(1.5);transform:scale(1.5)}.mdl-slider.is-upgraded:active::-moz-range-thumb{background-image:none;background:#3f51b5;transform:scale(1.5)}.mdl-slider.is-upgraded::-ms-thumb{width:32px;height:32px;border:none;border-radius:50%;background:#3f51b5;transform:scale(.375);transition:transform .18s cubic-bezier(.4,0,.2,1),background .28s cubic-bezier(.4,0,.2,1);transition:transform .18s cubic-bezier(.4,0,.2,1),background .28s cubic-bezier(.4,0,.2,1),-webkit-transform .18s cubic-bezier(.4,0,.2,1)}.mdl-slider.is-upgraded:focus:not(:active)::-ms-thumb{background:radial-gradient(circle closest-side,#3f51b5 0%,#3f51b5 37.5%,rgba(63,81,181,.26)37.5%,rgba(63,81,181,.26)100%);transform:scale(1)}.mdl-slider.is-upgraded:active::-ms-thumb{background:#3f51b5;transform:scale(.5625)}.mdl-slider.is-upgraded.is-lowest-value::-webkit-slider-thumb{border:2px solid rgba(0,0,0,.26);background:0 0}.mdl-slider.is-upgraded.is-lowest-value::-moz-range-thumb{border:2px solid rgba(0,0,0,.26);background:0 0}.mdl-slider.is-upgraded.is-lowest-value+.mdl-slider__background-flex>.mdl-slider__background-upper{left:6px}.mdl-slider.is-upgraded.is-lowest-value:focus:not(:active)::-webkit-slider-thumb{box-shadow:0 0 0 10px rgba(0,0,0,.12);background:rgba(0,0,0,.12)}.mdl-slider.is-upgraded.is-lowest-value:focus:not(:active)::-moz-range-thumb{box-shadow:0 0 0 10px rgba(0,0,0,.12);background:rgba(0,0,0,.12)}.mdl-slider.is-upgraded.is-lowest-value:active::-webkit-slider-thumb{border:1.6px solid rgba(0,0,0,.26);-webkit-transform:scale(1.5);transform:scale(1.5)}.mdl-slider.is-upgraded.is-lowest-value:active+.mdl-slider__background-flex>.mdl-slider__background-upper{left:9px}.mdl-slider.is-upgraded.is-lowest-value:active::-moz-range-thumb{border:1.5px solid rgba(0,0,0,.26);transform:scale(1.5)}.mdl-slider.is-upgraded.is-lowest-value::-ms-thumb{background:radial-gradient(circle closest-side,transparent 0%,transparent 66.67%,rgba(0,0,0,.26)66.67%,rgba(0,0,0,.26)100%)}.mdl-slider.is-upgraded.is-lowest-value:focus:not(:active)::-ms-thumb{background:radial-gradient(circle closest-side,rgba(0,0,0,.12)0%,rgba(0,0,0,.12)25%,rgba(0,0,0,.26)25%,rgba(0,0,0,.26)37.5%,rgba(0,0,0,.12)37.5%,rgba(0,0,0,.12)100%);transform:scale(1)}.mdl-slider.is-upgraded.is-lowest-value:active::-ms-thumb{transform:scale(.5625);background:radial-gradient(circle closest-side,transparent 0%,transparent 77.78%,rgba(0,0,0,.26)77.78%,rgba(0,0,0,.26)100%)}.mdl-slider.is-upgraded.is-lowest-value::-ms-fill-lower{background:0 0}.mdl-slider.is-upgraded.is-lowest-value::-ms-fill-upper{margin-left:6px}.mdl-slider.is-upgraded.is-lowest-value:active::-ms-fill-upper{margin-left:9px}.mdl-slider.is-upgraded:disabled:focus::-webkit-slider-thumb,.mdl-slider.is-upgraded:disabled:active::-webkit-slider-thumb,.mdl-slider.is-upgraded:disabled::-webkit-slider-thumb{-webkit-transform:scale(.667);transform:scale(.667);background:rgba(0,0,0,.26)}.mdl-slider.is-upgraded:disabled:focus::-moz-range-thumb,.mdl-slider.is-upgraded:disabled:active::-moz-range-thumb,.mdl-slider.is-upgraded:disabled::-moz-range-thumb{transform:scale(.667);background:rgba(0,0,0,.26)}.mdl-slider.is-upgraded:disabled+.mdl-slider__background-flex>.mdl-slider__background-lower{background-color:rgba(0,0,0,.26);left:-6px}.mdl-slider.is-upgraded:disabled+.mdl-slider__background-flex>.mdl-slider__background-upper{left:6px}.mdl-slider.is-upgraded.is-lowest-value:disabled:focus::-webkit-slider-thumb,.mdl-slider.is-upgraded.is-lowest-value:disabled:active::-webkit-slider-thumb,.mdl-slider.is-upgraded.is-lowest-value:disabled::-webkit-slider-thumb{border:3px solid rgba(0,0,0,.26);background:0 0;-webkit-transform:scale(.667);transform:scale(.667)}.mdl-slider.is-upgraded.is-lowest-value:disabled:focus::-moz-range-thumb,.mdl-slider.is-upgraded.is-lowest-value:disabled:active::-moz-range-thumb,.mdl-slider.is-upgraded.is-lowest-value:disabled::-moz-range-thumb{border:3px solid rgba(0,0,0,.26);background:0 0;transform:scale(.667)}.mdl-slider.is-upgraded.is-lowest-value:disabled:active+.mdl-slider__background-flex>.mdl-slider__background-upper{left:6px}.mdl-slider.is-upgraded:disabled:focus::-ms-thumb,.mdl-slider.is-upgraded:disabled:active::-ms-thumb,.mdl-slider.is-upgraded:disabled::-ms-thumb{transform:scale(.25);background:rgba(0,0,0,.26)}.mdl-slider.is-upgraded.is-lowest-value:disabled:focus::-ms-thumb,.mdl-slider.is-upgraded.is-lowest-value:disabled:active::-ms-thumb,.mdl-slider.is-upgraded.is-lowest-value:disabled::-ms-thumb{transform:scale(.25);background:radial-gradient(circle closest-side,transparent 0%,transparent 50%,rgba(0,0,0,.26)50%,rgba(0,0,0,.26)100%)}.mdl-slider.is-upgraded:disabled::-ms-fill-lower{margin-right:6px;background:linear-gradient(to right,transparent,transparent 25px,rgba(0,0,0,.26)25px,rgba(0,0,0,.26)0)}.mdl-slider.is-upgraded:disabled::-ms-fill-upper{margin-left:6px}.mdl-slider.is-upgraded.is-lowest-value:disabled:active::-ms-fill-upper{margin-left:6px}.mdl-slider__ie-container{height:18px;overflow:visible;border:none;margin:none;padding:none}.mdl-slider__container{height:18px;position:relative;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row}.mdl-slider__container,.mdl-slider__background-flex{background:0 0;display:-webkit-flex;display:-ms-flexbox;display:flex}.mdl-slider__background-flex{position:absolute;height:2px;width:calc(100% - 52px);top:50%;left:0;margin:0 26px;overflow:hidden;border:0;padding:0;-webkit-transform:translate(0,-1px);transform:translate(0,-1px)}.mdl-slider__background-lower{background:#3f51b5}.mdl-slider__background-lower,.mdl-slider__background-upper{-webkit-flex:0;-ms-flex:0;flex:0;position:relative;border:0;padding:0}.mdl-slider__background-upper{background:rgba(0,0,0,.26);transition:left .18s cubic-bezier(.4,0,.2,1)}.mdl-snackbar{position:fixed;bottom:0;left:50%;cursor:default;background-color:#323232;z-index:3;display:block;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between;font-family:"Roboto","Helvetica","Arial",sans-serif;will-change:transform;-webkit-transform:translate(0,80px);transform:translate(0,80px);transition:transform .25s cubic-bezier(.4,0,1,1);transition:transform .25s cubic-bezier(.4,0,1,1),-webkit-transform .25s cubic-bezier(.4,0,1,1);pointer-events:none}@media (max-width:479px){.mdl-snackbar{width:100%;left:0;min-height:48px;max-height:80px}}@media (min-width:480px){.mdl-snackbar{min-width:288px;max-width:568px;border-radius:2px;-webkit-transform:translate(-50%,80px);transform:translate(-50%,80px)}}.mdl-snackbar--active{-webkit-transform:translate(0,0);transform:translate(0,0);pointer-events:auto;transition:transform .25s cubic-bezier(0,0,.2,1);transition:transform .25s cubic-bezier(0,0,.2,1),-webkit-transform .25s cubic-bezier(0,0,.2,1)}@media (min-width:480px){.mdl-snackbar--active{-webkit-transform:translate(-50%,0);transform:translate(-50%,0)}}.mdl-snackbar__text{padding:14px 12px 14px 24px;vertical-align:middle;color:#fff;float:left}.mdl-snackbar__action{background:0 0;border:none;color:#ff4081;float:right;padding:14px 24px 14px 12px;font-family:"Roboto","Helvetica","Arial",sans-serif;font-size:14px;font-weight:500;text-transform:uppercase;line-height:1;letter-spacing:0;overflow:hidden;outline:none;opacity:0;pointer-events:none;cursor:pointer;text-decoration:none;text-align:center;-webkit-align-self:center;-ms-flex-item-align:center;align-self:center}.mdl-snackbar__action::-moz-focus-inner{border:0}.mdl-snackbar__action:not([aria-hidden]){opacity:1;pointer-events:auto}.mdl-spinner{display:inline-block;position:relative;width:28px;height:28px}.mdl-spinner:not(.is-upgraded).is-active:after{content:"Loading..."}.mdl-spinner.is-upgraded.is-active{-webkit-animation:mdl-spinner__container-rotate 1568.23529412ms linear infinite;animation:mdl-spinner__container-rotate 1568.23529412ms linear infinite}@-webkit-keyframes mdl-spinner__container-rotate{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes mdl-spinner__container-rotate{to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}.mdl-spinner__layer{position:absolute;width:100%;height:100%;opacity:0}.mdl-spinner__layer-1{border-color:#42a5f5}.mdl-spinner--single-color .mdl-spinner__layer-1{border-color:#3f51b5}.mdl-spinner.is-active .mdl-spinner__layer-1{-webkit-animation:mdl-spinner__fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1)infinite both,mdl-spinner__layer-1-fade-in-out 5332ms cubic-bezier(.4,0,.2,1)infinite both;animation:mdl-spinner__fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1)infinite both,mdl-spinner__layer-1-fade-in-out 5332ms cubic-bezier(.4,0,.2,1)infinite both}.mdl-spinner__layer-2{border-color:#f44336}.mdl-spinner--single-color .mdl-spinner__layer-2{border-color:#3f51b5}.mdl-spinner.is-active .mdl-spinner__layer-2{-webkit-animation:mdl-spinner__fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1)infinite both,mdl-spinner__layer-2-fade-in-out 5332ms cubic-bezier(.4,0,.2,1)infinite both;animation:mdl-spinner__fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1)infinite both,mdl-spinner__layer-2-fade-in-out 5332ms cubic-bezier(.4,0,.2,1)infinite both}.mdl-spinner__layer-3{border-color:#fdd835}.mdl-spinner--single-color .mdl-spinner__layer-3{border-color:#3f51b5}.mdl-spinner.is-active .mdl-spinner__layer-3{-webkit-animation:mdl-spinner__fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1)infinite both,mdl-spinner__layer-3-fade-in-out 5332ms cubic-bezier(.4,0,.2,1)infinite both;animation:mdl-spinner__fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1)infinite both,mdl-spinner__layer-3-fade-in-out 5332ms cubic-bezier(.4,0,.2,1)infinite both}.mdl-spinner__layer-4{border-color:#4caf50}.mdl-spinner--single-color .mdl-spinner__layer-4{border-color:#3f51b5}.mdl-spinner.is-active .mdl-spinner__layer-4{-webkit-animation:mdl-spinner__fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1)infinite both,mdl-spinner__layer-4-fade-in-out 5332ms cubic-bezier(.4,0,.2,1)infinite both;animation:mdl-spinner__fill-unfill-rotate 5332ms cubic-bezier(.4,0,.2,1)infinite both,mdl-spinner__layer-4-fade-in-out 5332ms cubic-bezier(.4,0,.2,1)infinite both}@-webkit-keyframes mdl-spinner__fill-unfill-rotate{12.5%{-webkit-transform:rotate(135deg);transform:rotate(135deg)}25%{-webkit-transform:rotate(270deg);transform:rotate(270deg)}37.5%{-webkit-transform:rotate(405deg);transform:rotate(405deg)}50%{-webkit-transform:rotate(540deg);transform:rotate(540deg)}62.5%{-webkit-transform:rotate(675deg);transform:rotate(675deg)}75%{-webkit-transform:rotate(810deg);transform:rotate(810deg)}87.5%{-webkit-transform:rotate(945deg);transform:rotate(945deg)}to{-webkit-transform:rotate(1080deg);transform:rotate(1080deg)}}@keyframes mdl-spinner__fill-unfill-rotate{12.5%{-webkit-transform:rotate(135deg);transform:rotate(135deg)}25%{-webkit-transform:rotate(270deg);transform:rotate(270deg)}37.5%{-webkit-transform:rotate(405deg);transform:rotate(405deg)}50%{-webkit-transform:rotate(540deg);transform:rotate(540deg)}62.5%{-webkit-transform:rotate(675deg);transform:rotate(675deg)}75%{-webkit-transform:rotate(810deg);transform:rotate(810deg)}87.5%{-webkit-transform:rotate(945deg);transform:rotate(945deg)}to{-webkit-transform:rotate(1080deg);transform:rotate(1080deg)}}@-webkit-keyframes mdl-spinner__layer-1-fade-in-out{from,25%{opacity:.99}26%,89%{opacity:0}90%,100%{opacity:.99}}@keyframes mdl-spinner__layer-1-fade-in-out{from,25%{opacity:.99}26%,89%{opacity:0}90%,100%{opacity:.99}}@-webkit-keyframes mdl-spinner__layer-2-fade-in-out{from,15%{opacity:0}25%,50%{opacity:.99}51%{opacity:0}}@keyframes mdl-spinner__layer-2-fade-in-out{from,15%{opacity:0}25%,50%{opacity:.99}51%{opacity:0}}@-webkit-keyframes mdl-spinner__layer-3-fade-in-out{from,40%{opacity:0}50%,75%{opacity:.99}76%{opacity:0}}@keyframes mdl-spinner__layer-3-fade-in-out{from,40%{opacity:0}50%,75%{opacity:.99}76%{opacity:0}}@-webkit-keyframes mdl-spinner__layer-4-fade-in-out{from,65%{opacity:0}75%,90%{opacity:.99}100%{opacity:0}}@keyframes mdl-spinner__layer-4-fade-in-out{from,65%{opacity:0}75%,90%{opacity:.99}100%{opacity:0}}.mdl-spinner__gap-patch{position:absolute;box-sizing:border-box;top:0;left:45%;width:10%;height:100%;overflow:hidden;border-color:inherit}.mdl-spinner__gap-patch .mdl-spinner__circle{width:1000%;left:-450%}.mdl-spinner__circle-clipper{display:inline-block;position:relative;width:50%;height:100%;overflow:hidden;border-color:inherit}.mdl-spinner__circle-clipper .mdl-spinner__circle{width:200%}.mdl-spinner__circle{box-sizing:border-box;height:100%;border-width:3px;border-style:solid;border-color:inherit;border-bottom-color:transparent!important;border-radius:50%;-webkit-animation:none;animation:none;position:absolute;top:0;right:0;bottom:0;left:0}.mdl-spinner__left .mdl-spinner__circle{border-right-color:transparent!important;-webkit-transform:rotate(129deg);transform:rotate(129deg)}.mdl-spinner.is-active .mdl-spinner__left .mdl-spinner__circle{-webkit-animation:mdl-spinner__left-spin 1333ms cubic-bezier(.4,0,.2,1)infinite both;animation:mdl-spinner__left-spin 1333ms cubic-bezier(.4,0,.2,1)infinite both}.mdl-spinner__right .mdl-spinner__circle{left:-100%;border-left-color:transparent!important;-webkit-transform:rotate(-129deg);transform:rotate(-129deg)}.mdl-spinner.is-active .mdl-spinner__right .mdl-spinner__circle{-webkit-animation:mdl-spinner__right-spin 1333ms cubic-bezier(.4,0,.2,1)infinite both;animation:mdl-spinner__right-spin 1333ms cubic-bezier(.4,0,.2,1)infinite both}@-webkit-keyframes mdl-spinner__left-spin{from{-webkit-transform:rotate(130deg);transform:rotate(130deg)}50%{-webkit-transform:rotate(-5deg);transform:rotate(-5deg)}to{-webkit-transform:rotate(130deg);transform:rotate(130deg)}}@keyframes mdl-spinner__left-spin{from{-webkit-transform:rotate(130deg);transform:rotate(130deg)}50%{-webkit-transform:rotate(-5deg);transform:rotate(-5deg)}to{-webkit-transform:rotate(130deg);transform:rotate(130deg)}}@-webkit-keyframes mdl-spinner__right-spin{from{-webkit-transform:rotate(-130deg);transform:rotate(-130deg)}50%{-webkit-transform:rotate(5deg);transform:rotate(5deg)}to{-webkit-transform:rotate(-130deg);transform:rotate(-130deg)}}@keyframes mdl-spinner__right-spin{from{-webkit-transform:rotate(-130deg);transform:rotate(-130deg)}50%{-webkit-transform:rotate(5deg);transform:rotate(5deg)}to{-webkit-transform:rotate(-130deg);transform:rotate(-130deg)}}.mdl-switch{position:relative;z-index:1;vertical-align:middle;display:inline-block;box-sizing:border-box;width:100%;height:24px;margin:0;padding:0;overflow:visible;-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.mdl-switch.is-upgraded{padding-left:28px}.mdl-switch__input{line-height:24px}.mdl-switch.is-upgraded .mdl-switch__input{position:absolute;width:0;height:0;margin:0;padding:0;opacity:0;-ms-appearance:none;-moz-appearance:none;-webkit-appearance:none;appearance:none;border:none}.mdl-switch__track{background:rgba(0,0,0,.26);position:absolute;left:0;top:5px;height:14px;width:36px;border-radius:14px;cursor:pointer}.mdl-switch.is-checked .mdl-switch__track{background:rgba(63,81,181,.5)}.mdl-switch__track fieldset[disabled] .mdl-switch,.mdl-switch.is-disabled .mdl-switch__track{background:rgba(0,0,0,.12);cursor:auto}.mdl-switch__thumb{background:#fafafa;position:absolute;left:0;top:2px;height:20px;width:20px;border-radius:50%;cursor:pointer;box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12);transition-duration:.28s;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-property:left}.mdl-switch.is-checked .mdl-switch__thumb{background:#3f51b5;left:16px;box-shadow:0 3px 4px 0 rgba(0,0,0,.14),0 3px 3px -2px rgba(0,0,0,.2),0 1px 8px 0 rgba(0,0,0,.12)}.mdl-switch__thumb fieldset[disabled] .mdl-switch,.mdl-switch.is-disabled .mdl-switch__thumb{background:#bdbdbd;cursor:auto}.mdl-switch__focus-helper{position:absolute;top:50%;left:50%;-webkit-transform:translate(-4px,-4px);transform:translate(-4px,-4px);display:inline-block;box-sizing:border-box;width:8px;height:8px;border-radius:50%;background-color:transparent}.mdl-switch.is-focused .mdl-switch__focus-helper{box-shadow:0 0 0 20px rgba(0,0,0,.1);background-color:rgba(0,0,0,.1)}.mdl-switch.is-focused.is-checked .mdl-switch__focus-helper{box-shadow:0 0 0 20px rgba(63,81,181,.26);background-color:rgba(63,81,181,.26)}.mdl-switch__label{position:relative;cursor:pointer;font-size:16px;line-height:24px;margin:0;left:24px}.mdl-switch__label fieldset[disabled] .mdl-switch,.mdl-switch.is-disabled .mdl-switch__label{color:#bdbdbd;cursor:auto}.mdl-switch__ripple-container{position:absolute;z-index:2;top:-12px;left:-14px;box-sizing:border-box;width:48px;height:48px;border-radius:50%;cursor:pointer;overflow:hidden;-webkit-mask-image:-webkit-radial-gradient(circle,#fff,#000);transition-duration:.4s;transition-timing-function:step-end;transition-property:left}.mdl-switch__ripple-container .mdl-ripple{background:#3f51b5}.mdl-switch__ripple-container fieldset[disabled] .mdl-switch,.mdl-switch.is-disabled .mdl-switch__ripple-container{cursor:auto}fieldset[disabled] .mdl-switch .mdl-switch__ripple-container .mdl-ripple,.mdl-switch.is-disabled .mdl-switch__ripple-container .mdl-ripple{background:0 0}.mdl-switch.is-checked .mdl-switch__ripple-container{left:2px}.mdl-tabs{display:block;width:100%}.mdl-tabs__tab-bar{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;-webkit-align-content:space-between;-ms-flex-line-pack:justify;align-content:space-between;-webkit-align-items:flex-start;-ms-flex-align:start;align-items:flex-start;height:48px;padding:0;margin:0;border-bottom:1px solid #e0e0e0}.mdl-tabs__tab{margin:0;border:none;padding:0 24px;float:left;position:relative;display:block;text-decoration:none;height:48px;line-height:48px;text-align:center;font-weight:500;font-size:14px;text-transform:uppercase;color:rgba(0,0,0,.54);overflow:hidden}.mdl-tabs.is-upgraded .mdl-tabs__tab.is-active{color:rgba(0,0,0,.87)}.mdl-tabs.is-upgraded .mdl-tabs__tab.is-active:after{height:2px;width:100%;display:block;content:" ";bottom:0;left:0;position:absolute;background:#3f51b5;-webkit-animation:border-expand .2s cubic-bezier(.4,0,.4,1).01s alternate forwards;animation:border-expand .2s cubic-bezier(.4,0,.4,1).01s alternate forwards;transition:all 1s cubic-bezier(.4,0,1,1)}.mdl-tabs__tab .mdl-tabs__ripple-container{display:block;position:absolute;height:100%;width:100%;left:0;top:0;z-index:1;overflow:hidden}.mdl-tabs__tab .mdl-tabs__ripple-container .mdl-ripple{background:#3f51b5}.mdl-tabs__panel{display:block}.mdl-tabs.is-upgraded .mdl-tabs__panel{display:none}.mdl-tabs.is-upgraded .mdl-tabs__panel.is-active{display:block}@-webkit-keyframes border-expand{0%{opacity:0;width:0}100%{opacity:1;width:100%}}@keyframes border-expand{0%{opacity:0;width:0}100%{opacity:1;width:100%}}.mdl-textfield{position:relative;font-size:16px;display:inline-block;box-sizing:border-box;width:300px;max-width:100%;margin:0;padding:20px 0}.mdl-textfield .mdl-button{position:absolute;bottom:20px}.mdl-textfield--align-right{text-align:right}.mdl-textfield--full-width{width:100%}.mdl-textfield--expandable{min-width:32px;width:auto;min-height:32px}.mdl-textfield__input{border:none;border-bottom:1px solid rgba(0,0,0,.12);display:block;font-size:16px;font-family:"Helvetica","Arial",sans-serif;margin:0;padding:4px 0;width:100%;background:0 0;text-align:left;color:inherit}.mdl-textfield__input[type="number"]{-moz-appearance:textfield}.mdl-textfield__input[type="number"]::-webkit-inner-spin-button,.mdl-textfield__input[type="number"]::-webkit-outer-spin-button{-webkit-appearance:none;margin:0}.mdl-textfield.is-focused .mdl-textfield__input{outline:none}.mdl-textfield.is-invalid .mdl-textfield__input{border-color:#d50000;box-shadow:none}fieldset[disabled] .mdl-textfield .mdl-textfield__input,.mdl-textfield.is-disabled .mdl-textfield__input{background-color:transparent;border-bottom:1px dotted rgba(0,0,0,.12);color:rgba(0,0,0,.26)}.mdl-textfield textarea.mdl-textfield__input{display:block}.mdl-textfield__label{bottom:0;color:rgba(0,0,0,.26);font-size:16px;left:0;right:0;pointer-events:none;position:absolute;display:block;top:24px;width:100%;overflow:hidden;white-space:nowrap;text-align:left}.mdl-textfield.is-dirty .mdl-textfield__label,.mdl-textfield.has-placeholder .mdl-textfield__label{visibility:hidden}.mdl-textfield--floating-label .mdl-textfield__label{transition-duration:.2s;transition-timing-function:cubic-bezier(.4,0,.2,1)}.mdl-textfield--floating-label.has-placeholder .mdl-textfield__label{transition:none}fieldset[disabled] .mdl-textfield .mdl-textfield__label,.mdl-textfield.is-disabled.is-disabled .mdl-textfield__label{color:rgba(0,0,0,.26)}.mdl-textfield--floating-label.is-focused .mdl-textfield__label,.mdl-textfield--floating-label.is-dirty .mdl-textfield__label,.mdl-textfield--floating-label.has-placeholder .mdl-textfield__label{color:#3f51b5;font-size:12px;top:4px;visibility:visible}.mdl-textfield--floating-label.is-focused .mdl-textfield__expandable-holder .mdl-textfield__label,.mdl-textfield--floating-label.is-dirty .mdl-textfield__expandable-holder .mdl-textfield__label,.mdl-textfield--floating-label.has-placeholder .mdl-textfield__expandable-holder .mdl-textfield__label{top:-16px}.mdl-textfield--floating-label.is-invalid .mdl-textfield__label{color:#d50000;font-size:12px}.mdl-textfield__label:after{background-color:#3f51b5;bottom:20px;content:'';height:2px;left:45%;position:absolute;transition-duration:.2s;transition-timing-function:cubic-bezier(.4,0,.2,1);visibility:hidden;width:10px}.mdl-textfield.is-focused .mdl-textfield__label:after{left:0;visibility:visible;width:100%}.mdl-textfield.is-invalid .mdl-textfield__label:after{background-color:#d50000}.mdl-textfield__error{color:#d50000;position:absolute;font-size:12px;margin-top:3px;visibility:hidden;display:block}.mdl-textfield.is-invalid .mdl-textfield__error{visibility:visible}.mdl-textfield__expandable-holder{display:inline-block;position:relative;margin-left:32px;transition-duration:.2s;transition-timing-function:cubic-bezier(.4,0,.2,1);display:inline-block;max-width:.1px}.mdl-textfield.is-focused .mdl-textfield__expandable-holder,.mdl-textfield.is-dirty .mdl-textfield__expandable-holder{max-width:600px}.mdl-textfield__expandable-holder .mdl-textfield__label:after{bottom:0}.mdl-tooltip{-webkit-transform:scale(0);transform:scale(0);-webkit-transform-origin:top center;transform-origin:top center;will-change:transform;z-index:999;background:rgba(97,97,97,.9);border-radius:2px;color:#fff;display:inline-block;font-size:10px;font-weight:500;line-height:14px;max-width:170px;position:fixed;top:-500px;left:-500px;padding:8px;text-align:center}.mdl-tooltip.is-active{-webkit-animation:pulse 200ms cubic-bezier(0,0,.2,1)forwards;animation:pulse 200ms cubic-bezier(0,0,.2,1)forwards}.mdl-tooltip--large{line-height:14px;font-size:14px;padding:16px}@-webkit-keyframes pulse{0%{-webkit-transform:scale(0);transform:scale(0);opacity:0}50%{-webkit-transform:scale(.99);transform:scale(.99)}100%{-webkit-transform:scale(1);transform:scale(1);opacity:1;visibility:visible}}@keyframes pulse{0%{-webkit-transform:scale(0);transform:scale(0);opacity:0}50%{-webkit-transform:scale(.99);transform:scale(.99)}100%{-webkit-transform:scale(1);transform:scale(1);opacity:1;visibility:visible}}.mdl-shadow--2dp{box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 3px 1px -2px rgba(0,0,0,.2),0 1px 5px 0 rgba(0,0,0,.12)}.mdl-shadow--3dp{box-shadow:0 3px 4px 0 rgba(0,0,0,.14),0 3px 3px -2px rgba(0,0,0,.2),0 1px 8px 0 rgba(0,0,0,.12)}.mdl-shadow--4dp{box-shadow:0 4px 5px 0 rgba(0,0,0,.14),0 1px 10px 0 rgba(0,0,0,.12),0 2px 4px -1px rgba(0,0,0,.2)}.mdl-shadow--6dp{box-shadow:0 6px 10px 0 rgba(0,0,0,.14),0 1px 18px 0 rgba(0,0,0,.12),0 3px 5px -1px rgba(0,0,0,.2)}.mdl-shadow--8dp{box-shadow:0 8px 10px 1px rgba(0,0,0,.14),0 3px 14px 2px rgba(0,0,0,.12),0 5px 5px -3px rgba(0,0,0,.2)}.mdl-shadow--16dp{box-shadow:0 16px 24px 2px rgba(0,0,0,.14),0 6px 30px 5px rgba(0,0,0,.12),0 8px 10px -5px rgba(0,0,0,.2)}.mdl-shadow--24dp{box-shadow:0 9px 46px 8px rgba(0,0,0,.14),0 11px 15px -7px rgba(0,0,0,.12),0 24px 38px 3px rgba(0,0,0,.2)}.mdl-grid{display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex-flow:row wrap;-ms-flex-flow:row wrap;flex-flow:row wrap;margin:0 auto;-webkit-align-items:stretch;-ms-flex-align:stretch;align-items:stretch}.mdl-grid.mdl-grid--no-spacing{padding:0}.mdl-cell{box-sizing:border-box}.mdl-cell--top{-webkit-align-self:flex-start;-ms-flex-item-align:start;align-self:flex-start}.mdl-cell--middle{-webkit-align-self:center;-ms-flex-item-align:center;align-self:center}.mdl-cell--bottom{-webkit-align-self:flex-end;-ms-flex-item-align:end;align-self:flex-end}.mdl-cell--stretch{-webkit-align-self:stretch;-ms-flex-item-align:stretch;align-self:stretch}.mdl-grid.mdl-grid--no-spacing>.mdl-cell{margin:0}.mdl-cell--order-1{-webkit-order:1;-ms-flex-order:1;order:1}.mdl-cell--order-2{-webkit-order:2;-ms-flex-order:2;order:2}.mdl-cell--order-3{-webkit-order:3;-ms-flex-order:3;order:3}.mdl-cell--order-4{-webkit-order:4;-ms-flex-order:4;order:4}.mdl-cell--order-5{-webkit-order:5;-ms-flex-order:5;order:5}.mdl-cell--order-6{-webkit-order:6;-ms-flex-order:6;order:6}.mdl-cell--order-7{-webkit-order:7;-ms-flex-order:7;order:7}.mdl-cell--order-8{-webkit-order:8;-ms-flex-order:8;order:8}.mdl-cell--order-9{-webkit-order:9;-ms-flex-order:9;order:9}.mdl-cell--order-10{-webkit-order:10;-ms-flex-order:10;order:10}.mdl-cell--order-11{-webkit-order:11;-ms-flex-order:11;order:11}.mdl-cell--order-12{-webkit-order:12;-ms-flex-order:12;order:12}@media (max-width:479px){.mdl-grid{padding:8px}.mdl-cell{margin:8px;width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell{width:100%}.mdl-cell--hide-phone{display:none!important}.mdl-cell--order-1-phone.mdl-cell--order-1-phone{-webkit-order:1;-ms-flex-order:1;order:1}.mdl-cell--order-2-phone.mdl-cell--order-2-phone{-webkit-order:2;-ms-flex-order:2;order:2}.mdl-cell--order-3-phone.mdl-cell--order-3-phone{-webkit-order:3;-ms-flex-order:3;order:3}.mdl-cell--order-4-phone.mdl-cell--order-4-phone{-webkit-order:4;-ms-flex-order:4;order:4}.mdl-cell--order-5-phone.mdl-cell--order-5-phone{-webkit-order:5;-ms-flex-order:5;order:5}.mdl-cell--order-6-phone.mdl-cell--order-6-phone{-webkit-order:6;-ms-flex-order:6;order:6}.mdl-cell--order-7-phone.mdl-cell--order-7-phone{-webkit-order:7;-ms-flex-order:7;order:7}.mdl-cell--order-8-phone.mdl-cell--order-8-phone{-webkit-order:8;-ms-flex-order:8;order:8}.mdl-cell--order-9-phone.mdl-cell--order-9-phone{-webkit-order:9;-ms-flex-order:9;order:9}.mdl-cell--order-10-phone.mdl-cell--order-10-phone{-webkit-order:10;-ms-flex-order:10;order:10}.mdl-cell--order-11-phone.mdl-cell--order-11-phone{-webkit-order:11;-ms-flex-order:11;order:11}.mdl-cell--order-12-phone.mdl-cell--order-12-phone{-webkit-order:12;-ms-flex-order:12;order:12}.mdl-cell--1-col,.mdl-cell--1-col-phone.mdl-cell--1-col-phone{width:calc(25% - 16px)}.mdl-grid--no-spacing>.mdl-cell--1-col,.mdl-grid--no-spacing>.mdl-cell--1-col-phone.mdl-cell--1-col-phone{width:25%}.mdl-cell--2-col,.mdl-cell--2-col-phone.mdl-cell--2-col-phone{width:calc(50% - 16px)}.mdl-grid--no-spacing>.mdl-cell--2-col,.mdl-grid--no-spacing>.mdl-cell--2-col-phone.mdl-cell--2-col-phone{width:50%}.mdl-cell--3-col,.mdl-cell--3-col-phone.mdl-cell--3-col-phone{width:calc(75% - 16px)}.mdl-grid--no-spacing>.mdl-cell--3-col,.mdl-grid--no-spacing>.mdl-cell--3-col-phone.mdl-cell--3-col-phone{width:75%}.mdl-cell--4-col,.mdl-cell--4-col-phone.mdl-cell--4-col-phone{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--4-col,.mdl-grid--no-spacing>.mdl-cell--4-col-phone.mdl-cell--4-col-phone{width:100%}.mdl-cell--5-col,.mdl-cell--5-col-phone.mdl-cell--5-col-phone{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--5-col,.mdl-grid--no-spacing>.mdl-cell--5-col-phone.mdl-cell--5-col-phone{width:100%}.mdl-cell--6-col,.mdl-cell--6-col-phone.mdl-cell--6-col-phone{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--6-col,.mdl-grid--no-spacing>.mdl-cell--6-col-phone.mdl-cell--6-col-phone{width:100%}.mdl-cell--7-col,.mdl-cell--7-col-phone.mdl-cell--7-col-phone{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--7-col,.mdl-grid--no-spacing>.mdl-cell--7-col-phone.mdl-cell--7-col-phone{width:100%}.mdl-cell--8-col,.mdl-cell--8-col-phone.mdl-cell--8-col-phone{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--8-col,.mdl-grid--no-spacing>.mdl-cell--8-col-phone.mdl-cell--8-col-phone{width:100%}.mdl-cell--9-col,.mdl-cell--9-col-phone.mdl-cell--9-col-phone{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--9-col,.mdl-grid--no-spacing>.mdl-cell--9-col-phone.mdl-cell--9-col-phone{width:100%}.mdl-cell--10-col,.mdl-cell--10-col-phone.mdl-cell--10-col-phone{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--10-col,.mdl-grid--no-spacing>.mdl-cell--10-col-phone.mdl-cell--10-col-phone{width:100%}.mdl-cell--11-col,.mdl-cell--11-col-phone.mdl-cell--11-col-phone{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--11-col,.mdl-grid--no-spacing>.mdl-cell--11-col-phone.mdl-cell--11-col-phone{width:100%}.mdl-cell--12-col,.mdl-cell--12-col-phone.mdl-cell--12-col-phone{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--12-col,.mdl-grid--no-spacing>.mdl-cell--12-col-phone.mdl-cell--12-col-phone{width:100%}.mdl-cell--1-offset,.mdl-cell--1-offset-phone.mdl-cell--1-offset-phone{margin-left:calc(25% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--1-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--1-offset-phone.mdl-cell--1-offset-phone{margin-left:25%}.mdl-cell--2-offset,.mdl-cell--2-offset-phone.mdl-cell--2-offset-phone{margin-left:calc(50% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--2-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--2-offset-phone.mdl-cell--2-offset-phone{margin-left:50%}.mdl-cell--3-offset,.mdl-cell--3-offset-phone.mdl-cell--3-offset-phone{margin-left:calc(75% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--3-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--3-offset-phone.mdl-cell--3-offset-phone{margin-left:75%}}@media (min-width:480px) and (max-width:839px){.mdl-grid{padding:8px}.mdl-cell{margin:8px;width:calc(50% - 16px)}.mdl-grid--no-spacing>.mdl-cell{width:50%}.mdl-cell--hide-tablet{display:none!important}.mdl-cell--order-1-tablet.mdl-cell--order-1-tablet{-webkit-order:1;-ms-flex-order:1;order:1}.mdl-cell--order-2-tablet.mdl-cell--order-2-tablet{-webkit-order:2;-ms-flex-order:2;order:2}.mdl-cell--order-3-tablet.mdl-cell--order-3-tablet{-webkit-order:3;-ms-flex-order:3;order:3}.mdl-cell--order-4-tablet.mdl-cell--order-4-tablet{-webkit-order:4;-ms-flex-order:4;order:4}.mdl-cell--order-5-tablet.mdl-cell--order-5-tablet{-webkit-order:5;-ms-flex-order:5;order:5}.mdl-cell--order-6-tablet.mdl-cell--order-6-tablet{-webkit-order:6;-ms-flex-order:6;order:6}.mdl-cell--order-7-tablet.mdl-cell--order-7-tablet{-webkit-order:7;-ms-flex-order:7;order:7}.mdl-cell--order-8-tablet.mdl-cell--order-8-tablet{-webkit-order:8;-ms-flex-order:8;order:8}.mdl-cell--order-9-tablet.mdl-cell--order-9-tablet{-webkit-order:9;-ms-flex-order:9;order:9}.mdl-cell--order-10-tablet.mdl-cell--order-10-tablet{-webkit-order:10;-ms-flex-order:10;order:10}.mdl-cell--order-11-tablet.mdl-cell--order-11-tablet{-webkit-order:11;-ms-flex-order:11;order:11}.mdl-cell--order-12-tablet.mdl-cell--order-12-tablet{-webkit-order:12;-ms-flex-order:12;order:12}.mdl-cell--1-col,.mdl-cell--1-col-tablet.mdl-cell--1-col-tablet{width:calc(12.5% - 16px)}.mdl-grid--no-spacing>.mdl-cell--1-col,.mdl-grid--no-spacing>.mdl-cell--1-col-tablet.mdl-cell--1-col-tablet{width:12.5%}.mdl-cell--2-col,.mdl-cell--2-col-tablet.mdl-cell--2-col-tablet{width:calc(25% - 16px)}.mdl-grid--no-spacing>.mdl-cell--2-col,.mdl-grid--no-spacing>.mdl-cell--2-col-tablet.mdl-cell--2-col-tablet{width:25%}.mdl-cell--3-col,.mdl-cell--3-col-tablet.mdl-cell--3-col-tablet{width:calc(37.5% - 16px)}.mdl-grid--no-spacing>.mdl-cell--3-col,.mdl-grid--no-spacing>.mdl-cell--3-col-tablet.mdl-cell--3-col-tablet{width:37.5%}.mdl-cell--4-col,.mdl-cell--4-col-tablet.mdl-cell--4-col-tablet{width:calc(50% - 16px)}.mdl-grid--no-spacing>.mdl-cell--4-col,.mdl-grid--no-spacing>.mdl-cell--4-col-tablet.mdl-cell--4-col-tablet{width:50%}.mdl-cell--5-col,.mdl-cell--5-col-tablet.mdl-cell--5-col-tablet{width:calc(62.5% - 16px)}.mdl-grid--no-spacing>.mdl-cell--5-col,.mdl-grid--no-spacing>.mdl-cell--5-col-tablet.mdl-cell--5-col-tablet{width:62.5%}.mdl-cell--6-col,.mdl-cell--6-col-tablet.mdl-cell--6-col-tablet{width:calc(75% - 16px)}.mdl-grid--no-spacing>.mdl-cell--6-col,.mdl-grid--no-spacing>.mdl-cell--6-col-tablet.mdl-cell--6-col-tablet{width:75%}.mdl-cell--7-col,.mdl-cell--7-col-tablet.mdl-cell--7-col-tablet{width:calc(87.5% - 16px)}.mdl-grid--no-spacing>.mdl-cell--7-col,.mdl-grid--no-spacing>.mdl-cell--7-col-tablet.mdl-cell--7-col-tablet{width:87.5%}.mdl-cell--8-col,.mdl-cell--8-col-tablet.mdl-cell--8-col-tablet{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--8-col,.mdl-grid--no-spacing>.mdl-cell--8-col-tablet.mdl-cell--8-col-tablet{width:100%}.mdl-cell--9-col,.mdl-cell--9-col-tablet.mdl-cell--9-col-tablet{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--9-col,.mdl-grid--no-spacing>.mdl-cell--9-col-tablet.mdl-cell--9-col-tablet{width:100%}.mdl-cell--10-col,.mdl-cell--10-col-tablet.mdl-cell--10-col-tablet{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--10-col,.mdl-grid--no-spacing>.mdl-cell--10-col-tablet.mdl-cell--10-col-tablet{width:100%}.mdl-cell--11-col,.mdl-cell--11-col-tablet.mdl-cell--11-col-tablet{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--11-col,.mdl-grid--no-spacing>.mdl-cell--11-col-tablet.mdl-cell--11-col-tablet{width:100%}.mdl-cell--12-col,.mdl-cell--12-col-tablet.mdl-cell--12-col-tablet{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--12-col,.mdl-grid--no-spacing>.mdl-cell--12-col-tablet.mdl-cell--12-col-tablet{width:100%}.mdl-cell--1-offset,.mdl-cell--1-offset-tablet.mdl-cell--1-offset-tablet{margin-left:calc(12.5% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--1-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--1-offset-tablet.mdl-cell--1-offset-tablet{margin-left:12.5%}.mdl-cell--2-offset,.mdl-cell--2-offset-tablet.mdl-cell--2-offset-tablet{margin-left:calc(25% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--2-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--2-offset-tablet.mdl-cell--2-offset-tablet{margin-left:25%}.mdl-cell--3-offset,.mdl-cell--3-offset-tablet.mdl-cell--3-offset-tablet{margin-left:calc(37.5% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--3-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--3-offset-tablet.mdl-cell--3-offset-tablet{margin-left:37.5%}.mdl-cell--4-offset,.mdl-cell--4-offset-tablet.mdl-cell--4-offset-tablet{margin-left:calc(50% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--4-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--4-offset-tablet.mdl-cell--4-offset-tablet{margin-left:50%}.mdl-cell--5-offset,.mdl-cell--5-offset-tablet.mdl-cell--5-offset-tablet{margin-left:calc(62.5% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--5-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--5-offset-tablet.mdl-cell--5-offset-tablet{margin-left:62.5%}.mdl-cell--6-offset,.mdl-cell--6-offset-tablet.mdl-cell--6-offset-tablet{margin-left:calc(75% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--6-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--6-offset-tablet.mdl-cell--6-offset-tablet{margin-left:75%}.mdl-cell--7-offset,.mdl-cell--7-offset-tablet.mdl-cell--7-offset-tablet{margin-left:calc(87.5% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--7-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--7-offset-tablet.mdl-cell--7-offset-tablet{margin-left:87.5%}}@media (min-width:840px){.mdl-grid{padding:8px}.mdl-cell{margin:8px;width:calc(33.3333333333% - 16px)}.mdl-grid--no-spacing>.mdl-cell{width:33.3333333333%}.mdl-cell--hide-desktop{display:none!important}.mdl-cell--order-1-desktop.mdl-cell--order-1-desktop{-webkit-order:1;-ms-flex-order:1;order:1}.mdl-cell--order-2-desktop.mdl-cell--order-2-desktop{-webkit-order:2;-ms-flex-order:2;order:2}.mdl-cell--order-3-desktop.mdl-cell--order-3-desktop{-webkit-order:3;-ms-flex-order:3;order:3}.mdl-cell--order-4-desktop.mdl-cell--order-4-desktop{-webkit-order:4;-ms-flex-order:4;order:4}.mdl-cell--order-5-desktop.mdl-cell--order-5-desktop{-webkit-order:5;-ms-flex-order:5;order:5}.mdl-cell--order-6-desktop.mdl-cell--order-6-desktop{-webkit-order:6;-ms-flex-order:6;order:6}.mdl-cell--order-7-desktop.mdl-cell--order-7-desktop{-webkit-order:7;-ms-flex-order:7;order:7}.mdl-cell--order-8-desktop.mdl-cell--order-8-desktop{-webkit-order:8;-ms-flex-order:8;order:8}.mdl-cell--order-9-desktop.mdl-cell--order-9-desktop{-webkit-order:9;-ms-flex-order:9;order:9}.mdl-cell--order-10-desktop.mdl-cell--order-10-desktop{-webkit-order:10;-ms-flex-order:10;order:10}.mdl-cell--order-11-desktop.mdl-cell--order-11-desktop{-webkit-order:11;-ms-flex-order:11;order:11}.mdl-cell--order-12-desktop.mdl-cell--order-12-desktop{-webkit-order:12;-ms-flex-order:12;order:12}.mdl-cell--1-col,.mdl-cell--1-col-desktop.mdl-cell--1-col-desktop{width:calc(8.3333333333% - 16px)}.mdl-grid--no-spacing>.mdl-cell--1-col,.mdl-grid--no-spacing>.mdl-cell--1-col-desktop.mdl-cell--1-col-desktop{width:8.3333333333%}.mdl-cell--2-col,.mdl-cell--2-col-desktop.mdl-cell--2-col-desktop{width:calc(16.6666666667% - 16px)}.mdl-grid--no-spacing>.mdl-cell--2-col,.mdl-grid--no-spacing>.mdl-cell--2-col-desktop.mdl-cell--2-col-desktop{width:16.6666666667%}.mdl-cell--3-col,.mdl-cell--3-col-desktop.mdl-cell--3-col-desktop{width:calc(25% - 16px)}.mdl-grid--no-spacing>.mdl-cell--3-col,.mdl-grid--no-spacing>.mdl-cell--3-col-desktop.mdl-cell--3-col-desktop{width:25%}.mdl-cell--4-col,.mdl-cell--4-col-desktop.mdl-cell--4-col-desktop{width:calc(33.3333333333% - 16px)}.mdl-grid--no-spacing>.mdl-cell--4-col,.mdl-grid--no-spacing>.mdl-cell--4-col-desktop.mdl-cell--4-col-desktop{width:33.3333333333%}.mdl-cell--5-col,.mdl-cell--5-col-desktop.mdl-cell--5-col-desktop{width:calc(41.6666666667% - 16px)}.mdl-grid--no-spacing>.mdl-cell--5-col,.mdl-grid--no-spacing>.mdl-cell--5-col-desktop.mdl-cell--5-col-desktop{width:41.6666666667%}.mdl-cell--6-col,.mdl-cell--6-col-desktop.mdl-cell--6-col-desktop{width:calc(50% - 16px)}.mdl-grid--no-spacing>.mdl-cell--6-col,.mdl-grid--no-spacing>.mdl-cell--6-col-desktop.mdl-cell--6-col-desktop{width:50%}.mdl-cell--7-col,.mdl-cell--7-col-desktop.mdl-cell--7-col-desktop{width:calc(58.3333333333% - 16px)}.mdl-grid--no-spacing>.mdl-cell--7-col,.mdl-grid--no-spacing>.mdl-cell--7-col-desktop.mdl-cell--7-col-desktop{width:58.3333333333%}.mdl-cell--8-col,.mdl-cell--8-col-desktop.mdl-cell--8-col-desktop{width:calc(66.6666666667% - 16px)}.mdl-grid--no-spacing>.mdl-cell--8-col,.mdl-grid--no-spacing>.mdl-cell--8-col-desktop.mdl-cell--8-col-desktop{width:66.6666666667%}.mdl-cell--9-col,.mdl-cell--9-col-desktop.mdl-cell--9-col-desktop{width:calc(75% - 16px)}.mdl-grid--no-spacing>.mdl-cell--9-col,.mdl-grid--no-spacing>.mdl-cell--9-col-desktop.mdl-cell--9-col-desktop{width:75%}.mdl-cell--10-col,.mdl-cell--10-col-desktop.mdl-cell--10-col-desktop{width:calc(83.3333333333% - 16px)}.mdl-grid--no-spacing>.mdl-cell--10-col,.mdl-grid--no-spacing>.mdl-cell--10-col-desktop.mdl-cell--10-col-desktop{width:83.3333333333%}.mdl-cell--11-col,.mdl-cell--11-col-desktop.mdl-cell--11-col-desktop{width:calc(91.6666666667% - 16px)}.mdl-grid--no-spacing>.mdl-cell--11-col,.mdl-grid--no-spacing>.mdl-cell--11-col-desktop.mdl-cell--11-col-desktop{width:91.6666666667%}.mdl-cell--12-col,.mdl-cell--12-col-desktop.mdl-cell--12-col-desktop{width:calc(100% - 16px)}.mdl-grid--no-spacing>.mdl-cell--12-col,.mdl-grid--no-spacing>.mdl-cell--12-col-desktop.mdl-cell--12-col-desktop{width:100%}.mdl-cell--1-offset,.mdl-cell--1-offset-desktop.mdl-cell--1-offset-desktop{margin-left:calc(8.3333333333% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--1-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--1-offset-desktop.mdl-cell--1-offset-desktop{margin-left:8.3333333333%}.mdl-cell--2-offset,.mdl-cell--2-offset-desktop.mdl-cell--2-offset-desktop{margin-left:calc(16.6666666667% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--2-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--2-offset-desktop.mdl-cell--2-offset-desktop{margin-left:16.6666666667%}.mdl-cell--3-offset,.mdl-cell--3-offset-desktop.mdl-cell--3-offset-desktop{margin-left:calc(25% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--3-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--3-offset-desktop.mdl-cell--3-offset-desktop{margin-left:25%}.mdl-cell--4-offset,.mdl-cell--4-offset-desktop.mdl-cell--4-offset-desktop{margin-left:calc(33.3333333333% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--4-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--4-offset-desktop.mdl-cell--4-offset-desktop{margin-left:33.3333333333%}.mdl-cell--5-offset,.mdl-cell--5-offset-desktop.mdl-cell--5-offset-desktop{margin-left:calc(41.6666666667% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--5-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--5-offset-desktop.mdl-cell--5-offset-desktop{margin-left:41.6666666667%}.mdl-cell--6-offset,.mdl-cell--6-offset-desktop.mdl-cell--6-offset-desktop{margin-left:calc(50% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--6-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--6-offset-desktop.mdl-cell--6-offset-desktop{margin-left:50%}.mdl-cell--7-offset,.mdl-cell--7-offset-desktop.mdl-cell--7-offset-desktop{margin-left:calc(58.3333333333% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--7-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--7-offset-desktop.mdl-cell--7-offset-desktop{margin-left:58.3333333333%}.mdl-cell--8-offset,.mdl-cell--8-offset-desktop.mdl-cell--8-offset-desktop{margin-left:calc(66.6666666667% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--8-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--8-offset-desktop.mdl-cell--8-offset-desktop{margin-left:66.6666666667%}.mdl-cell--9-offset,.mdl-cell--9-offset-desktop.mdl-cell--9-offset-desktop{margin-left:calc(75% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--9-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--9-offset-desktop.mdl-cell--9-offset-desktop{margin-left:75%}.mdl-cell--10-offset,.mdl-cell--10-offset-desktop.mdl-cell--10-offset-desktop{margin-left:calc(83.3333333333% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--10-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--10-offset-desktop.mdl-cell--10-offset-desktop{margin-left:83.3333333333%}.mdl-cell--11-offset,.mdl-cell--11-offset-desktop.mdl-cell--11-offset-desktop{margin-left:calc(91.6666666667% + 8px)}.mdl-grid.mdl-grid--no-spacing>.mdl-cell--11-offset,.mdl-grid.mdl-grid--no-spacing>.mdl-cell--11-offset-desktop.mdl-cell--11-offset-desktop{margin-left:91.6666666667%}}
-/*# sourceMappingURL=material.min.css.map */
diff --git a/etc/cli.angular.io/theme.css b/etc/cli.angular.io/theme.css
deleted file mode 100644
index b6a336e98b0c..000000000000
--- a/etc/cli.angular.io/theme.css
+++ /dev/null
@@ -1 +0,0 @@
-.console{width:360px;max-width:92vw;margin-left:15px;margin-right:40px;text-align:left;border-radius:5px;margin-bottom:10px}@media (max-width:830px){.console{margin-right:auto;margin-left:auto}}.console__head{overflow:hidden;background-color:#d5d5d5;padding:8px 15px;border-top-left-radius:5px;border-top-right-radius:5px}.console__dot{float:left;width:12px;height:12px;border-radius:50%;margin-right:7px;box-shadow:0 1px 1px 0 rgba(0,0,0,.2)}.console__dot--red{background-color:#ff6057}.console__dot--yellow{background-color:#ffc22e}.console__dot--green{background-color:#28ca40}.console__body{background-color:#1e1e1e;padding:30px 17px 20px;border-bottom-left-radius:5px;border-bottom-right-radius:5px}.console__prompt{display:block;margin-bottom:15px;font-family:"Source Code Pro",monospace;font-size:15px}.console__prompt::before{content:">";padding-right:15px;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.mdl-base{height:100vh}
diff --git a/integration/angular_cli/e2e/tsconfig.json b/integration/angular_cli/e2e/tsconfig.json
index c92199cfd63f..a82df00eef37 100644
--- a/integration/angular_cli/e2e/tsconfig.json
+++ b/integration/angular_cli/e2e/tsconfig.json
@@ -6,7 +6,6 @@
"target": "es2018",
"types": [
"jasmine",
- "jasminewd2",
"node"
]
}
diff --git a/integration/angular_cli/karma.conf.js b/integration/angular_cli/karma.conf.js
index a9c5297de149..646bfde5c8a6 100644
--- a/integration/angular_cli/karma.conf.js
+++ b/integration/angular_cli/karma.conf.js
@@ -18,6 +18,9 @@ module.exports = function (config) {
client: {
clearContext: false, // leave Jasmine Spec Runner output visible in browser
},
+ jasmineHtmlReporter: {
+ suppressAll: true // removes the duplicated traces
+ },
coverageReporter: {
dir: path.join(__dirname, 'coverage'),
subdir: '.',
diff --git a/integration/angular_cli/package.json b/integration/angular_cli/package.json
index feba5be5709a..fb653a1c165a 100644
--- a/integration/angular_cli/package.json
+++ b/integration/angular_cli/package.json
@@ -8,7 +8,7 @@
"test": "ng test",
"lint": "ng lint",
"e2e": "ng e2e --prod",
- "postinstall": "webdriver-manager update --standalone false --gecko false --versions.chrome 85.0.4183.38"
+ "postinstall": "webdriver-manager update --standalone false --gecko false --versions.chrome 89.0.4389.0"
},
"private": true,
"dependencies": {
@@ -30,7 +30,6 @@
"@angular/compiler-cli": "~9.1.1",
"@types/node": "^12.11.1",
"@types/jasmine": "~3.5.0",
- "@types/jasminewd2": "~2.0.3",
"codelyzer": "^5.1.2",
"jasmine-core": "~3.6.0",
"jasmine-spec-reporter": "~5.0.0",
@@ -40,10 +39,10 @@
"karma-jasmine": "~3.3.0",
"karma-jasmine-html-reporter": "^1.5.0",
"protractor": "~5.4.3",
- "puppeteer": "5.2.1",
+ "puppeteer": "6.0.0",
"ts-node": "~8.3.0",
"tslint": "~6.1.0",
- "typescript": "~3.9.2"
+ "typescript": "~4.0.0"
},
"resolutions": {
"rxjs": "6.5.4",
diff --git a/lib/bootstrap-local.js b/lib/bootstrap-local.js
index e491aad25108..b8fbe0962bb8 100644
--- a/lib/bootstrap-local.js
+++ b/lib/bootstrap-local.js
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
diff --git a/lib/packages.ts b/lib/packages.ts
index dff7a59efbe1..68f76ef586bb 100644
--- a/lib/packages.ts
+++ b/lib/packages.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
@@ -41,8 +41,12 @@ export interface PackageInfo {
export type PackageMap = { [name: string]: PackageInfo };
+export function loadRootPackageJson() {
+ return require('../package.json');
+}
+
function loadPackageJson(p: string) {
- const root = require('../package.json');
+ const root = loadRootPackageJson();
const pkg = require(p);
for (const key of Object.keys(root)) {
@@ -84,7 +88,7 @@ function loadPackageJson(p: string) {
case 'engines':
pkg['engines'] = {
'node': '>= 10.13.0',
- 'npm': '>= 6.11.0',
+ 'npm': '^6.11.0 || ^7.5.6',
'yarn': '>= 1.13.0',
};
break;
@@ -156,25 +160,8 @@ function _getSnapshotHash(_pkg: PackageInfo): string {
}
-let stableVersion = '';
-let experimentalVersion = '';
-function _getVersionFromGit(experimental: boolean): string {
- if (stableVersion && experimentalVersion) {
- return experimental ? experimentalVersion : stableVersion;
- }
-
- const hasLocalChanges = _exec(`git status --porcelain`) != '';
- const scmVersionTagRaw = _exec(`git describe --match v[0-9]*.[0-9]*.[0-9]* --abbrev=7 --tags`)
- .slice(1);
- stableVersion = scmVersionTagRaw.replace(/-([0-9]+)-g/, '+$1.');
- if (hasLocalChanges) {
- stableVersion += stableVersion.includes('+') ? '.with-local-changes' : '+with-local-changes';
- }
-
- experimentalVersion = stableToExperimentalVersion(stableVersion);
-
- return experimental ? experimentalVersion : stableVersion;
-}
+const stableVersion = loadRootPackageJson().version;
+const experimentalVersion = stableToExperimentalVersion(stableVersion);
/**
* Convert a stable version to its experimental equivalent. For example,
@@ -241,9 +228,7 @@ export const packages: PackageMap =
dependencies: [],
reverseDependencies: [],
- get version() {
- return _getVersionFromGit(experimental);
- },
+ version: experimental ? experimentalVersion : stableVersion,
};
return packages;
diff --git a/lib/registries.ts b/lib/registries.ts
index 0bfea087bc2e..2e21b8a4716d 100644
--- a/lib/registries.ts
+++ b/lib/registries.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
diff --git a/package.json b/package.json
index 57e213e6c2f1..6e635d9486f0 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@angular/devkit-repo",
- "version": "0.0.0",
+ "version": "11.2.19",
"private": true,
"description": "Software Development Kit for Angular",
"bin": {
@@ -33,9 +33,9 @@
"templates": "node ./bin/devkit-admin templates",
"validate": "node ./bin/devkit-admin validate",
"preinstall": "node ./tools/yarn/check-yarn.js",
- "postinstall": "yarn webdriver-update && yarn ngcc",
+ "postinstall": "yarn webdriver-update && yarn ngcc && yarn husky install",
"//webdriver-update-README": "ChromeDriver version must match Puppeteer Chromium version, see https://github.com/GoogleChrome/puppeteer/releases http://chromedriver.chromium.org/downloads",
- "webdriver-update": "webdriver-manager update --standalone false --gecko false --versions.chrome 85.0.4183.38",
+ "webdriver-update": "webdriver-manager update --standalone false --gecko false --versions.chrome 89.0.4389.0",
"ngcc": "ngcc --properties es2015 browser module main --first-only --create-ivy-entry-points"
},
"repository": {
@@ -64,34 +64,35 @@
]
},
"devDependencies": {
- "@angular/animations": "11.0.0-next.6",
- "@angular/cdk": "10.2.5",
- "@angular/common": "11.0.0-next.6",
- "@angular/compiler": "11.0.0-next.6",
- "@angular/compiler-cli": "11.0.0-next.6",
- "@angular/core": "11.0.0-next.6",
- "@angular/dev-infra-private": "https://github.com/angular/dev-infra-private-builds.git#d79eccd725b4421e50b566bf001f553b561e3813",
- "@angular/forms": "11.0.0-next.6",
- "@angular/localize": "11.0.0-next.6",
- "@angular/material": "10.2.5",
- "@angular/platform-browser": "11.0.0-next.6",
- "@angular/platform-browser-dynamic": "11.0.0-next.6",
- "@angular/platform-server": "11.0.0-next.6",
- "@angular/router": "11.0.0-next.6",
- "@angular/service-worker": "11.0.0-next.6",
- "@babel/core": "7.11.6",
- "@babel/generator": "7.11.6",
- "@babel/plugin-transform-runtime": "7.11.5",
- "@babel/preset-env": "7.11.5",
- "@babel/runtime": "7.11.2",
- "@babel/template": "7.10.4",
- "@bazel/bazelisk": "1.7.2",
- "@bazel/buildifier": "3.5.0",
- "@bazel/jasmine": "2.2.1",
- "@bazel/typescript": "2.2.1",
- "@jsdevtools/coverage-istanbul-loader": "3.0.3",
- "@types/babel__core": "7.1.10",
- "@types/babel__template": "7.0.3",
+ "@angular/animations": "11.2.0-next.0",
+ "@angular/cdk": "11.1.1",
+ "@angular/common": "11.2.0-next.0",
+ "@angular/compiler": "11.2.0-next.0",
+ "@angular/compiler-cli": "11.2.0-next.0",
+ "@angular/core": "11.2.0-next.0",
+ "@angular/dev-infra-private": "https://github.com/angular/dev-infra-private-builds.git#0fd0f441648577e8c3966e9b59a88fefe350f514",
+ "@angular/forms": "11.2.0-next.0",
+ "@angular/localize": "11.2.0-next.0",
+ "@angular/material": "11.1.1",
+ "@angular/platform-browser": "11.2.0-next.0",
+ "@angular/platform-browser-dynamic": "11.2.0-next.0",
+ "@angular/platform-server": "11.2.0-next.0",
+ "@angular/router": "11.2.0-next.0",
+ "@angular/service-worker": "11.2.0-next.0",
+ "@babel/core": "7.12.10",
+ "@babel/generator": "7.12.11",
+ "@babel/plugin-transform-runtime": "7.12.10",
+ "@babel/preset-env": "7.12.11",
+ "@babel/runtime": "7.12.5",
+ "@babel/template": "7.12.7",
+ "@bazel/bazelisk": "1.7.3",
+ "@bazel/buildifier": "4.0.0",
+ "@bazel/jasmine": "3.2.1",
+ "@bazel/typescript": "3.2.1",
+ "@discoveryjs/json-ext": "0.5.2",
+ "@jsdevtools/coverage-istanbul-loader": "3.0.5",
+ "@types/babel__core": "7.1.12",
+ "@types/babel__template": "7.4.0",
"@types/browserslist": "^4.4.0",
"@types/cacache": "^12.0.1",
"@types/caniuse-lite": "^1.0.0",
@@ -101,10 +102,11 @@
"@types/express": "^4.16.0",
"@types/find-cache-dir": "^3.0.0",
"@types/glob": "^7.1.1",
+ "@types/http-proxy": "^1.17.4",
"@types/inquirer": "^7.3.0",
- "@types/jasmine": "~3.5.0",
+ "@types/jasmine": "~3.6.0",
"@types/karma": "^5.0.0",
- "@types/license-checker-webpack-plugin": "^0.0.2",
+ "@types/license-checker-webpack-plugin": "^0.0.3",
"@types/loader-utils": "^2.0.0",
"@types/minimatch": "3.0.3",
"@types/minimist": "^1.2.0",
@@ -128,119 +130,117 @@
"@yarnpkg/lockfile": "1.1.0",
"ajv": "6.12.6",
"ansi-colors": "4.1.1",
- "autoprefixer": "9.8.6",
- "babel-loader": "8.1.0",
+ "autoprefixer": "10.2.4",
+ "babel-loader": "8.2.2",
"bootstrap": "^4.0.0",
"browserslist": "^4.9.1",
"cacache": "15.0.5",
"caniuse-lite": "^1.0.30001032",
- "circular-dependency-plugin": "5.2.0",
+ "circular-dependency-plugin": "5.2.2",
"codelyzer": "^6.0.0",
"common-tags": "^1.8.0",
"conventional-changelog": "^3.0.0",
"conventional-commits-parser": "^3.0.0",
- "copy-webpack-plugin": "6.2.1",
- "core-js": "3.6.5",
- "css-loader": "5.0.0",
- "cssnano": "4.1.10",
+ "copy-webpack-plugin": "6.3.2",
+ "core-js": "3.8.3",
+ "critters": "0.0.12",
+ "css-loader": "5.0.1",
+ "cssnano": "5.0.2",
"debug": "^4.1.1",
- "enhanced-resolve": "5.2.0",
+ "enhanced-resolve": "5.7.0",
"express": "4.17.1",
"fast-json-stable-stringify": "2.1.0",
- "file-loader": "6.1.1",
+ "file-loader": "6.2.0",
"find-cache-dir": "3.3.1",
"font-awesome": "^4.7.0",
"gh-got": "^9.0.0",
"git-raw-commits": "^2.0.0",
"glob": "7.1.6",
- "husky": "^4.0.10",
+ "http-proxy": "^1.18.1",
+ "https-proxy-agent": "5.0.0",
+ "husky": "5.0.8",
"inquirer": "7.3.3",
"jasmine": "^3.3.1",
"jasmine-core": "~3.6.0",
"jasmine-spec-reporter": "~6.0.0",
- "jest-worker": "26.5.0",
+ "jest-worker": "26.6.2",
"jquery": "^3.3.1",
- "jsonc-parser": "2.3.1",
- "karma": "~5.2.0",
+ "jsonc-parser": "3.0.0",
+ "karma": "~6.1.0",
"karma-chrome-launcher": "~3.1.0",
"karma-coverage": "~2.0.3",
"karma-jasmine": "~4.0.0",
"karma-jasmine-html-reporter": "^1.5.0",
"karma-source-map-support": "1.4.0",
- "less": "3.12.2",
- "less-loader": "7.0.2",
+ "less": "4.1.1",
+ "less-loader": "7.3.0",
"license-checker": "^25.0.0",
- "license-checker-webpack-plugin": "0.1.5",
+ "license-checker-webpack-plugin": "0.2.1",
"loader-utils": "2.0.0",
- "mini-css-extract-plugin": "1.0.0",
+ "magic-string": "0.25.7",
+ "mini-css-extract-plugin": "1.3.5",
"minimatch": "3.0.4",
- "minimist": "^1.2.0",
- "ng-packagr": "~11.0.0-next.0",
+ "minimist": "1.2.6",
+ "ng-packagr": "~11.1.0",
"node-fetch": "^2.2.0",
"npm-registry-client": "8.6.0",
- "open": "7.3.0",
- "ora": "5.1.0",
- "pacote": "11.1.4",
+ "open": "7.4.0",
+ "ora": "5.3.0",
+ "pacote": "11.2.4",
"parse5-html-rewriting-stream": "6.0.1",
"pidtree": "^0.5.0",
"pidusage": "^2.0.17",
"pnp-webpack-plugin": "1.6.4",
"popper.js": "^1.14.1",
- "postcss": "7.0.32",
- "postcss-import": "12.0.1",
- "postcss-loader": "4.0.4",
+ "postcss": "8.2.15",
+ "postcss-import": "14.0.0",
+ "postcss-loader": "4.2.0",
"prettier": "^2.0.0",
"protractor": "~7.0.0",
- "puppeteer": "5.3.1",
- "quicktype-core": "^6.0.15",
+ "puppeteer": "6.0.0",
+ "quicktype-core": "^6.0.69",
"raw-loader": "4.0.2",
"regenerator-runtime": "0.13.7",
- "resolve-url-loader": "3.1.1",
+ "resolve-url-loader": "4.0.0",
"rimraf": "3.0.2",
- "rollup": "2.30.0",
+ "rollup": "2.38.4",
"rxjs": "6.6.3",
- "sass": "1.27.0",
- "sass-loader": "10.0.3",
- "sauce-connect-proxy": "https://saucelabs.com/downloads/sc-4.6.2-linux.tar.gz",
- "semver": "7.3.2",
+ "sass": "1.32.6",
+ "sass-loader": "10.1.1",
+ "sauce-connect-proxy": "https://saucelabs.com/downloads/sc-4.6.4-linux.tar.gz",
+ "semver": "7.3.4",
"source-map": "0.7.3",
- "source-map-loader": "1.1.1",
- "source-map-support": "0.5.16",
+ "source-map-loader": "1.1.3",
+ "source-map-support": "0.5.19",
"spdx-satisfies": "^5.0.0",
- "speed-measure-webpack-plugin": "1.3.3",
+ "speed-measure-webpack-plugin": "1.4.2",
"style-loader": "2.0.0",
- "stylus": "0.54.7",
- "stylus-loader": "4.1.1",
- "symbol-observable": "2.0.3",
+ "stylus": "0.54.8",
+ "stylus-loader": "4.3.3",
+ "symbol-observable": "3.0.0",
"tar": "^6.0.0",
"temp": "^0.9.0",
- "terser": "5.3.5",
+ "terser": "5.5.1",
"terser-webpack-plugin": "4.2.3",
"text-table": "0.2.0",
"through2": "^4.0.0",
"tree-kill": "1.2.2",
- "ts-api-guardian": "0.5.0",
+ "ts-api-guardian": "0.6.0",
"ts-node": "^5.0.0",
"tslib": "^2.0.0",
"tslint": "^6.1.3",
"tslint-no-circular-imports": "^0.7.0",
"tslint-sonarts": "1.9.0",
- "typescript": "4.0.3",
- "verdaccio": "4.8.1",
+ "typescript": "4.1.5",
+ "verdaccio": "4.11.0",
"verdaccio-auth-memory": "^9.7.2",
"webpack": "4.44.2",
"webpack-dev-middleware": "3.7.2",
- "webpack-dev-server": "3.10.3",
- "webpack-merge": "5.2.0",
- "webpack-sources": "2.0.1",
- "webpack-subresource-integrity": "1.5.0",
+ "webpack-dev-server": "3.11.3",
+ "webpack-merge": "5.7.3",
+ "webpack-sources": "2.2.0",
+ "webpack-subresource-integrity": "1.5.2",
"worker-plugin": "5.0.0",
- "zone.js": "^0.10.2"
- },
- "husky": {
- "hooks": {
- "commit-msg": "yarn -s ng-dev commit-message pre-commit-validate --file-env-variable HUSKY_GIT_PARAMS",
- "prepare-commit-msg": "yarn -s ng-dev commit-message restore-commit-message-draft --file-env-variable HUSKY_GIT_PARAMS"
- }
+ "zone.js": "^0.11.3"
}
}
diff --git a/packages/angular/cli/BUILD.bazel b/packages/angular/cli/BUILD.bazel
index 8395b2a215eb..9e20fc349a84 100644
--- a/packages/angular/cli/BUILD.bazel
+++ b/packages/angular/cli/BUILD.bazel
@@ -46,7 +46,7 @@ ts_library(
"//packages/angular/cli:commands/update.ts",
"//packages/angular/cli:commands/version.ts",
"//packages/angular/cli:commands/run.ts",
- "//packages/angular/cli:commands/xi18n.ts",
+ "//packages/angular/cli:commands/extract-i18n.ts",
# @external_end
],
data = glob(
@@ -69,6 +69,7 @@ ts_library(
"//packages/angular_devkit/core/node",
"//packages/angular_devkit/schematics",
"//packages/angular_devkit/schematics/tools",
+ "@npm//@angular/core",
"@npm//@types/debug",
"@npm//@types/inquirer",
"@npm//@types/node",
@@ -78,6 +79,9 @@ ts_library(
"@npm//@types/universal-analytics",
"@npm//@types/uuid",
"@npm//ansi-colors",
+ "@npm//jsonc-parser",
+ "@npm//open",
+ "@npm//ora",
],
)
@@ -223,8 +227,8 @@ ts_json_schema(
)
ts_json_schema(
- name = "xi18n_schema",
- src = "commands/xi18n.json",
+ name = "extract-i18n_schema",
+ src = "commands/extract-i18n.json",
data = [
"commands/definitions.json",
],
diff --git a/packages/angular/cli/README.md b/packages/angular/cli/README.md
index c4c5e25a8440..96b40aacbad5 100644
--- a/packages/angular/cli/README.md
+++ b/packages/angular/cli/README.md
@@ -250,7 +250,7 @@ In addition to this one, another, more elaborated way to capture a CPU profile u
## Documentation
-The documentation for the Angular CLI is located in this repo's [wiki](https://angular.io/cli).
+The documentation for the Angular CLI is located on our [documentation website](https://angular.io/cli).
## License
diff --git a/packages/angular/cli/commands.json b/packages/angular/cli/commands.json
index 85de6cc170d3..0b65947a0647 100644
--- a/packages/angular/cli/commands.json
+++ b/packages/angular/cli/commands.json
@@ -6,6 +6,7 @@
"deploy": "./commands/deploy.json",
"doc": "./commands/doc.json",
"e2e": "./commands/e2e.json",
+ "extract-i18n": "./commands/extract-i18n.json",
"make-this-awesome": "./commands/easter-egg.json",
"generate": "./commands/generate.json",
"help": "./commands/help.json",
@@ -15,6 +16,5 @@
"serve": "./commands/serve.json",
"test": "./commands/test.json",
"update": "./commands/update.json",
- "version": "./commands/version.json",
- "xi18n": "./commands/xi18n.json"
+ "version": "./commands/version.json"
}
diff --git a/packages/angular/cli/commands/add-impl.ts b/packages/angular/cli/commands/add-impl.ts
index 321601e22e22..a58f7ec33e95 100644
--- a/packages/angular/cli/commands/add-impl.ts
+++ b/packages/angular/cli/commands/add-impl.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
@@ -13,15 +13,16 @@ import { PackageManager } from '../lib/config/schema';
import { isPackageNameSafeForAnalytics } from '../models/analytics';
import { Arguments } from '../models/interface';
import { RunSchematicOptions, SchematicCommand } from '../models/schematic-command';
-import { installPackage, installTempPackage } from '../tasks/install-package';
import { colors } from '../utilities/color';
-import { getPackageManager } from '../utilities/package-manager';
+import { installPackage, installTempPackage } from '../utilities/install-package';
+import { ensureCompatibleNpm, getPackageManager } from '../utilities/package-manager';
import {
NgAddSaveDepedency,
PackageManifest,
fetchPackageManifest,
fetchPackageMetadata,
} from '../utilities/package-metadata';
+import { Spinner } from '../utilities/spinner';
import { Schema as AddCommandSchema } from './add';
const npa = require('npm-package-arg');
@@ -38,6 +39,8 @@ export class AddCommand extends SchematicCommand {
}
async run(options: AddCommandSchema & Arguments) {
+ await ensureCompatibleNpm(this.context.root);
+
if (!options.collection) {
this.logger.fatal(
`The "ng add" command requires a name argument to be specified eg. ` +
@@ -79,12 +82,18 @@ export class AddCommand extends SchematicCommand {
}
}
+ const spinner = new Spinner();
+
+ spinner.start('Determining package manager...');
const packageManager = await getPackageManager(this.context.root);
const usingYarn = packageManager === PackageManager.Yarn;
+ spinner.info(`Using package manager: ${colors.grey(packageManager)}`);
if (packageIdentifier.type === 'tag' && !packageIdentifier.rawSpec) {
// only package name provided; search for viable version
// plus special cases for packages that did not have peer deps setup
+ spinner.start('Searching for compatible package version...');
+
let packageMetadata;
try {
packageMetadata = await fetchPackageMetadata(packageIdentifier.name, this.logger, {
@@ -93,7 +102,7 @@ export class AddCommand extends SchematicCommand {
verbose: options.verbose,
});
} catch (e) {
- this.logger.error('Unable to fetch package metadata: ' + e.message);
+ spinner.fail('Unable to load package information from registry: ' + e.message);
return 1;
}
@@ -111,11 +120,14 @@ export class AddCommand extends SchematicCommand {
) {
packageIdentifier = npa.resolve('@angular/pwa', '0.12');
}
+ } else {
+ packageIdentifier = npa.resolve(latestManifest.name, latestManifest.version);
}
+ spinner.succeed(`Found compatible package version: ${colors.grey(packageIdentifier)}.`);
} else if (!latestManifest || (await this.hasMismatchedPeer(latestManifest))) {
// 'latest' is invalid so search for most recent matching package
const versionManifests = Object.values(packageMetadata.versions).filter(
- (value: PackageManifest) => !prerelease(value.version),
+ (value: PackageManifest) => !prerelease(value.version) && !value.deprecated,
) as PackageManifest[];
versionManifests.sort((a, b) => rcompare(a.version, b.version, true));
@@ -129,10 +141,14 @@ export class AddCommand extends SchematicCommand {
}
if (!newIdentifier) {
- this.logger.warn("Unable to find compatible package. Using 'latest'.");
+ spinner.warn("Unable to find compatible package. Using 'latest'.");
} else {
packageIdentifier = newIdentifier;
+ spinner.succeed(`Found compatible package version: ${colors.grey(packageIdentifier)}.`);
}
+ } else {
+ packageIdentifier = npa.resolve(latestManifest.name, latestManifest.version);
+ spinner.succeed(`Found compatible package version: ${colors.grey(packageIdentifier)}.`);
}
}
@@ -140,6 +156,7 @@ export class AddCommand extends SchematicCommand {
let savePackage: NgAddSaveDepedency | undefined;
try {
+ spinner.start('Loading package information from registry...');
const manifest = await fetchPackageManifest(packageIdentifier, this.logger, {
registry: options.registry,
verbose: options.verbose,
@@ -150,41 +167,51 @@ export class AddCommand extends SchematicCommand {
collectionName = manifest.name;
if (await this.hasMismatchedPeer(manifest)) {
- this.logger.warn(
+ spinner.warn(
'Package has unmet peer dependencies. Adding the package may not succeed.',
);
+ } else {
+ spinner.succeed(`Package information loaded.`);
}
} catch (e) {
- this.logger.error('Unable to fetch package manifest: ' + e.message);
+ spinner.fail(`Unable to fetch package information for '${packageIdentifier}': ${e.message}`);
return 1;
}
- if (savePackage === false) {
- // Temporary packages are located in a different directory
- // Hence we need to resolve them using the temp path
- const tempPath = installTempPackage(
- packageIdentifier.raw,
- this.logger,
- packageManager,
- options.registry ? [`--registry="${options.registry}"`] : undefined,
- );
- const resolvedCollectionPath = require.resolve(
- join(collectionName, 'package.json'),
- {
- paths: [tempPath],
- },
- );
+ try {
+ spinner.start('Installing package...');
+ if (savePackage === false) {
+ // Temporary packages are located in a different directory
+ // Hence we need to resolve them using the temp path
+ const tempPath = installTempPackage(
+ packageIdentifier.raw,
+ undefined,
+ packageManager,
+ options.registry ? [`--registry="${options.registry}"`] : undefined,
+ );
+ const resolvedCollectionPath = require.resolve(
+ join(collectionName, 'package.json'),
+ {
+ paths: [tempPath],
+ },
+ );
- collectionName = dirname(resolvedCollectionPath);
- } else {
- installPackage(
- packageIdentifier.raw,
- this.logger,
- packageManager,
- savePackage,
- options.registry ? [`--registry="${options.registry}"`] : undefined,
- );
+ collectionName = dirname(resolvedCollectionPath);
+ } else {
+ installPackage(
+ packageIdentifier.raw,
+ undefined,
+ packageManager,
+ savePackage,
+ options.registry ? [`--registry="${options.registry}"`] : undefined,
+ );
+ }
+ spinner.succeed('Package successfully installed.');
+ } catch (error) {
+ spinner.fail(`Package installation failed: ${error.message}`);
+
+ return 1;
}
return this.executeSchematic(collectionName, options['--']);
diff --git a/packages/angular/cli/commands/analytics-impl.ts b/packages/angular/cli/commands/analytics-impl.ts
index 9174005d7b18..9897437d50c0 100644
--- a/packages/angular/cli/commands/analytics-impl.ts
+++ b/packages/angular/cli/commands/analytics-impl.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
diff --git a/packages/angular/cli/commands/build-impl.ts b/packages/angular/cli/commands/build-impl.ts
index 0bdd50b57949..e0336279a456 100644
--- a/packages/angular/cli/commands/build-impl.ts
+++ b/packages/angular/cli/commands/build-impl.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
diff --git a/packages/angular/cli/commands/build-long.md b/packages/angular/cli/commands/build-long.md
index 4e10498f4cd5..a28a5f56d532 100644
--- a/packages/angular/cli/commands/build-long.md
+++ b/packages/angular/cli/commands/build-long.md
@@ -3,7 +3,7 @@ When used to build a library, a different builder is invoked, and only the `ts-c
All other options apply only to building applications.
The application builder uses the [webpack](https://webpack.js.org/) build tool, with default configuration options specified in the workspace configuration file (`angular.json`) or with a named alternative configuration.
-A "production" configuration is created by default when you use the CLI to create the project, and you can use that configuration by specifying the `--configuration="production"` or the `--prod="true"` option.
+A "production" configuration is created by default when you use the CLI to create the project, and you can use that configuration by specifying the `--configuration="production"` or the `--prod` option.
The configuration options generally correspond to the command options.
You can override individual configuration defaults by specifying the corresponding options on the command line.
diff --git a/packages/angular/cli/commands/config-impl.ts b/packages/angular/cli/commands/config-impl.ts
index 944ca1df100a..3144f23f7bd9 100644
--- a/packages/angular/cli/commands/config-impl.ts
+++ b/packages/angular/cli/commands/config-impl.ts
@@ -1,21 +1,12 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
-import {
- InvalidJsonCharacterException,
- JsonArray,
- JsonObject,
- JsonParseMode,
- JsonValue,
- parseJson,
- tags,
-} from '@angular-devkit/core';
-import { writeFileSync } from 'fs';
+import { JsonValue, tags } from '@angular-devkit/core';
import { v4 as uuidV4 } from 'uuid';
import { Command } from '../models/command';
import { Arguments, CommandScope } from '../models/interface';
@@ -24,50 +15,16 @@ import {
migrateLegacyGlobalConfig,
validateWorkspace,
} from '../utilities/config';
-import { Schema as ConfigCommandSchema, Value as ConfigCommandSchemaValue } from './config';
-
-function _validateBoolean(value: string) {
- if (('' + value).trim() === 'true') {
- return true;
- } else if (('' + value).trim() === 'false') {
- return false;
- } else {
- throw new Error(`Invalid value type; expected Boolean, received ${JSON.stringify(value)}.`);
- }
-}
-function _validateString(value: string) {
- return value;
-}
-function _validateAnalytics(value: string) {
- if (value === '') {
- // Disable analytics.
- return null;
- } else {
- return value;
- }
-}
-function _validateAnalyticsSharingUuid(value: string) {
- if (value == '') {
- return uuidV4();
- } else {
- return value;
- }
-}
-function _validateAnalyticsSharingTracking(value: string) {
- if (!value.match(/^GA-\d+-\d+$/)) {
- throw new Error(`Invalid GA property ID: ${JSON.stringify(value)}.`);
- }
-
- return value;
-}
-
-const validCliPaths = new Map JsonValue>([
- ['cli.warnings.versionMismatch', _validateBoolean],
- ['cli.defaultCollection', _validateString],
- ['cli.packageManager', _validateString],
- ['cli.analytics', _validateAnalytics],
- ['cli.analyticsSharing.tracking', _validateAnalyticsSharingTracking],
- ['cli.analyticsSharing.uuid', _validateAnalyticsSharingUuid],
+import { JSONFile, parseJson } from '../utilities/json-file';
+import { Schema as ConfigCommandSchema } from './config';
+
+const validCliPaths = new Map string) | undefined>([
+ ['cli.warnings.versionMismatch', undefined],
+ ['cli.defaultCollection', undefined],
+ ['cli.packageManager', undefined],
+ ['cli.analytics', undefined],
+ ['cli.analyticsSharing.tracking', undefined],
+ ['cli.analyticsSharing.uuid', v => v ? `${v}` : uuidV4()],
]);
/**
@@ -106,92 +63,17 @@ function parseJsonPath(path: string): (string | number)[] {
return result.filter(fragment => fragment != null);
}
-function getValueFromPath(
- root: T,
- path: string,
-): JsonValue | undefined {
- const fragments = parseJsonPath(path);
-
- try {
- return fragments.reduce((value: JsonValue | undefined, current: string | number) => {
- if (value == undefined || typeof value != 'object') {
- return undefined;
- } else if (typeof current == 'string' && !Array.isArray(value)) {
- return value[current];
- } else if (typeof current == 'number' && Array.isArray(value)) {
- return value[current];
- } else {
- return undefined;
- }
- }, root);
- } catch {
- return undefined;
- }
-}
-
-function setValueFromPath(
- root: T,
- path: string,
- newValue: JsonValue,
-): JsonValue | undefined {
- const fragments = parseJsonPath(path);
-
- try {
- return fragments.reduce((value: JsonValue | undefined, current: string | number, index: number) => {
- if (value == undefined || typeof value != 'object') {
- return undefined;
- } else if (typeof current == 'string' && !Array.isArray(value)) {
- if (index === fragments.length - 1) {
- value[current] = newValue;
- } else if (value[current] == undefined) {
- if (typeof fragments[index + 1] == 'number') {
- value[current] = [];
- } else if (typeof fragments[index + 1] == 'string') {
- value[current] = {};
- }
- }
-
- return value[current];
- } else if (typeof current == 'number' && Array.isArray(value)) {
- if (index === fragments.length - 1) {
- value[current] = newValue;
- } else if (value[current] == undefined) {
- if (typeof fragments[index + 1] == 'number') {
- value[current] = [];
- } else if (typeof fragments[index + 1] == 'string') {
- value[current] = {};
- }
- }
-
- return value[current];
- } else {
- return undefined;
- }
- }, root);
- } catch {
- return undefined;
- }
-}
-
-function normalizeValue(value: ConfigCommandSchemaValue, path: string): JsonValue {
- const cliOptionType = validCliPaths.get(path);
- if (cliOptionType) {
- return cliOptionType('' + value);
- }
-
- if (typeof value === 'string') {
- try {
- return parseJson(value, JsonParseMode.Loose);
- } catch (e) {
- if (e instanceof InvalidJsonCharacterException && !value.startsWith('{')) {
- return value;
- } else {
- throw e;
- }
- }
+function normalizeValue(value: string | undefined | boolean | number): JsonValue | undefined {
+ const valueString = `${value}`.trim();
+ if (valueString === 'true') {
+ return true;
+ } else if (valueString === 'false') {
+ return false;
+ } else if (isFinite(+valueString)) {
+ return +valueString;
}
- return value;
+ return value || undefined;
}
export class ConfigCommand extends Command {
@@ -212,7 +94,7 @@ export class ConfigCommand extends Command {
We found a global configuration that was used in Angular CLI 1.
It has been automatically migrated.`);
}
- } catch {}
+ } catch { }
}
if (options.value == undefined) {
@@ -222,35 +104,35 @@ export class ConfigCommand extends Command {
return 1;
}
- return this.get(config.value, options);
+ return this.get(config, options);
} else {
return this.set(options);
}
}
- private get(config: JsonObject, options: ConfigCommandSchema) {
+ private get(jsonFile: JSONFile, options: ConfigCommandSchema) {
let value;
if (options.jsonPath) {
- value = getValueFromPath(config, options.jsonPath);
+ value = jsonFile.get(parseJsonPath(options.jsonPath));
} else {
- value = config;
+ value = jsonFile.content;
}
if (value === undefined) {
this.logger.error('Value cannot be found.');
return 1;
- } else if (typeof value == 'object') {
- this.logger.info(JSON.stringify(value, null, 2));
+ } else if (typeof value === 'string') {
+ this.logger.info(value);
} else {
- this.logger.info(value.toString());
+ this.logger.info(JSON.stringify(value, null, 2));
}
return 0;
}
private async set(options: ConfigCommandSchema) {
- if (!options.jsonPath || !options.jsonPath.trim()) {
+ if (!options.jsonPath?.trim()) {
throw new Error('Invalid Path.');
}
@@ -269,28 +151,25 @@ export class ConfigCommand extends Command {
return 1;
}
- // TODO: Modify & save without destroying comments
- const configValue = config.value;
-
- const value = normalizeValue(options.value || '', options.jsonPath);
- const result = setValueFromPath(configValue, options.jsonPath, value);
+ const jsonPath = parseJsonPath(options.jsonPath);
+ const value = validCliPaths.get(options.jsonPath)?.(options.value) ?? options.value;
+ const modified = config.modify(jsonPath, normalizeValue(value));
- if (result === undefined) {
+ if (!modified) {
this.logger.error('Value cannot be found.');
return 1;
}
try {
- await validateWorkspace(configValue);
+ await validateWorkspace(parseJson(config.content));
} catch (error) {
this.logger.fatal(error.message);
return 1;
}
- const output = JSON.stringify(configValue, null, 2);
- writeFileSync(configPath, output);
+ config.save();
return 0;
}
diff --git a/packages/angular/cli/commands/config.json b/packages/angular/cli/commands/config.json
index d0c3d59520b3..21a289f269d6 100644
--- a/packages/angular/cli/commands/config.json
+++ b/packages/angular/cli/commands/config.json
@@ -31,7 +31,7 @@
},
"global": {
"type": "boolean",
- "description": "When true, accesses the global configuration in the caller's home directory.",
+ "description": "Access the global configuration in the caller's home directory.",
"default": false,
"aliases": ["g"]
}
diff --git a/packages/angular/cli/commands/definitions.json b/packages/angular/cli/commands/definitions.json
index 1713520fc1d1..222a790bf09e 100644
--- a/packages/angular/cli/commands/definitions.json
+++ b/packages/angular/cli/commands/definitions.json
@@ -14,14 +14,14 @@
}
},
"configuration": {
- "description": "A named build target, as specified in the \"configurations\" section of angular.json.\nEach named target is accompanied by a configuration of option defaults for that target.\nSetting this explicitly overrides the \"--prod\" flag",
+ "description": "One or more named builder configurations as a comma-separated list as specified in the \"configurations\" section of angular.json.\nThe builder uses the named configurations to run the given target.\nFor more information, see https://angular.io/guide/workspace-config#alternate-build-configurations.\nSetting this explicitly overrides the \"--prod\" flag.",
"type": "string",
"aliases": [
"c"
]
},
"prod": {
- "description": "Shorthand for \"--configuration=production\".\nWhen true, sets the build configuration to the production target.\nBy default, the production target is set up in the workspace configuration such that all builds make use of bundling, limited tree-shaking, and also limited dead code elimination.",
+ "description": "Shorthand for \"--configuration=production\".\nSet the build configuration to the production target.\nBy default, the production target is set up in the workspace configuration such that all builds make use of bundling, limited tree-shaking, and also limited dead code elimination.",
"type": "boolean"
}
}
@@ -42,13 +42,13 @@
"type": "boolean",
"default": false,
"aliases": [ "d" ],
- "description": "When true, runs through and reports activity without writing out results."
+ "description": "Run through and reports activity without writing out results."
},
"force": {
"type": "boolean",
"default": false,
"aliases": [ "f" ],
- "description": "When true, forces overwriting of existing files."
+ "description": "Force overwriting of existing files."
}
}
},
@@ -57,12 +57,12 @@
"interactive": {
"type": "boolean",
"default": "true",
- "description": "When false, disables interactive input prompts."
+ "description": "Enable interactive input prompts."
},
"defaults": {
"type": "boolean",
"default": "false",
- "description": "When true, disables interactive input prompts for options with a default."
+ "description": "Disable interactive input prompts for options with a default."
}
}
}
diff --git a/packages/angular/cli/commands/deploy-impl.ts b/packages/angular/cli/commands/deploy-impl.ts
index 9ce45a22ed28..819b63f61986 100644
--- a/packages/angular/cli/commands/deploy-impl.ts
+++ b/packages/angular/cli/commands/deploy-impl.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
diff --git a/packages/angular/cli/commands/deploy-long.md b/packages/angular/cli/commands/deploy-long.md
index 5d600ef2e8d8..afe619565c0a 100644
--- a/packages/angular/cli/commands/deploy-long.md
+++ b/packages/angular/cli/commands/deploy-long.md
@@ -8,15 +8,15 @@ For example:
```json
"projects": {
- "my-project": {
- ...
- "architect": {
- ...
- "deploy": {
- "builder": "@angular/fire:deploy",
- "options": {}
- }
- }
- }
+ "my-project": {
+ ...
+ "architect": {
+ ...
+ "deploy": {
+ "builder": "@angular/fire:deploy",
+ "options": {}
+ }
}
+ }
+}
```
\ No newline at end of file
diff --git a/packages/angular/cli/commands/deploy.json b/packages/angular/cli/commands/deploy.json
index 9bfc66786ca4..30e84ab4e0ac 100644
--- a/packages/angular/cli/commands/deploy.json
+++ b/packages/angular/cli/commands/deploy.json
@@ -20,7 +20,7 @@
}
},
"configuration": {
- "description": "A named build target, as specified in the \"configurations\" section of angular.json.\nEach named target is accompanied by a configuration of option defaults for that target.",
+ "description": "One or more named builder configurations as a comma-separated list as specified in the \"configurations\" section of angular.json.\nThe builder uses the named configurations to run the given target.\nFor more information, see https://angular.io/guide/workspace-config#alternate-build-configurations.",
"type": "string",
"aliases": [
"c"
diff --git a/packages/angular/cli/commands/doc-impl.ts b/packages/angular/cli/commands/doc-impl.ts
index 587d760c3346..6369fe2fd054 100644
--- a/packages/angular/cli/commands/doc-impl.ts
+++ b/packages/angular/cli/commands/doc-impl.ts
@@ -1,17 +1,16 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
+import * as open from 'open';
import { Command } from '../models/command';
import { Arguments } from '../models/interface';
import { Schema as DocCommandSchema } from './doc';
-const open = require('open');
-
export class DocCommand extends Command {
public async run(options: DocCommandSchema & Arguments) {
if (!options.keyword) {
@@ -39,7 +38,7 @@ export class DocCommand extends Command {
// and use it if we can find it
try {
/* tslint:disable-next-line:no-implicit-dependencies */
- const currentNgVersion = require('@angular/core').VERSION.major;
+ const currentNgVersion = (await import('@angular/core')).VERSION.major;
domain = `v${currentNgVersion}.angular.io`;
} catch (e) { }
}
@@ -50,11 +49,8 @@ export class DocCommand extends Command {
searchUrl = `https://${domain}/docs?search=${options.keyword}`;
}
- // We should wrap `open` in a new Promise because `open` is already resolved
- await new Promise(() => {
- open(searchUrl, {
- wait: false,
- });
+ await open(searchUrl, {
+ wait: false,
});
}
}
diff --git a/packages/angular/cli/commands/doc.json b/packages/angular/cli/commands/doc.json
index b43f448d03cb..eb8c31d935bd 100644
--- a/packages/angular/cli/commands/doc.json
+++ b/packages/angular/cli/commands/doc.json
@@ -24,7 +24,7 @@
"aliases": ["s"],
"type": "boolean",
"default": false,
- "description": "When true, searches all of angular.io. Otherwise, searches only API reference documentation."
+ "description": "Search all of angular.io. Otherwise, searches only API reference documentation."
},
"version" : {
"oneOf": [
diff --git a/packages/angular/cli/commands/e2e-impl.ts b/packages/angular/cli/commands/e2e-impl.ts
index f24a44ca4122..1fe6a373ba2c 100644
--- a/packages/angular/cli/commands/e2e-impl.ts
+++ b/packages/angular/cli/commands/e2e-impl.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
diff --git a/packages/angular/cli/commands/easter-egg-impl.ts b/packages/angular/cli/commands/easter-egg-impl.ts
index 8e70d46dc827..22e43d6fe996 100644
--- a/packages/angular/cli/commands/easter-egg-impl.ts
+++ b/packages/angular/cli/commands/easter-egg-impl.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
diff --git a/packages/angular/cli/commands/xi18n-impl.ts b/packages/angular/cli/commands/extract-i18n-impl.ts
similarity index 56%
rename from packages/angular/cli/commands/xi18n-impl.ts
rename to packages/angular/cli/commands/extract-i18n-impl.ts
index ae0e9253a3ca..e41e9bcd26f1 100644
--- a/packages/angular/cli/commands/xi18n-impl.ts
+++ b/packages/angular/cli/commands/extract-i18n-impl.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
@@ -8,12 +8,12 @@
import { ArchitectCommand } from '../models/architect-command';
import { Arguments } from '../models/interface';
-import { Schema as Xi18nCommandSchema } from './xi18n';
+import { Schema as ExtractI18nCommandSchema } from './extract-i18n';
-export class Xi18nCommand extends ArchitectCommand {
+export class ExtractI18nCommand extends ArchitectCommand {
public readonly target = 'extract-i18n';
- public async run(options: Xi18nCommandSchema & Arguments) {
+ public async run(options: ExtractI18nCommandSchema & Arguments) {
const version = process.version.substr(1).split('.');
if (Number(version[0]) === 12 && Number(version[1]) === 0) {
this.logger.error(
@@ -23,6 +23,11 @@ export class Xi18nCommand extends ArchitectCommand {
return 1;
}
+ const commandName = process.argv[2];
+ if (['xi18n', 'i18n-extract'].includes(commandName)) {
+ this.logger.warn(`Warning: "ng ${commandName}" has been deprecated and will be removed in a future major version. Please use "ng extract-i18n" instead.`);
+ }
+
return this.runArchitectTarget(options);
}
}
diff --git a/packages/angular/cli/commands/xi18n.json b/packages/angular/cli/commands/extract-i18n.json
similarity index 70%
rename from packages/angular/cli/commands/xi18n.json
rename to packages/angular/cli/commands/extract-i18n.json
index c48d958440fa..85ecb0ffeeed 100644
--- a/packages/angular/cli/commands/xi18n.json
+++ b/packages/angular/cli/commands/extract-i18n.json
@@ -1,13 +1,13 @@
{
"$schema": "http://json-schema.org/schema",
- "$id": "ng-cli://commands/xi18n.json",
+ "$id": "ng-cli://commands/extract-i18n.json",
"description": "Extracts i18n messages from source code.",
"$longDescription": "",
- "$aliases": ["i18n-extract"],
+ "$aliases": ["i18n-extract", "xi18n"],
"$scope": "in",
"$type": "architect",
- "$impl": "./xi18n-impl#Xi18nCommand",
+ "$impl": "./extract-i18n-impl#ExtractI18nCommand",
"type": "object",
"allOf": [
diff --git a/packages/angular/cli/commands/generate-impl.ts b/packages/angular/cli/commands/generate-impl.ts
index c32ccd7b87ea..6741be40f9e4 100644
--- a/packages/angular/cli/commands/generate-impl.ts
+++ b/packages/angular/cli/commands/generate-impl.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
diff --git a/packages/angular/cli/commands/help-impl.ts b/packages/angular/cli/commands/help-impl.ts
index 9c30c5e2f608..82edd6a441b4 100644
--- a/packages/angular/cli/commands/help-impl.ts
+++ b/packages/angular/cli/commands/help-impl.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
diff --git a/packages/angular/cli/commands/lint-impl.ts b/packages/angular/cli/commands/lint-impl.ts
index 6edd8556f994..acf415fcdc2a 100644
--- a/packages/angular/cli/commands/lint-impl.ts
+++ b/packages/angular/cli/commands/lint-impl.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
diff --git a/packages/angular/cli/commands/lint-long.md b/packages/angular/cli/commands/lint-long.md
index 03917ffb252e..480b069f1e2c 100644
--- a/packages/angular/cli/commands/lint-long.md
+++ b/packages/angular/cli/commands/lint-long.md
@@ -1,4 +1,7 @@
Takes the name of the project, as specified in the `projects` section of the `angular.json` workspace configuration file.
When a project name is not supplied, it will execute for all projects.
-The default linting tool is [TSLint](https://palantir.github.io/tslint/), and the default configuration is specified in the project's `tslint.json` file.
\ No newline at end of file
+The default linting tool is [TSLint](https://palantir.github.io/tslint/), and the default configuration is specified in the project's `tslint.json` file.
+
+**Note**: TSLint has been discontinued and support has been deprecated in the Angular CLI. The options shown below are for the deprecated TSLint builder.
+To opt-in using the community driven ESLint builder, see [angular-eslint](https://github.com/angular-eslint/angular-eslint#migrating-an-angular-cli-project-from-codelyzer-and-tslint) README.
diff --git a/packages/angular/cli/commands/new-impl.ts b/packages/angular/cli/commands/new-impl.ts
index b5f0221b3104..8bfd286e1b56 100644
--- a/packages/angular/cli/commands/new-impl.ts
+++ b/packages/angular/cli/commands/new-impl.ts
@@ -1,12 +1,13 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import { Arguments } from '../models/interface';
import { SchematicCommand } from '../models/schematic-command';
+import { ensureCompatibleNpm } from '../utilities/package-manager';
import { Schema as NewCommandSchema } from './new';
diff --git a/packages/angular/cli/commands/new.json b/packages/angular/cli/commands/new.json
index 89cfbda500dd..47dc5861726a 100644
--- a/packages/angular/cli/commands/new.json
+++ b/packages/angular/cli/commands/new.json
@@ -1,7 +1,7 @@
{
"$schema": "http://json-schema.org/schema",
"$id": "ng-cli://commands/new.json",
- "description": "Creates a new workspace and an initial Angular app.",
+ "description": "Creates a new workspace and an initial Angular application.",
"$longDescription": "./new.md",
"$aliases": [ "n" ],
@@ -16,13 +16,13 @@
"collection": {
"type": "string",
"aliases": [ "c" ],
- "description": "A collection of schematics to use in generating the initial app."
+ "description": "A collection of schematics to use in generating the initial application."
},
"verbose": {
"type": "boolean",
"default": false,
"aliases": [ "v" ],
- "description": "When true, adds more details to output logging."
+ "description": "Add more details to output logging."
}
},
"required": []
diff --git a/packages/angular/cli/commands/new.md b/packages/angular/cli/commands/new.md
index 822deb745ee1..5d344b5d4312 100644
--- a/packages/angular/cli/commands/new.md
+++ b/packages/angular/cli/commands/new.md
@@ -1,16 +1,16 @@
-Creates and initializes a new Angular app that is the default project for a new workspace.
+Creates and initializes a new Angular application that is the default project for a new workspace.
Provides interactive prompts for optional configuration, such as adding routing support.
All prompts can safely be allowed to default.
* The new workspace folder is given the specified project name, and contains configuration files at the top level.
-* By default, the files for a new initial app (with the same name as the workspace) are placed in the `src/` subfolder. Corresponding end-to-end tests are placed in the `e2e/` subfolder.
+* By default, the files for a new initial application (with the same name as the workspace) are placed in the `src/` subfolder. Corresponding end-to-end tests are placed in the `e2e/` subfolder.
-* The new app's configuration appears in the `projects` section of the `angular.json` workspace configuration file, under its project name.
+* The new application's configuration appears in the `projects` section of the `angular.json` workspace configuration file, under its project name.
-* Subsequent apps that you generate in the workspace reside in the `projects/` subfolder.
+* Subsequent applications that you generate in the workspace reside in the `projects/` subfolder.
-If you plan to have multiple apps in the workspace, you can create an empty workspace by setting the `--createApplication` option to false.
-You can then use `ng generate application` to create an initial app.
-This allows a workspace name different from the initial app name, and ensures that all apps reside in the `/projects` subfolder, matching the structure of the configuration file.
\ No newline at end of file
+If you plan to have multiple applications in the workspace, you can create an empty workspace by setting the `--createApplication` option to false.
+You can then use `ng generate application` to create an initial application.
+This allows a workspace name different from the initial app name, and ensures that all applications reside in the `/projects` subfolder, matching the structure of the configuration file.
\ No newline at end of file
diff --git a/packages/angular/cli/commands/run-impl.ts b/packages/angular/cli/commands/run-impl.ts
index feefe731fa5b..3a0968e55898 100644
--- a/packages/angular/cli/commands/run-impl.ts
+++ b/packages/angular/cli/commands/run-impl.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
diff --git a/packages/angular/cli/commands/run.json b/packages/angular/cli/commands/run.json
index 4111cc014a67..b6c3d64bb065 100644
--- a/packages/angular/cli/commands/run.json
+++ b/packages/angular/cli/commands/run.json
@@ -22,7 +22,7 @@
}
},
"configuration": {
- "description": "A named builder configuration, defined in the \"configurations\" section of angular.json.\nThe builder uses the named configuration to run the given target.",
+ "description": "One or more named builder configurations as a comma-separated list as specified in the \"configurations\" section of angular.json.\nThe builder uses the named configurations to run the given target.\nFor more information, see https://angular.io/guide/workspace-config#alternate-build-configurations.",
"type": "string",
"aliases": [ "c" ]
}
diff --git a/packages/angular/cli/commands/serve-impl.ts b/packages/angular/cli/commands/serve-impl.ts
index 04b51eee4e4a..fc5cf333b09c 100644
--- a/packages/angular/cli/commands/serve-impl.ts
+++ b/packages/angular/cli/commands/serve-impl.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
diff --git a/packages/angular/cli/commands/test-impl.ts b/packages/angular/cli/commands/test-impl.ts
index 28f09df4d7b2..71d7b9147b36 100644
--- a/packages/angular/cli/commands/test-impl.ts
+++ b/packages/angular/cli/commands/test-impl.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
diff --git a/packages/angular/cli/commands/update-impl.ts b/packages/angular/cli/commands/update-impl.ts
index b67d46aa1ec5..485fe6e05e65 100644
--- a/packages/angular/cli/commands/update-impl.ts
+++ b/packages/angular/cli/commands/update-impl.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
@@ -11,13 +11,15 @@ import { execSync } from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import * as semver from 'semver';
+import { VERSION } from '../lib/cli';
import { PackageManager } from '../lib/config/schema';
import { Command } from '../models/command';
import { Arguments } from '../models/interface';
-import { runTempPackageBin } from '../tasks/install-package';
+import { SchematicEngineHost } from '../models/schematic-engine-host';
import { colors } from '../utilities/color';
+import { runTempPackageBin } from '../utilities/install-package';
import { writeErrorToLogFile } from '../utilities/log-file';
-import { getPackageManager } from '../utilities/package-manager';
+import { ensureCompatibleNpm, getPackageManager } from '../utilities/package-manager';
import {
PackageIdentifier,
PackageManifest,
@@ -41,11 +43,6 @@ const pickManifest = require('npm-pick-manifest') as (
const oldConfigFileNames = ['.angular-cli.json', 'angular-cli.json'];
-const NG_VERSION_9_POST_MSG = colors.cyan(
- '\nYour project has been updated to Angular version 9!\n' +
- 'For more info, please see: https://v9.angular.io/guide/updating-to-version-9',
-);
-
/**
* Disable CLI version mismatch checks and forces usage of the invoked CLI
* instead of invoking the local installed version.
@@ -56,6 +53,8 @@ const disableVersionCheck =
disableVersionCheckEnv !== '0' &&
disableVersionCheckEnv.toLowerCase() !== 'false';
+const ANGULAR_PACKAGES_REGEXP = /^@(?:angular|nguniversal)\//;
+
export class UpdateCommand extends Command {
public readonly allowMissingWorkspace = true;
private workflow!: NodeWorkflow;
@@ -63,16 +62,14 @@ export class UpdateCommand extends Command {
async initialize() {
this.packageManager = await getPackageManager(this.context.root);
- this.workflow = new NodeWorkflow(
- this.context.root,
- {
- packageManager: this.packageManager,
- // __dirname -> favor @schematics/update from this package
- // Otherwise, use packages from the active workspace (migrations)
- resolvePaths: [__dirname, this.context.root],
- schemaValidation: true,
- },
- );
+ this.workflow = new NodeWorkflow(this.context.root, {
+ packageManager: this.packageManager,
+ // __dirname -> favor @schematics/update from this package
+ // Otherwise, use packages from the active workspace (migrations)
+ resolvePaths: [__dirname, this.context.root],
+ schemaValidation: true,
+ engineHostCreator: (options) => new SchematicEngineHost(options.resolvePaths),
+ });
}
private async executeSchematic(
@@ -84,7 +81,7 @@ export class UpdateCommand extends Command {
let logs: string[] = [];
const files = new Set();
- const reporterSubscription = this.workflow.reporter.subscribe(event => {
+ const reporterSubscription = this.workflow.reporter.subscribe((event) => {
// Strip leading slash to prevent confusion.
const eventPath = event.path.startsWith('/') ? event.path.substr(1) : event.path;
@@ -114,11 +111,11 @@ export class UpdateCommand extends Command {
}
});
- const lifecycleSubscription = this.workflow.lifeCycle.subscribe(event => {
+ const lifecycleSubscription = this.workflow.lifeCycle.subscribe((event) => {
if (event.kind == 'end' || event.kind == 'post-tasks-start') {
if (!error) {
// Output the logging queue, no error happened.
- logs.forEach(log => this.logger.info(` ${log}`));
+ logs.forEach((log) => this.logger.info(` ${log}`));
logs = [];
}
}
@@ -141,12 +138,14 @@ export class UpdateCommand extends Command {
return { success: !error, files };
} catch (e) {
if (e instanceof UnsuccessfulWorkflowExecution) {
- this.logger.error(`${colors.symbols.cross} Migration failed. See above for further details.\n`);
+ this.logger.error(
+ `${colors.symbols.cross} Migration failed. See above for further details.\n`,
+ );
} else {
const logPath = writeErrorToLogFile(e);
this.logger.fatal(
`${colors.symbols.cross} Migration failed: ${e.message}\n` +
- ` See "${logPath}" for further details.\n`,
+ ` See "${logPath}" for further details.\n`,
);
}
@@ -164,7 +163,7 @@ export class UpdateCommand extends Command {
commit?: boolean,
): Promise {
const collection = this.workflow.engine.createCollection(collectionPath);
- const name = collection.listSchematicNames().find(name => name === migrationName);
+ const name = collection.listSchematicNames().find((name) => name === migrationName);
if (!name) {
this.logger.error(`Cannot find migration '${migrationName}' in '${packageName}'.`);
@@ -213,15 +212,13 @@ export class UpdateCommand extends Command {
return true;
}
- this.logger.info(
- colors.cyan(`** Executing migrations of package '${packageName}' **\n`),
- );
+ this.logger.info(colors.cyan(`** Executing migrations of package '${packageName}' **\n`));
return this.executePackageMigrations(migrations, packageName, commit);
}
private async executePackageMigrations(
- migrations: Iterable<{ name: string; description: string; collection: { name: string }}>,
+ migrations: Iterable<{ name: string; description: string; collection: { name: string } }>,
packageName: string,
commit = false,
): Promise {
@@ -229,8 +226,9 @@ export class UpdateCommand extends Command {
const [title, ...description] = migration.description.split('. ');
this.logger.info(
- colors.cyan(colors.symbols.pointer) + ' ' +
- colors.bold(title.endsWith('.') ? title : title + '.'),
+ colors.cyan(colors.symbols.pointer) +
+ ' ' +
+ colors.bold(title.endsWith('.') ? title : title + '.'),
);
if (description.length) {
@@ -265,47 +263,42 @@ export class UpdateCommand extends Command {
// tslint:disable-next-line:no-big-function
async run(options: UpdateCommandSchema & Arguments) {
- // Check if the @angular-devkit/schematics package can be resolved from the workspace root
- // This works around issues with packages containing migrations that cannot directly depend on the package
- // This check can be removed once the schematic runtime handles this situation
- try {
- require.resolve('@angular-devkit/schematics', { paths: [this.context.root] });
- } catch (e) {
- if (e.code === 'MODULE_NOT_FOUND') {
- this.logger.fatal(
- 'The "@angular-devkit/schematics" package cannot be resolved from the workspace root directory. ' +
- 'This may be due to an unsupported node modules structure.\n' +
- 'Please remove both the "node_modules" directory and the package lock file; and then reinstall.\n' +
- 'If this does not correct the problem, ' +
- 'please temporarily install the "@angular-devkit/schematics" package within the workspace. ' +
- 'It can be removed once the update is complete.',
+ await ensureCompatibleNpm(this.context.root);
+
+ // Check if the current installed CLI version is older than the latest compatible version.
+ if (!disableVersionCheck) {
+ const cliVersionToInstall = await this.checkCLIVersion(
+ options['--'],
+ options.verbose,
+ options.next,
+ );
+
+ if (cliVersionToInstall) {
+ this.logger.warn(
+ 'The installed Angular CLI version is outdated.\n' +
+ `Installing a temporary Angular CLI versioned ${cliVersionToInstall} to perform the update.`,
);
- return 1;
+ return runTempPackageBin(
+ `@angular/cli@${cliVersionToInstall}`,
+ this.logger,
+ this.packageManager,
+ process.argv.slice(2),
+ );
}
-
- throw e;
}
- // Check if the current installed CLI version is older than the latest version.
- if (!disableVersionCheck && await this.checkCLILatestVersion(options.verbose, options.next)) {
- this.logger.warn(
- `The installed local Angular CLI version is older than the latest ${options.next ? 'pre-release' : 'stable'} version.\n` +
- 'Installing a temporary version to perform the update.',
- );
-
- return runTempPackageBin(
- `@angular/cli@${options.next ? 'next' : 'latest'}`,
- this.logger,
- this.packageManager,
- process.argv.slice(2),
- );
- }
+ const logVerbose = (message: string) => {
+ if (options.verbose) {
+ this.logger.info(message);
+ }
+ };
if (options.all) {
- const updateCmd = this.packageManager === PackageManager.Yarn
- ? `'yarn upgrade-interactive' or 'yarn upgrade'`
- : `'${this.packageManager} update'`;
+ const updateCmd =
+ this.packageManager === PackageManager.Yarn
+ ? `'yarn upgrade-interactive' or 'yarn upgrade'`
+ : `'${this.packageManager} update'`;
this.logger.warn(`
'--all' functionality has been removed as updating multiple packages at once is not recommended.
@@ -328,7 +321,7 @@ export class UpdateCommand extends Command {
return 1;
}
- if (packages.some(v => v.name === packageIdentifier.name)) {
+ if (packages.some((v) => v.name === packageIdentifier.name)) {
this.logger.error(`Duplicate package '${packageIdentifier.name}' specified.`);
return 1;
@@ -409,7 +402,9 @@ export class UpdateCommand extends Command {
if (options.migrateOnly) {
if (!options.from && typeof options.migrateOnly !== 'string') {
- this.logger.error('"from" option is required when using the "migrate-only" option without a migration name.');
+ this.logger.error(
+ '"from" option is required when using the "migrate-only" option without a migration name.',
+ );
return 1;
} else if (packages.length !== 1) {
@@ -439,7 +434,7 @@ export class UpdateCommand extends Command {
const packageJson = findPackageJson(this.context.root, packageName);
if (packageJson) {
packagePath = path.dirname(packageJson);
- packageNode = await readPackageJson(packagePath);
+ packageNode = await readPackageJson(packageJson);
}
}
@@ -472,8 +467,7 @@ export class UpdateCommand extends Command {
if (migrations.startsWith('../')) {
this.logger.error(
- 'Package contains an invalid migrations field. ' +
- 'Paths outside the package root are not permitted.',
+ 'Package contains an invalid migrations field. Paths outside the package root are not permitted.',
);
return 1;
@@ -499,9 +493,9 @@ export class UpdateCommand extends Command {
}
}
- let success = false;
+ let result: boolean;
if (typeof options.migrateOnly == 'string') {
- success = await this.executeMigration(
+ result = await this.executeMigration(
packageName,
migrations,
options.migrateOnly,
@@ -518,8 +512,7 @@ export class UpdateCommand extends Command {
const migrationRange = new semver.Range(
'>' + from + ' <=' + (options.to || packageNode.version),
);
-
- success = await this.executeMigrations(
+ result = await this.executeMigrations(
packageName,
migrations,
migrationRange,
@@ -527,20 +520,7 @@ export class UpdateCommand extends Command {
);
}
- if (success) {
- if (
- packageName === '@angular/core'
- && options.from
- && +options.from.split('.')[0] < 9
- && (options.to || packageNode.version).split('.')[0] === '9'
- ) {
- this.logger.info(NG_VERSION_9_POST_MSG);
- }
-
- return 0;
- }
-
- return 1;
+ return result ? 0 : 1;
}
const requests: {
@@ -635,6 +615,35 @@ export class UpdateCommand extends Command {
continue;
}
+ if (node.package && ANGULAR_PACKAGES_REGEXP.test(node.package.name)) {
+ const { name, version } = node.package;
+ const toBeInstalledMajorVersion = +manifest.version.split('.')[0];
+ const currentMajorVersion = +version.split('.')[0];
+
+ if (toBeInstalledMajorVersion - currentMajorVersion > 1) {
+ // Only allow updating a single version at a time.
+ if (currentMajorVersion < 6) {
+ // Before version 6, the major versions were not always sequential.
+ // Example @angular/core skipped version 3, @angular/cli skipped versions 2-5.
+ this.logger.error(
+ `Updating multiple major versions of '${name}' at once is not supported. Please migrate each major version individually.\n` +
+ `For more information about the update process, see https://update.angular.io/.`,
+ );
+ } else {
+ const nextMajorVersionFromCurrent = currentMajorVersion + 1;
+
+ this.logger.error(
+ `Updating multiple major versions of '${name}' at once is not supported. Please migrate each major version individually.\n` +
+ `Run 'ng update ${name}@${nextMajorVersionFromCurrent}' in your workspace directory ` +
+ `to update to latest '${nextMajorVersionFromCurrent}.x' version of '${name}'.\n\n` +
+ `For more information about the update process, see https://update.angular.io/?v=${currentMajorVersion}.0-${nextMajorVersionFromCurrent}.0`,
+ );
+ }
+
+ return 1;
+ }
+ }
+
packagesToUpdate.push(requestIdentifier.toString());
}
@@ -653,7 +662,8 @@ export class UpdateCommand extends Command {
if (success && options.createCommits) {
const committed = this.commit(
- `Angular CLI update for packages - ${packagesToUpdate.join(', ')}`);
+ `Angular CLI update for packages - ${packagesToUpdate.join(', ')}`,
+ );
if (!committed) {
return 1;
}
@@ -670,11 +680,70 @@ export class UpdateCommand extends Command {
if (success && migrations) {
for (const migration of migrations) {
+ // Resolve the package from the workspace root, as otherwise it will be resolved from the temp
+ // installed CLI version.
+ let packagePath;
+ logVerbose(
+ `Resolving migration package '${migration.package}' from '${this.context.root}'...`,
+ );
+ try {
+ try {
+ packagePath = path.dirname(
+ // This may fail if the `package.json` is not exported as an entry point
+ require.resolve(path.join(migration.package, 'package.json'), {
+ paths: [this.context.root],
+ }),
+ );
+ } catch (e) {
+ if (e.code === 'MODULE_NOT_FOUND') {
+ // Fallback to trying to resolve the package's main entry point
+ packagePath = require.resolve(migration.package, { paths: [this.context.root] });
+ } else {
+ throw e;
+ }
+ }
+ } catch (e) {
+ if (e.code === 'MODULE_NOT_FOUND') {
+ logVerbose(e.toString());
+ this.logger.error(
+ `Migrations for package (${migration.package}) were not found.` +
+ ' The package could not be found in the workspace.',
+ );
+ } else {
+ this.logger.error(
+ `Unable to resolve migrations for package (${migration.package}). [${e.message}]`,
+ );
+ }
+
+ return 1;
+ }
+
+ let migrations;
+
+ // Check if it is a package-local location
+ const localMigrations = path.join(packagePath, migration.collection);
+ if (fs.existsSync(localMigrations)) {
+ migrations = localMigrations;
+ } else {
+ // Try to resolve from package location.
+ // This avoids issues with package hoisting.
+ try {
+ migrations = require.resolve(migration.collection, { paths: [packagePath] });
+ } catch (e) {
+ if (e.code === 'MODULE_NOT_FOUND') {
+ this.logger.error(`Migrations for package (${migration.package}) were not found.`);
+ } else {
+ this.logger.error(
+ `Unable to resolve migrations for package (${migration.package}). [${e.message}]`,
+ );
+ }
+
+ return 1;
+ }
+ }
const result = await this.executeMigrations(
migration.package,
- // Resolve the collection from the workspace root, as otherwise it will be resolved from the temp
- // installed CLI version.
- require.resolve(migration.collection, { paths: [this.context.root] }),
+ migrations,
new semver.Range('>' + migration.from + ' <=' + migration.to),
options.createCommits,
);
@@ -683,10 +752,6 @@ export class UpdateCommand extends Command {
return 0;
}
}
-
- if (migrations.some(m => m.package === '@angular/core' && m.to.split('.')[0] === '9' && +m.from.split('.')[0] < 9)) {
- this.logger.info(NG_VERSION_9_POST_MSG);
- }
}
return success ? 0 : 1;
@@ -716,8 +781,7 @@ export class UpdateCommand extends Command {
try {
createCommit(message);
} catch (err) {
- this.logger.error(
- `Failed to commit update (${message}):\n${err.stderr}`);
+ this.logger.error(`Failed to commit update (${message}):\n${err.stderr}`);
return false;
}
@@ -726,8 +790,7 @@ export class UpdateCommand extends Command {
const hash = findCurrentGitSha();
const shortMessage = message.split('\n')[0];
if (hash) {
- this.logger.info(` Committed migration step (${getShortHash(hash)}): ${
- shortMessage}.`);
+ this.logger.info(` Committed migration step (${getShortHash(hash)}): ${shortMessage}.`);
} else {
// Commit was successful, but reading the hash was not. Something weird happened,
// but nothing that would stop the update. Just log the weirdness and continue.
@@ -740,7 +803,10 @@ export class UpdateCommand extends Command {
private checkCleanGit(): boolean {
try {
- const topLevel = execSync('git rev-parse --show-toplevel', { encoding: 'utf8', stdio: 'pipe' });
+ const topLevel = execSync('git rev-parse --show-toplevel', {
+ encoding: 'utf8',
+ stdio: 'pipe',
+ });
const result = execSync('git status --porcelain', { encoding: 'utf8', stdio: 'pipe' });
if (result.trim().length === 0) {
return true;
@@ -763,14 +829,16 @@ export class UpdateCommand extends Command {
}
/**
- * Checks if the current installed CLI version is older than the latest version.
- * @returns `true` when the installed version is older.
- */
- private async checkCLILatestVersion(verbose = false, next = false): Promise {
- const { version: installedCLIVersion } = require('../package.json');
-
- const LatestCLIManifest = await fetchPackageManifest(
- `@angular/cli@${next ? 'next' : 'latest'}`,
+ * Checks if the current installed CLI version is older or newer than a compatible version.
+ * @returns the version to install or null when there is no update to install.
+ */
+ private async checkCLIVersion(
+ packagesToUpdate: string[] | undefined,
+ verbose = false,
+ next = false,
+ ): Promise {
+ const { version } = await fetchPackageManifest(
+ `@angular/cli@${this.getCLIUpdateRunnerVersion(packagesToUpdate, next)}`,
this.logger,
{
verbose,
@@ -778,7 +846,38 @@ export class UpdateCommand extends Command {
},
);
- return semver.lt(installedCLIVersion, LatestCLIManifest.version);
+ return VERSION.full === version ? null : version;
+ }
+
+ private getCLIUpdateRunnerVersion(
+ packagesToUpdate: string[] | undefined,
+ next: boolean,
+ ): string | number {
+ if (next) {
+ return 'next';
+ }
+
+ const updatingAngularPackage = packagesToUpdate?.find((r) => ANGULAR_PACKAGES_REGEXP.test(r));
+ if (updatingAngularPackage) {
+ // If we are updating any Angular package we can update the CLI to the target version because
+ // migrations for @angular/core@13 can be executed using Angular/cli@13.
+ // This is same behaviour as `npx @angular/cli@13 update @angular/core@13`.
+
+ // `@angular/cli@13` -> ['', 'angular/cli', '13']
+ // `@angular/cli` -> ['', 'angular/cli']
+ const tempVersion = coerceVersionNumber(updatingAngularPackage.split('@')[2]);
+
+ return semver.parse(tempVersion)?.major ?? 'latest';
+ }
+
+ // When not updating an Angular package we cannot determine which schematic runtime the migration should to be executed in.
+ // Typically, we can assume that the `@angular/cli` was updated previously.
+ // Example: Angular official packages are typically updated prior to NGRX etc...
+ // Therefore, we only update to the latest patch version of the installed major version of the Angular CLI.
+
+ // This is important because we might end up in a scenario where locally Angular v12 is installed, updating NGRX from 11 to 12.
+ // We end up using Angular ClI v13 to run the migrations if we run the migrations using the CLI installed major version + 1 logic.
+ return VERSION.major;
}
}
@@ -811,7 +910,7 @@ function createCommit(message: string) {
*/
function findCurrentGitSha(): string | null {
try {
- const hash = execSync('git rev-parse HEAD', {encoding: 'utf8', stdio: 'pipe'});
+ const hash = execSync('git rev-parse HEAD', { encoding: 'utf8', stdio: 'pipe' });
return hash.trim();
} catch {
diff --git a/packages/angular/cli/commands/update-long.md b/packages/angular/cli/commands/update-long.md
index 93be9c0f3c78..72df66ce35da 100644
--- a/packages/angular/cli/commands/update-long.md
+++ b/packages/angular/cli/commands/update-long.md
@@ -4,14 +4,19 @@ Perform a basic update to the current stable release of the core framework and C
ng update @angular/cli @angular/core
```
-To update to the next beta or pre-release version, use the `--next=true` option.
+To update to the next beta or pre-release version, use the `--next` option.
To update from one major version to another, use the format
-`ng update @angular/cli@^ @angular/core@^`.
+
+```
+ng update @angular/cli@^ @angular/core@^
+```
We recommend that you always update to the latest patch version, as it contains fixes we released since the initial major release.
-For example, use the following command to take the latest 9.x.x version and use that to update.
+For example, use the following command to take the latest 10.x.x version and use that to update.
-`ng update @angular/cli@^9 @angular/core@^9`
+```
+ng update @angular/cli@^10 @angular/core@^10
+```
For detailed information and guidance on updating your application, see the interactive [Angular Update Guide](https://update.angular.io/).
diff --git a/packages/angular/cli/commands/update.json b/packages/angular/cli/commands/update.json
index 0d1205c86078..b87e75a35fbb 100644
--- a/packages/angular/cli/commands/update.json
+++ b/packages/angular/cli/commands/update.json
@@ -44,7 +44,7 @@
"type": "boolean"
},
"migrateOnly": {
- "description": "Only perform a migration, does not update the installed version.",
+ "description": "Only perform a migration, do not update the installed version.",
"oneOf": [
{
"type": "boolean"
diff --git a/packages/angular/cli/commands/version-impl.ts b/packages/angular/cli/commands/version-impl.ts
index 669ce2d2ba54..70a8fc6faa27 100644
--- a/packages/angular/cli/commands/version-impl.ts
+++ b/packages/angular/cli/commands/version-impl.ts
@@ -1,15 +1,14 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
-import { JsonParseMode, isJsonObject, parseJson } from '@angular-devkit/core';
-import * as fs from 'fs';
import * as path from 'path';
import { Command } from '../models/command';
import { colors } from '../utilities/color';
+import { JSONFile } from '../utilities/json-file';
import { Schema as VersionCommandSchema } from './version';
interface PartialPackageInfo {
@@ -169,15 +168,9 @@ export class VersionCommand extends Command {
private getIvyWorkspace(): string {
try {
- const content = fs.readFileSync(path.resolve(this.context.root, 'tsconfig.json'), 'utf-8');
- const tsConfig = parseJson(content, JsonParseMode.Loose);
- if (!isJsonObject(tsConfig)) {
- return '';
- }
-
- const { angularCompilerOptions } = tsConfig;
+ const json = new JSONFile(path.resolve(this.context.root, 'tsconfig.json'));
- return isJsonObject(angularCompilerOptions) && angularCompilerOptions.enableIvy === false
+ return json.get(['angularCompilerOptions', 'enableIvy']) === false
? 'No'
: 'Yes';
} catch {
diff --git a/packages/angular/cli/lib/cli/index.ts b/packages/angular/cli/lib/cli/index.ts
index d7e3d913260a..d9878d04fa21 100644
--- a/packages/angular/cli/lib/cli/index.ts
+++ b/packages/angular/cli/lib/cli/index.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
diff --git a/packages/angular/cli/lib/config/schema.json b/packages/angular/cli/lib/config/schema.json
index c8e163d12103..72ed61fb4ba3 100644
--- a/packages/angular/cli/lib/config/schema.json
+++ b/packages/angular/cli/lib/config/schema.json
@@ -64,6 +64,20 @@
"analytics": {
"type": ["boolean", "string"],
"description": "Share anonymous usage data with the Angular Team at Google."
+ },
+ "analyticsSharing": {
+ "type": "object",
+ "properties": {
+ "tracking": {
+ "description": "Analytics sharing info tracking ID.",
+ "type": "string",
+ "pattern": "^GA-\\d+-\\d+$"
+ },
+ "uuid": {
+ "description": "Analytics sharing info universally unique identifier.",
+ "type": "string"
+ }
+ }
}
},
"additionalProperties": false
@@ -155,7 +169,7 @@
},
"skipTests": {
"type": "boolean",
- "description": "When true, does not create test files.",
+ "description": "Do not create test files.",
"default": false
}
}
@@ -197,7 +211,7 @@
},
"skipTests": {
"type": "boolean",
- "description": "When true, does not create test files.",
+ "description": "Do not create test files.",
"default": false
}
}
@@ -244,7 +258,7 @@
},
"skipTests": {
"type": "boolean",
- "description": "When true, does not create test files.",
+ "description": "Do not create test files.",
"default": false
}
}
@@ -259,7 +273,7 @@
},
"skipTests": {
"type": "boolean",
- "description": "When true, does not create test files.",
+ "description": "Do not create test files.",
"default": false
},
"skipImport": {
@@ -285,7 +299,7 @@
"properties": {
"skipTests": {
"type": "boolean",
- "description": "When true, does not create test files.",
+ "description": "Do not create test files.",
"default": false
}
}
@@ -478,7 +492,8 @@
"@angular-devkit/build-angular:karma",
"@angular-devkit/build-angular:protractor",
"@angular-devkit/build-angular:server",
- "@angular-devkit/build-angular:tslint"
+ "@angular-devkit/build-angular:tslint",
+ "@angular-devkit/build-angular:ng-packagr"
]
}
},
@@ -584,6 +599,17 @@
"additionalProperties": { "$ref": "#/definitions/targetOptions/definitions/tslint" }
}
}
+ },
+ {
+ "type": "object",
+ "properties": {
+ "builder": { "const": "@angular-devkit/build-angular:ng-packagr" },
+ "options": { "$ref": "#/definitions/targetOptions/definitions/ngPackagr" },
+ "configurations": {
+ "type": "object",
+ "additionalProperties": { "$ref": "#/definitions/targetOptions/definitions/ngPackagr" }
+ }
+ }
}
]
}
@@ -619,11 +645,11 @@
"properties": {
"browserTarget": {
"type": "string",
- "description": "Target to build."
+ "description": "A browser builder target to use for rendering the app shell in the format of `project:target[:configuration]`. You can also pass in more than one configuration name as a comma-separated list. Example: `project:target:production,staging`."
},
"serverTarget": {
"type": "string",
- "description": "Server target to use for rendering the app shell."
+ "description": "A server builder target to use for rendering the app shell in the format of `project:target[:configuration]`. You can also pass in more than one configuration name as a comma-separated list. Example: `project:target:production,staging`."
},
"appModuleBundle": {
"type": "string",
@@ -701,7 +727,7 @@
"additionalProperties": false
},
"optimization": {
- "description": "Enables optimization of the build output.",
+ "description": "Enables optimization of the build output. Including minification of scripts and styles, tree-shaking, dead-code elimination, inlining of critical CSS and fonts inlining. For more information, see https://angular.io/guide/workspace-config#optimization-configuration.",
"oneOf": [
{
"type": "object",
@@ -712,12 +738,32 @@
"default": true
},
"styles": {
- "type": "boolean",
"description": "Enables optimization of the styles output.",
- "default": true
+ "default": true,
+ "oneOf": [
+ {
+ "type": "object",
+ "properties": {
+ "minify": {
+ "type": "boolean",
+ "description": "Minify CSS definitions by removing extraneous whitespace and comments, merging identifiers and minimizing values.",
+ "default": true
+ },
+ "inlineCritical": {
+ "type": "boolean",
+ "description": "Extract and inline critical CSS definitions to improve first paint time.",
+ "default": false
+ }
+ },
+ "additionalProperties": false
+ },
+ {
+ "type": "boolean"
+ }
+ ]
},
"fonts": {
- "description": "Enables optimization for fonts. This requires internet access.",
+ "description": "Enables optimization for fonts. This option requires internet access. `HTTPS_PROXY` environment variable can be used to specify a proxy server.",
"default": true,
"oneOf": [
{
@@ -725,7 +771,7 @@
"properties": {
"inline": {
"type": "boolean",
- "description": "Reduce render blocking requests by inlining external fonts in the application's HTML index file. This requires internet access.",
+ "description": "Reduce render blocking requests by inlining external Google fonts and icons CSS definitions in the application's HTML index file. This option requires internet access. `HTTPS_PROXY` environment variable can be used to specify a proxy server.",
"default": true
}
},
@@ -766,7 +812,7 @@
"default": false
},
"sourceMap": {
- "description": "Output sourcemaps.",
+ "description": "Output source maps for scripts and styles. For more information, see https://angular.io/guide/workspace-config#source-map-configuration.",
"default": true,
"oneOf": [
{
@@ -774,22 +820,22 @@
"properties": {
"scripts": {
"type": "boolean",
- "description": "Output sourcemaps for all scripts.",
+ "description": "Output source maps for all scripts.",
"default": true
},
"styles": {
"type": "boolean",
- "description": "Output sourcemaps for all styles.",
+ "description": "Output source maps for all styles.",
"default": true
},
"hidden": {
"type": "boolean",
- "description": "Output sourcemaps used for error reporting tools.",
+ "description": "Output source maps used for error reporting tools.",
"default": false
},
"vendor": {
"type": "boolean",
- "description": "Resolve vendor packages sourcemaps.",
+ "description": "Resolve vendor packages source maps.",
"default": false
}
},
@@ -802,12 +848,12 @@
},
"vendorChunk": {
"type": "boolean",
- "description": "Use a separate bundle containing only vendor libraries.",
+ "description": "Generate a seperate bundle containing only vendor libraries. This option should only used for development.",
"default": true
},
"commonChunk": {
"type": "boolean",
- "description": "Use a separate bundle containing code used across multiple bundles.",
+ "description": "Generate a seperate bundle containing code used across multiple bundles.",
"default": true
},
"baseHref": {
@@ -955,7 +1001,7 @@
"default": true
},
"lazyModules": {
- "description": "List of additional NgModule files that will be lazy loaded. Lazy router modules with be discovered automatically.",
+ "description": "List of additional NgModule files that will be lazy loaded. Lazy router modules will be discovered automatically.",
"type": "array",
"items": {
"type": "string"
@@ -970,12 +1016,6 @@
},
"default": []
},
- "rebaseRootRelativeCssUrls": {
- "description": "Change root relative URLs in stylesheets to include base HREF and deploy URL. Use only for compatibility and transition. The behavior of this option is non-standard and will be removed in the next major release.",
- "type": "boolean",
- "default": false,
- "x-deprecated": true
- },
"webWorkerTsConfig": {
"type": "string",
"description": "TypeScript configuration for Web Worker modules."
@@ -1010,6 +1050,11 @@
{
"type": "object",
"properties": {
+ "followSymlinks": {
+ "type": "boolean",
+ "default": false,
+ "description": "Allow glob patterns to follow symlink directories. This allows subdirectories of the symlink to be searched."
+ },
"glob": {
"type": "string",
"description": "The pattern to match."
@@ -1049,10 +1094,12 @@
"type": "object",
"properties": {
"src": {
- "type": "string"
+ "type": "string",
+ "pattern": "\\.(([cm]?j|t)sx?|json)$"
},
"replaceWith": {
- "type": "string"
+ "type": "string",
+ "pattern": "\\.(([cm]?j|t)sx?|json)$"
}
},
"additionalProperties": false,
@@ -1065,10 +1112,12 @@
"type": "object",
"properties": {
"replace": {
- "type": "string"
+ "type": "string",
+ "pattern": "\\.(([cm]?j|t)sx?|json)$"
},
"with": {
- "type": "string"
+ "type": "string",
+ "pattern": "\\.(([cm]?j|t)sx?|json)$"
}
},
"additionalProperties": false,
@@ -1171,7 +1220,7 @@
"properties": {
"browserTarget": {
"type": "string",
- "description": "Target to serve."
+ "description": "A browser builder target to serve in the format of `project:target[:configuration]`. You can also pass in more than one configuration name as a comma-separated list. Example: `project:target:production,staging`."
},
"port": {
"type": "number",
@@ -1212,13 +1261,13 @@
},
"open": {
"type": "boolean",
- "description": "When true, open the live-reload URL in default browser.",
+ "description": "Open the live-reload URL in default browser.",
"default": false,
"alias": "o"
},
"liveReload": {
"type": "boolean",
- "description": "When true, reload the page on change using live-reload.",
+ "description": "Reload the page on change using live-reload.",
"default": true
},
"publicHost": {
@@ -1239,31 +1288,31 @@
},
"disableHostCheck": {
"type": "boolean",
- "description": "When true, don't verify that connected clients are part of allowed hosts.",
+ "description": "Do not verify that connected clients are part of allowed hosts.",
"default": false
},
"hmr": {
"type": "boolean",
- "description": "When true, enable hot module replacement.",
+ "description": "Enable hot module replacement.",
"default": false
},
"watch": {
"type": "boolean",
- "description": "When true, rebuild on change.",
+ "description": "Rebuild on change.",
"default": true
},
"hmrWarning": {
"type": "boolean",
- "description": "When true, show a warning when the --hmr option is enabled.",
+ "description": "Show a warning when the --hmr option is enabled.",
"default": true
},
"servePathDefaultWarning": {
"type": "boolean",
- "description": "When true, show a warning when deploy-url/base-href use unsupported serve path values.",
+ "description": "Show a warning when deploy-url/base-href use unsupported serve path values.",
"default": true
},
"optimization": {
- "description": "Enable optimization of the build output.",
+ "description": "Enables optimization of the build output. Including minification of scripts and styles, tree-shaking, dead-code elimination and fonts inlining. For more information, see https://angular.io/guide/workspace-config#optimization-configuration.",
"default": false,
"oneOf": [
{
@@ -1271,12 +1320,12 @@
"properties": {
"scripts": {
"type": "boolean",
- "description": "When true, enable optimization of the scripts output.",
+ "description": "Enable optimization of the scripts output.",
"default": true
},
"styles": {
"type": "boolean",
- "description": "When true, enable optimization of the styles output.",
+ "description": "Enable optimization of the styles output.",
"default": true
}
},
@@ -1292,7 +1341,7 @@
"description": "Build using ahead-of-time compilation."
},
"sourceMap": {
- "description": "When true, output sourcemaps.",
+ "description": "Output source maps for scripts and styles. For more information, see https://angular.io/guide/workspace-config#source-map-configuration.",
"default": true,
"oneOf": [
{
@@ -1300,17 +1349,17 @@
"properties": {
"scripts": {
"type": "boolean",
- "description": "When true, output sourcemaps for all scripts.",
+ "description": "Output source maps for all scripts.",
"default": true
},
"styles": {
"type": "boolean",
- "description": "When true, output sourcemaps for all styles.",
+ "description": "Output source maps for all styles.",
"default": true
},
"vendor": {
"type": "boolean",
- "description": "When true, resolve vendor packages sourcemaps.",
+ "description": "Resolve vendor packages source maps.",
"default": false
}
},
@@ -1323,11 +1372,11 @@
},
"vendorChunk": {
"type": "boolean",
- "description": "When true, use a separate bundle containing only vendor libraries."
+ "description": "Generate a seperate bundle containing only vendor libraries. This option should only used for development."
},
"commonChunk": {
"type": "boolean",
- "description": "When true, use a separate bundle containing code used across multiple bundles."
+ "description": "Generate a seperate bundle containing code used across multiple bundles."
},
"baseHref": {
"type": "string",
@@ -1339,11 +1388,11 @@
},
"verbose": {
"type": "boolean",
- "description": "When true, add more details to output logging."
+ "description": "Add more details to output logging."
},
"progress": {
"type": "boolean",
- "description": "When true, log progress to the console while building."
+ "description": "Log progress to the console while building."
}
},
"additionalProperties": false
@@ -1354,7 +1403,7 @@
"properties": {
"browserTarget": {
"type": "string",
- "description": "Target to extract from."
+ "description": "A browser builder target to extract i18n messages in the format of `project:target[:configuration]`. You can also pass in more than one configuration name as a comma-separated list. Example: `project:target:production,staging`."
},
"format": {
"type": "string",
@@ -1366,7 +1415,9 @@
"xlif",
"xliff",
"xlf2",
- "xliff2"
+ "xliff2",
+ "json",
+ "arb"
]
},
"i18nFormat": {
@@ -1380,7 +1431,9 @@
"xlif",
"xliff",
"xlf2",
- "xliff2"
+ "xliff2",
+ "json",
+ "arb"
]
},
"i18nLocale": {
@@ -1468,7 +1521,7 @@
"additionalProperties": false
},
"sourceMap": {
- "description": "Output sourcemaps.",
+ "description": "Output source maps for scripts and styles. For more information, see https://angular.io/guide/workspace-config#source-map-configuration.",
"default": true,
"oneOf": [
{
@@ -1476,17 +1529,17 @@
"properties": {
"scripts": {
"type": "boolean",
- "description": "Output sourcemaps for all scripts.",
+ "description": "Output source maps for all scripts.",
"default": true
},
"styles": {
"type": "boolean",
- "description": "Output sourcemaps for all styles.",
+ "description": "Output source maps for all styles.",
"default": true
},
"vendor": {
"type": "boolean",
- "description": "Resolve vendor packages sourcemaps.",
+ "description": "Resolve vendor packages source maps.",
"default": false
}
},
@@ -1592,6 +1645,11 @@
{
"type": "object",
"properties": {
+ "followSymlinks": {
+ "type": "boolean",
+ "default": false,
+ "description": "Allow glob patterns to follow symlink directories. This allows subdirectories of the symlink to be searched."
+ },
"glob": {
"type": "string",
"description": "The pattern to match."
@@ -1667,7 +1725,7 @@
},
"devServerTarget": {
"type": "string",
- "description": "Dev server target to run tests against."
+ "description": "A dev-server builder target to run tests against in the format of `project:target[:configuration]`. You can also pass in more than one configuration name as a comma-separated list. Example: `project:target:production,staging`."
},
"grep": {
"type": "string",
@@ -1745,7 +1803,7 @@
"additionalProperties": false
},
"optimization": {
- "description": "Enables optimization of the build output.",
+ "description": "Enables optimization of the build output. Including minification of scripts and styles, tree-shaking and dead-code elimination. For more information, see https://angular.io/guide/workspace-config#optimization-configuration.",
"default": false,
"oneOf": [
{
@@ -1786,7 +1844,7 @@
"description": "The path where style resources will be placed, relative to outputPath."
},
"sourceMap": {
- "description": "Output sourcemaps.",
+ "description": "Output source maps for scripts and styles. For more information, see https://angular.io/guide/workspace-config#source-map-configuration.",
"default": true,
"oneOf": [
{
@@ -1794,22 +1852,22 @@
"properties": {
"scripts": {
"type": "boolean",
- "description": "Output sourcemaps for all scripts.",
+ "description": "Output source maps for all scripts.",
"default": true
},
"styles": {
"type": "boolean",
- "description": "Output sourcemaps for all styles.",
+ "description": "Output source maps for all styles.",
"default": true
},
"hidden": {
"type": "boolean",
- "description": "Output sourcemaps used for error reporting tools.",
+ "description": "Output source maps used for error reporting tools.",
"default": false
},
"vendor": {
"type": "boolean",
- "description": "Resolve vendor packages sourcemaps.",
+ "description": "Resolve vendor packages source maps.",
"default": false
}
},
@@ -1937,10 +1995,12 @@
"type": "object",
"properties": {
"src": {
- "type": "string"
+ "type": "string",
+ "pattern": "\\.(([cm]?j|t)sx?|json)$"
},
"replaceWith": {
- "type": "string"
+ "type": "string",
+ "pattern": "\\.(([cm]?j|t)sx?|json)$"
}
},
"additionalProperties": false,
@@ -1953,10 +2013,12 @@
"type": "object",
"properties": {
"replace": {
- "type": "string"
+ "type": "string",
+ "pattern": "\\.(([cm]?j|t)sx?|json)$"
},
"with": {
- "type": "string"
+ "type": "string",
+ "pattern": "\\.(([cm]?j|t)sx?|json)$"
}
},
"additionalProperties": false,
@@ -2051,6 +2113,26 @@
}
},
"additionalProperties": false
+ },
+ "ngPackagr": {
+ "description": "ng-packagr target options for Build Architect. Use to build library projects.",
+ "type": "object",
+ "properties": {
+ "project": {
+ "type": "string",
+ "description": "The file path for the ng-packagr configuration file, relative to the current workspace."
+ },
+ "tsConfig": {
+ "type": "string",
+ "description": "The full path for the TypeScript configuration file, relative to the current workspace."
+ },
+ "watch": {
+ "type": "boolean",
+ "description": "Run build when files change.",
+ "default": false
+ }
+ },
+ "additionalProperties": false
}
}
},
@@ -2062,6 +2144,7 @@
"default": "warning"
},
"localize": {
+ "description": "Translate the bundles in one or more locales.",
"oneOf": [
{
"type": "boolean",
diff --git a/packages/angular/cli/lib/init.ts b/packages/angular/cli/lib/init.ts
index 2740fa69bf43..7a67dabea1c8 100644
--- a/packages/angular/cli/lib/init.ts
+++ b/packages/angular/cli/lib/init.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
@@ -81,33 +81,41 @@ if (process.env['NG_CLI_PROFILING']) {
const projectLocalCli = require.resolve('@angular/cli', { paths: [process.cwd()] });
cli = await import(projectLocalCli);
- // This was run from a global, check local version.
- if (await isWarningEnabled('versionMismatch')) {
- const globalVersion = new SemVer(require('../package.json').version);
-
- // Older versions might not have the VERSION export
- let localVersion = cli.VERSION?.full;
- if (!localVersion) {
- try {
- localVersion = require(path.join(path.dirname(projectLocalCli), '../../package.json'))
- .version;
- } catch (error) {
- // tslint:disable-next-line no-console
- console.error(
- 'Version mismatch check skipped. Unable to retrieve local version: ' + error,
- );
- }
- }
+ const globalVersion = new SemVer(require('../package.json').version);
- let shouldWarn = false;
+ // Older versions might not have the VERSION export
+ let localVersion = cli.VERSION?.full;
+ if (!localVersion) {
try {
- shouldWarn = !!localVersion && globalVersion.compare(localVersion) > 0;
+ localVersion = require(path.join(path.dirname(projectLocalCli), '../../package.json'))
+ .version;
} catch (error) {
// tslint:disable-next-line no-console
- console.error('Version mismatch check skipped. Unable to compare local version: ' + error);
+ console.error(
+ 'Version mismatch check skipped. Unable to retrieve local version: ' + error,
+ );
}
+ }
+
+ let isGlobalGreater = false;
+ try {
+ isGlobalGreater = !!localVersion && globalVersion.compare(localVersion) > 0;
+ } catch (error) {
+ // tslint:disable-next-line no-console
+ console.error('Version mismatch check skipped. Unable to compare local version: ' + error);
+ }
- if (shouldWarn) {
+ if (isGlobalGreater) {
+ // If using the update command and the global version is greater, use the newer update command
+ // This allows improvements in update to be used in older versions that do not have bootstrapping
+ if (
+ process.argv[2] === 'update' &&
+ cli.VERSION &&
+ cli.VERSION.major - globalVersion.major <= 1
+ ) {
+ cli = await import('./cli');
+ } else if (await isWarningEnabled('versionMismatch')) {
+ // Otherwise, use local version and warn if global is newer than local
const warning =
`Your global Angular CLI version (${globalVersion}) is greater than your local ` +
`version (${localVersion}). The local Angular CLI version is used.\n\n` +
diff --git a/packages/angular/cli/models/analytics.ts b/packages/angular/cli/models/analytics.ts
index 1118c2311b8d..76474dfc60a6 100644
--- a/packages/angular/cli/models/analytics.ts
+++ b/packages/angular/cli/models/analytics.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
@@ -8,7 +8,6 @@
import { analytics, json, tags } from '@angular-devkit/core';
import * as child_process from 'child_process';
import * as debug from 'debug';
-import { writeFileSync } from 'fs';
import * as inquirer from 'inquirer';
import * as os from 'os';
import * as ua from 'universal-analytics';
@@ -357,21 +356,21 @@ export function setAnalyticsConfig(level: 'global' | 'local', value: string | bo
throw new Error(`Could not find ${level} workspace.`);
}
- const configValue = config.value;
- const cli: json.JsonValue = configValue['cli'] || (configValue['cli'] = {});
+ const cli = config.get(['cli']);
- if (!json.isJsonObject(cli)) {
+ if (cli !== undefined && !json.isJsonObject(cli as json.JsonValue)) {
throw new Error(`Invalid config found at ${configPath}. CLI should be an object.`);
}
if (value === true) {
value = uuidV4();
}
- cli['analytics'] = value;
- const output = JSON.stringify(configValue, null, 2);
- writeFileSync(configPath, output);
+ config.modify(['cli', 'analytics'], value);
+ config.save();
+
analyticsDebug('done');
+
}
/**
@@ -389,7 +388,7 @@ export async function promptGlobalAnalytics(force = false) {
message: tags.stripIndents`
Would you like to share anonymous usage data with the Angular Team at Google under
Google’s Privacy Policy at https://policies.google.com/privacy? For more details and
- how to change this setting, see http://angular.io/analytics.
+ how to change this setting, see https://angular.io/analytics.
`,
default: false,
},
@@ -447,7 +446,7 @@ export async function promptProjectAnalytics(force = false): Promise {
message: tags.stripIndents`
Would you like to share anonymous usage data about this project with the Angular Team at
Google under Google’s Privacy Policy at https://policies.google.com/privacy? For more
- details and how to change this setting, see http://angular.io/analytics.
+ details and how to change this setting, see https://angular.io/analytics.
`,
default: false,
diff --git a/packages/angular/cli/models/architect-command.ts b/packages/angular/cli/models/architect-command.ts
index c9a9573dd711..57f8a9a88dd9 100644
--- a/packages/angular/cli/models/architect-command.ts
+++ b/packages/angular/cli/models/architect-command.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
diff --git a/packages/angular/cli/models/command-runner.ts b/packages/angular/cli/models/command-runner.ts
index b8ec88e57883..3fecd24d7639 100644
--- a/packages/angular/cli/models/command-runner.ts
+++ b/packages/angular/cli/models/command-runner.ts
@@ -1,12 +1,11 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {
- JsonParseMode,
analytics,
isJsonObject,
json,
@@ -18,6 +17,7 @@ import {
import { readFileSync } from 'fs';
import { join, resolve } from 'path';
import { AngularWorkspace } from '../utilities/config';
+import { readAndParseJson } from '../utilities/json-file';
import { parseJsonSchemaToCommandDescription } from '../utilities/json-schema';
import {
getGlobalAnalytics,
@@ -39,6 +39,7 @@ const standardCommands = {
'config': '../commands/config.json',
'doc': '../commands/doc.json',
'e2e': '../commands/e2e.json',
+ 'extract-i18n': '../commands/extract-i18n.json',
'make-this-awesome': '../commands/easter-egg.json',
'generate': '../commands/generate.json',
'help': '../commands/help.json',
@@ -49,7 +50,6 @@ const standardCommands = {
'test': '../commands/test.json',
'update': '../commands/update.json',
'version': '../commands/version.json',
- 'xi18n': '../commands/xi18n.json',
};
export interface CommandMapOptions {
@@ -97,8 +97,7 @@ async function loadCommandDescription(
registry: json.schema.CoreSchemaRegistry,
): Promise {
const schemaPath = resolve(__dirname, path);
- const schemaContent = readFileSync(schemaPath, 'utf-8');
- const schema = json.parseJson(schemaContent, JsonParseMode.Loose, { path: schemaPath });
+ const schema = readAndParseJson(schemaPath);
if (!isJsonObject(schema)) {
throw new Error('Invalid command JSON loaded from ' + JSON.stringify(schemaPath));
}
diff --git a/packages/angular/cli/models/command.ts b/packages/angular/cli/models/command.ts
index 4f7eac9fb837..eec0c3eec802 100644
--- a/packages/angular/cli/models/command.ts
+++ b/packages/angular/cli/models/command.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
@@ -54,7 +54,10 @@ export abstract class Command
}
async printJsonHelp(_options: T & Arguments): Promise {
- this.logger.info(JSON.stringify(this.description));
+ const replacer = (key: string, value: string) => key === 'name'
+ ? strings.dasherize(value)
+ : value;
+ this.logger.info(JSON.stringify(this.description, replacer, 2));
return 0;
}
@@ -160,7 +163,7 @@ export abstract class Command
this.analytics.pageview('/command/' + paths.join('/'), { dimensions, metrics });
}
- abstract async run(options: T & Arguments): Promise;
+ abstract run(options: T & Arguments): Promise;
async validateAndRun(options: T & Arguments): Promise {
if (!(options.help === true || options.help === 'json' || options.help === 'JSON')) {
diff --git a/packages/angular/cli/models/error.ts b/packages/angular/cli/models/error.ts
index 5d9d323ed103..dacdd2f3a38d 100644
--- a/packages/angular/cli/models/error.ts
+++ b/packages/angular/cli/models/error.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
diff --git a/packages/angular/cli/models/interface.ts b/packages/angular/cli/models/interface.ts
index 338b6310bce5..f5b3ee4de60e 100644
--- a/packages/angular/cli/models/interface.ts
+++ b/packages/angular/cli/models/interface.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
@@ -153,6 +153,12 @@ export interface Option {
* If this is falsey, do not report this option.
*/
userAnalytics?: number;
+
+ /**
+ * Deprecation. If this flag is not false a warning will be shown on the console. Either `true`
+ * or a string to show the user as a notice.
+ */
+ deprecated?: boolean | string;
}
/**
diff --git a/packages/angular/cli/models/parser.ts b/packages/angular/cli/models/parser.ts
index e09405132a33..951e35b1dadd 100644
--- a/packages/angular/cli/models/parser.ts
+++ b/packages/angular/cli/models/parser.ts
@@ -1,10 +1,9 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
- *
*/
import { BaseException, logging, strings } from '@angular-devkit/core';
import { Arguments, Option, OptionType, Value } from './interface';
@@ -211,6 +210,13 @@ function _assignOption(
errors.push(error);
ignored.push(arg);
}
+
+ if (/^[a-z]+[A-Z]/.test(key)) {
+ warnings.push(
+ 'Support for camel case arguments has been deprecated and will be removed in a future major version.\n' +
+ `Use '--${strings.dasherize(key)}' instead of '--${key}'.`,
+ );
+ }
}
return consumedNextArg;
diff --git a/packages/angular/cli/models/parser_spec.ts b/packages/angular/cli/models/parser_spec.ts
index cd6b3c09e801..152cdb36fe58 100644
--- a/packages/angular/cli/models/parser_spec.ts
+++ b/packages/angular/cli/models/parser_spec.ts
@@ -1,10 +1,9 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
- *
*/
// tslint:disable:no-global-tslint-disable no-big-function
import { logging } from '@angular-devkit/core';
diff --git a/packages/angular/cli/models/schematic-command.ts b/packages/angular/cli/models/schematic-command.ts
index 92f87ad0075b..f67024eb7761 100644
--- a/packages/angular/cli/models/schematic-command.ts
+++ b/packages/angular/cli/models/schematic-command.ts
@@ -1,12 +1,11 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {
- json,
logging,
normalize,
schema,
@@ -24,25 +23,20 @@ import {
FileSystemCollection,
FileSystemEngine,
FileSystemSchematic,
- FileSystemSchematicDescription,
NodeWorkflow,
} from '@angular-devkit/schematics/tools';
import * as inquirer from 'inquirer';
import * as systemPath from 'path';
import { colors } from '../utilities/color';
-import {
- getProjectByCwd,
- getSchematicDefaults,
- getWorkspace,
- getWorkspaceRaw,
-} from '../utilities/config';
+import { getProjectByCwd, getSchematicDefaults, getWorkspace } from '../utilities/config';
import { parseJsonSchemaToOptions } from '../utilities/json-schema';
-import { getPackageManager } from '../utilities/package-manager';
+import { ensureCompatibleNpm, getPackageManager } from '../utilities/package-manager';
import { isTTY } from '../utilities/tty';
import { isPackageNameSafeForAnalytics } from './analytics';
import { BaseCommandOptions, Command } from './command';
import { Arguments, CommandContext, CommandDescription, Option } from './interface';
import { parseArguments, parseFreeFormArguments } from './parser';
+import { SchematicEngineHost } from './schematic-engine-host';
export interface BaseSchematicSchema {
debug?: boolean;
@@ -257,6 +251,14 @@ export abstract class SchematicCommand<
// Global
: [__dirname, process.cwd()],
schemaValidation: true,
+ optionTransforms: [
+ // Add configuration file defaults
+ async (schematic, current) => ({
+ ...(await getSchematicDefaults(schematic.collection.name, schematic.name, getProjectName())),
+ ...current,
+ }),
+ ],
+ engineHostCreator: (options) => new SchematicEngineHost(options.resolvePaths),
});
const getProjectName = () => {
@@ -284,16 +286,6 @@ export abstract class SchematicCommand<
return undefined;
};
- const defaultOptionTransform = async (
- schematic: FileSystemSchematicDescription,
- current: {},
- ) => ({
- ...(await getSchematicDefaults(schematic.collection.name, schematic.name, getProjectName())),
- ...current,
- });
-
- workflow.engineHost.registerOptionsTransform(defaultOptionTransform);
-
workflow.registry.addPostTransform(schema.transforms.addUndefinedDefaults);
workflow.registry.addSmartDefaultProvider('projectName', getProjectName);
workflow.registry.useXDeprecatedProvider(msg => this.logger.warn(msg));
@@ -322,6 +314,32 @@ export abstract class SchematicCommand<
const validator = definition.validator;
if (validator) {
question.validate = input => validator(input);
+
+ // Filter allows transformation of the value prior to validation
+ question.filter = async (input) => {
+ for (const type of definition.propertyTypes) {
+ let value;
+ switch (type) {
+ case 'string':
+ value = String(input);
+ break;
+ case 'integer':
+ case 'number':
+ value = Number(input);
+ break;
+ default:
+ value = input;
+ break;
+ }
+ // Can be a string if validation fails
+ const isValid = (await validator(value)) === true;
+ if (isValid) {
+ return value;
+ }
+ }
+
+ return input;
+ };
}
switch (definition.type) {
@@ -407,43 +425,6 @@ export abstract class SchematicCommand<
collectionName = schematic.collection.description.name;
schematicName = schematic.description.name;
- // TODO: Remove warning check when 'targets' is default
- if (collectionName !== this.defaultCollectionName) {
- const [ast, configPath] = getWorkspaceRaw('local');
- if (ast) {
- const projectsKeyValue = ast.properties.find(p => p.key.value === 'projects');
- if (!projectsKeyValue || projectsKeyValue.value.kind !== 'object') {
- return;
- }
-
- const positions: json.Position[] = [];
- for (const projectKeyValue of projectsKeyValue.value.properties) {
- const projectNode = projectKeyValue.value;
- if (projectNode.kind !== 'object') {
- continue;
- }
- const targetsKeyValue = projectNode.properties.find(p => p.key.value === 'targets');
- if (targetsKeyValue) {
- positions.push(targetsKeyValue.start);
- }
- }
-
- if (positions.length > 0) {
- const warning = tags.oneLine`
- Warning: This command may not execute successfully.
- The package/collection may not support the 'targets' field within '${configPath}'.
- This can be corrected by renaming the following 'targets' fields to 'architect':
- `;
-
- const locations = positions
- .map((p, i) => `${i + 1}) Line: ${p.line + 1}; Column: ${p.character + 1}`)
- .join('\n');
-
- this.logger.warn(warning + '\n' + locations + '\n');
- }
- }
- }
-
// Set the options of format "path".
let o: Option[] | null = null;
let args: Arguments;
@@ -522,6 +503,16 @@ export abstract class SchematicCommand<
}
});
+ // Temporary compatibility check for NPM 7
+ if (collectionName === '@schematics/angular' && schematicName === 'ng-new') {
+ if (
+ !input.skipInstall &&
+ (input.packageManager === undefined || input.packageManager === 'npm')
+ ) {
+ await ensureCompatibleNpm(this.context.root);
+ }
+ }
+
return new Promise(resolve => {
workflow
.execute({
@@ -539,7 +530,7 @@ export abstract class SchematicCommand<
// "See above" because we already printed the error.
this.logger.fatal('The Schematic workflow failed. See above.');
} else if (debug) {
- this.logger.fatal(`An error occured:\n${err.message}\n${err.stack}`);
+ this.logger.fatal(`An error occurred:\n${err.message}\n${err.stack}`);
} else {
this.logger.fatal(err.message);
}
diff --git a/packages/angular/cli/models/schematic-engine-host.ts b/packages/angular/cli/models/schematic-engine-host.ts
new file mode 100644
index 000000000000..df3482e5adf7
--- /dev/null
+++ b/packages/angular/cli/models/schematic-engine-host.ts
@@ -0,0 +1,189 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+import { RuleFactory, SchematicsException, Tree } from '@angular-devkit/schematics';
+import { NodeModulesEngineHost } from '@angular-devkit/schematics/tools';
+import { readFileSync } from 'fs';
+import { parse as parseJson } from 'jsonc-parser';
+import { dirname, resolve } from 'path';
+import { Script } from 'vm';
+
+/**
+ * Environment variable to control schematic package redirection
+ * Default: Angular schematics only
+ */
+const schematicRedirectVariable = process.env['NG_SCHEMATIC_REDIRECT']?.toLowerCase();
+
+function shouldWrapSchematic(schematicFile: string): boolean {
+ // Check environment variable if present
+ if (schematicRedirectVariable !== undefined) {
+ switch (schematicRedirectVariable) {
+ case '0':
+ case 'false':
+ case 'off':
+ case 'none':
+ return false;
+ case 'all':
+ return true;
+ }
+ }
+
+ // Never wrap `@schematics/update` when executed directly
+ // It communicates with the update command via `global`
+ if (/[\/\\]node_modules[\/\\]@schematics[\/\\]update[\/\\]/.test(schematicFile)) {
+ return false;
+ }
+
+ // Default is only first-party Angular schematic packages
+ // Angular schematics are safe to use in the wrapped VM context
+ return /[\/\\]node_modules[\/\\]@(?:angular|schematics|nguniversal)[\/\\]/.test(schematicFile);
+}
+
+export class SchematicEngineHost extends NodeModulesEngineHost {
+ protected _resolveReferenceString(refString: string, parentPath: string) {
+ const [path, name] = refString.split('#', 2);
+ // Mimic behavior of ExportStringRef class used in default behavior
+ const fullPath = path[0] === '.' ? resolve(parentPath ?? process.cwd(), path) : path;
+
+ const schematicFile = require.resolve(fullPath, { paths: [parentPath] });
+
+ if (shouldWrapSchematic(schematicFile)) {
+ const schematicPath = dirname(schematicFile);
+
+ const moduleCache = new Map();
+ const factoryInitializer = wrap(
+ schematicFile,
+ schematicPath,
+ moduleCache,
+ name || 'default',
+ ) as () => RuleFactory<{}>;
+
+ const factory = factoryInitializer();
+ if (!factory || typeof factory !== 'function') {
+ return null;
+ }
+
+ return { ref: factory, path: schematicPath };
+ }
+
+ // All other schematics use default behavior
+ return super._resolveReferenceString(refString, parentPath);
+ }
+}
+
+/**
+ * Minimal shim modules for legacy deep imports of `@schematics/angular`
+ */
+const legacyModules: Record = {
+ '@schematics/angular/utility/config': {
+ getWorkspace(host: Tree) {
+ const path = '/.angular.json';
+ const data = host.read(path);
+ if (!data) {
+ throw new SchematicsException(`Could not find (${path})`);
+ }
+
+ return parseJson(data.toString(), [], { allowTrailingComma: true });
+ },
+ },
+ '@schematics/angular/utility/project': {
+ buildDefaultPath(project: { sourceRoot?: string; root: string; projectType: string }): string {
+ const root = project.sourceRoot ? `/${project.sourceRoot}/` : `/${project.root}/src/`;
+
+ return `${root}${project.projectType === 'application' ? 'app' : 'lib'}`;
+ },
+ },
+};
+
+/**
+ * Wrap a JavaScript file in a VM context to allow specific Angular dependencies to be redirected.
+ * This VM setup is ONLY intended to redirect dependencies.
+ *
+ * @param schematicFile A JavaScript schematic file path that should be wrapped.
+ * @param schematicDirectory A directory that will be used as the location of the JavaScript file.
+ * @param moduleCache A map to use for caching repeat module usage and proper `instanceof` support.
+ * @param exportName An optional name of a specific export to return. Otherwise, return all exports.
+ */
+function wrap(
+ schematicFile: string,
+ schematicDirectory: string,
+ moduleCache: Map,
+ exportName?: string,
+): () => unknown {
+ const { createRequire, createRequireFromPath } = require('module');
+ // Node.js 10.x does not support `createRequire` so fallback to `createRequireFromPath`
+ // `createRequireFromPath` is deprecated in 12+ and can be removed once 10.x support is removed
+ const scopedRequire = createRequire?.(schematicFile) || createRequireFromPath(schematicFile);
+
+ const customRequire = function (id: string) {
+ if (legacyModules[id]) {
+ // Provide compatibility modules for older versions of @angular/cdk
+ return legacyModules[id];
+ } else if (id.startsWith('@angular-devkit/') || id.startsWith('@schematics/')) {
+ // Resolve from inside the `@angular/cli` project
+ const packagePath = require.resolve(id);
+
+ return require(packagePath);
+ } else if (id.startsWith('.') || id.startsWith('@angular/cdk')) {
+ // Wrap relative files inside the schematic collection
+ // Also wrap `@angular/cdk`, it contains helper utilities that import core schematic packages
+
+ // Resolve from the original file
+ const modulePath = scopedRequire.resolve(id);
+
+ // Use cached module if available
+ const cachedModule = moduleCache.get(modulePath);
+ if (cachedModule) {
+ return cachedModule;
+ }
+
+ // Do not wrap vendored third-party packages or JSON files
+ if (
+ !/[\/\\]node_modules[\/\\]@schematics[\/\\]angular[\/\\]third_party[\/\\]/.test(
+ modulePath,
+ ) &&
+ !modulePath.endsWith('.json')
+ ) {
+ // Wrap module and save in cache
+ const wrappedModule = wrap(modulePath, dirname(modulePath), moduleCache)();
+ moduleCache.set(modulePath, wrappedModule);
+
+ return wrappedModule;
+ }
+ }
+
+ // All others are required directly from the original file
+ return scopedRequire(id);
+ };
+
+ // Setup a wrapper function to capture the module's exports
+ const schematicCode = readFileSync(schematicFile, 'utf8');
+ // `module` is required due to @angular/localize ng-add being in UMD format
+ const headerCode = '(function() {\nvar exports = {};\nvar module = { exports };\n';
+ const footerCode = exportName ? `\nreturn exports['${exportName}'];});` : '\nreturn exports;});';
+
+ const script = new Script(headerCode + schematicCode + footerCode, {
+ filename: schematicFile,
+ lineOffset: 3,
+ });
+
+ const context = {
+ __dirname: schematicDirectory,
+ __filename: schematicFile,
+ Buffer,
+ console,
+ process,
+ get global() {
+ return this;
+ },
+ require: customRequire,
+ };
+
+ const exportsFactory = script.runInNewContext(context);
+
+ return exportsFactory;
+}
diff --git a/packages/angular/cli/package.json b/packages/angular/cli/package.json
index 35b97a5fc985..4953c40e409d 100644
--- a/packages/angular/cli/package.json
+++ b/packages/angular/cli/package.json
@@ -32,26 +32,28 @@
"@schematics/update": "0.0.0",
"@yarnpkg/lockfile": "1.1.0",
"ansi-colors": "4.1.1",
- "debug": "4.2.0",
- "ini": "1.3.5",
+ "debug": "4.3.1",
+ "ini": "2.0.0",
"inquirer": "7.3.3",
+ "jsonc-parser": "3.0.0",
"npm-package-arg": "8.1.0",
"npm-pick-manifest": "6.1.0",
- "open": "7.3.0",
- "pacote": "9.5.12",
- "resolve": "1.17.0",
+ "open": "7.4.0",
+ "ora": "5.3.0",
+ "pacote": "11.2.4",
+ "resolve": "1.19.0",
"rimraf": "3.0.2",
- "semver": "7.3.2",
- "symbol-observable": "2.0.3",
+ "semver": "7.3.4",
+ "symbol-observable": "3.0.0",
"universal-analytics": "0.4.23",
- "uuid": "8.3.1"
+ "uuid": "8.3.2"
},
"ng-update": {
"migrations": "@schematics/angular/migrations/migration-collection.json",
"packageGroup": {
"@angular/cli": "0.0.0",
+ "@angular-devkit/architect": "0.0.0",
"@angular-devkit/build-angular": "0.0.0",
- "@angular-devkit/build-ng-packagr": "0.0.0",
"@angular-devkit/build-webpack": "0.0.0",
"@angular-devkit/core": "0.0.0",
"@angular-devkit/schematics": "0.0.0"
diff --git a/packages/angular/cli/utilities/color.ts b/packages/angular/cli/utilities/color.ts
index 6e7d11cb7fe2..c729fa6a5811 100644
--- a/packages/angular/cli/utilities/color.ts
+++ b/packages/angular/cli/utilities/color.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
diff --git a/packages/angular/cli/utilities/config.ts b/packages/angular/cli/utilities/config.ts
index 029aaa156858..9bbef68eeee6 100644
--- a/packages/angular/cli/utilities/config.ts
+++ b/packages/angular/cli/utilities/config.ts
@@ -1,30 +1,46 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
-
-import {
- JsonAstObject,
- JsonObject,
- JsonParseMode,
- json,
- parseJson,
- parseJsonAst,
- workspaces,
-} from '@angular-devkit/core';
-import { NodeJsSyncHost } from '@angular-devkit/core/node';
-import { existsSync, readFileSync, writeFileSync } from 'fs';
+import { json, workspaces } from '@angular-devkit/core';
+import { existsSync, readFileSync, statSync, writeFileSync } from 'fs';
import * as os from 'os';
import * as path from 'path';
import { findUp } from './find-up';
+import { JSONFile, readAndParseJson } from './json-file';
function isJsonObject(value: json.JsonValue | undefined): value is json.JsonObject {
return value !== undefined && json.isJsonObject(value);
}
+function createWorkspaceHost(): workspaces.WorkspaceHost {
+ return {
+ async readFile(path) {
+ return readFileSync(path, 'utf-8');
+ },
+ async writeFile(path, data) {
+ writeFileSync(path, data);
+ },
+ async isDirectory(path) {
+ try {
+ return statSync(path).isDirectory();
+ } catch {
+ return false;
+ }
+ },
+ async isFile(path) {
+ try {
+ return statSync(path).isFile();
+ } catch {
+ return false;
+ }
+ },
+ };
+}
+
function getSchemaLocation(): string {
return path.join(__dirname, '../lib/config/schema.json');
}
@@ -116,7 +132,7 @@ export class AngularWorkspace {
const result = await workspaces.readWorkspace(
workspaceFilePath,
- workspaces.createWorkspaceHost(new NodeJsSyncHost()),
+ createWorkspaceHost(),
workspaces.WorkspaceFormat.JSON,
);
@@ -143,13 +159,7 @@ export async function getWorkspace(
}
try {
- const result = await workspaces.readWorkspace(
- configPath,
- workspaces.createWorkspaceHost(new NodeJsSyncHost()),
- workspaces.WorkspaceFormat.JSON,
- );
-
- const workspace = new AngularWorkspace(result.workspace, configPath);
+ const workspace = await AngularWorkspace.load(configPath);
cachedWorkspaces.set(level, workspace);
return workspace;
@@ -175,7 +185,7 @@ export function createGlobalSettings(): string {
export function getWorkspaceRaw(
level: 'local' | 'global' = 'local',
-): [JsonAstObject | null, string | null] {
+): [JSONFile | null, string | null] {
let configPath = level === 'local' ? projectFilePath() : globalFilePath();
if (!configPath) {
@@ -186,28 +196,11 @@ export function getWorkspaceRaw(
}
}
- const data = readFileSync(configPath);
- let start = 0;
- if (data.length > 3 && data[0] === 0xef && data[1] === 0xbb && data[2] === 0xbf) {
- // Remove BOM
- start = 3;
- }
- const content = data.toString('utf-8', start);
- const ast = parseJsonAst(content, JsonParseMode.Loose);
-
- if (ast.kind != 'object') {
- throw new Error(`Invalid JSON file: ${configPath}`);
- }
-
- return [ast, configPath];
+ return [new JSONFile(configPath), configPath];
}
-export async function validateWorkspace(data: JsonObject): Promise {
- const schemaContent = readFileSync(
- path.join(__dirname, '..', 'lib', 'config', 'schema.json'),
- 'utf-8',
- );
- const schema = parseJson(schemaContent, JsonParseMode.Loose) as json.schema.JsonSchema;
+export async function validateWorkspace(data: json.JsonObject): Promise {
+ const schema = readAndParseJson(path.join(__dirname, '../lib/config/schema.json')) as json.schema.JsonSchema;
const { formats } = await import('@angular-devkit/schematics');
const registry = new json.schema.CoreSchemaRegistry(formats.standardFormats);
const validator = await registry.compile(schema).toPromise();
@@ -321,13 +314,12 @@ export function migrateLegacyGlobalConfig(): boolean {
if (homeDir) {
const legacyGlobalConfigPath = path.join(homeDir, '.angular-cli.json');
if (existsSync(legacyGlobalConfigPath)) {
- const content = readFileSync(legacyGlobalConfigPath, 'utf-8');
- const legacy = parseJson(content, JsonParseMode.Loose);
+ const legacy = readAndParseJson(legacyGlobalConfigPath);
if (!isJsonObject(legacy)) {
return false;
}
- const cli: JsonObject = {};
+ const cli: json.JsonObject = {};
if (
legacy.packageManager &&
@@ -346,7 +338,7 @@ export function migrateLegacyGlobalConfig(): boolean {
}
if (isJsonObject(legacy.warnings)) {
- const warnings: JsonObject = {};
+ const warnings: json.JsonObject = {};
if (typeof legacy.warnings.versionMismatch == 'boolean') {
warnings['versionMismatch'] = legacy.warnings.versionMismatch;
}
@@ -374,9 +366,7 @@ function getLegacyPackageManager(): string | null {
if (homeDir) {
const legacyGlobalConfigPath = path.join(homeDir, '.angular-cli.json');
if (existsSync(legacyGlobalConfigPath)) {
- const content = readFileSync(legacyGlobalConfigPath, 'utf-8');
-
- const legacy = parseJson(content, JsonParseMode.Loose);
+ const legacy = readAndParseJson(legacyGlobalConfigPath);
if (!isJsonObject(legacy)) {
return null;
}
diff --git a/packages/angular/cli/utilities/find-up.ts b/packages/angular/cli/utilities/find-up.ts
index 81891a96e565..3427d7ba15f4 100644
--- a/packages/angular/cli/utilities/find-up.ts
+++ b/packages/angular/cli/utilities/find-up.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
diff --git a/packages/angular/cli/tasks/install-package.ts b/packages/angular/cli/utilities/install-package.ts
similarity index 95%
rename from packages/angular/cli/tasks/install-package.ts
rename to packages/angular/cli/utilities/install-package.ts
index a4862a3966b3..8bdf179230e3 100644
--- a/packages/angular/cli/tasks/install-package.ts
+++ b/packages/angular/cli/utilities/install-package.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
@@ -26,7 +26,7 @@ interface PackageManagerOptions {
export function installPackage(
packageName: string,
- logger: logging.Logger,
+ logger: logging.Logger | undefined,
packageManager: PackageManager = PackageManager.Npm,
save: Exclude = true,
extraArgs: string[] = [],
@@ -40,7 +40,7 @@ export function installPackage(
packageManagerArgs.silent,
];
- logger.info(colors.green(`Installing packages for tooling via ${packageManager}.`));
+ logger?.info(colors.green(`Installing packages for tooling via ${packageManager}.`));
if (save === 'devDependencies') {
installArgs.push(packageManagerArgs.saveDev);
@@ -61,12 +61,12 @@ export function installPackage(
throw new Error(errorMessage + `Package install failed${errorMessage ? ', see above' : ''}.`);
}
- logger.info(colors.green(`Installed packages for tooling via ${packageManager}.`));
+ logger?.info(colors.green(`Installed packages for tooling via ${packageManager}.`));
}
export function installTempPackage(
packageName: string,
- logger: logging.Logger,
+ logger: logging.Logger | undefined,
packageManager: PackageManager = PackageManager.Npm,
extraArgs?: string[],
): string {
diff --git a/packages/angular/cli/utilities/json-file.ts b/packages/angular/cli/utilities/json-file.ts
new file mode 100644
index 000000000000..7163df2b0751
--- /dev/null
+++ b/packages/angular/cli/utilities/json-file.ts
@@ -0,0 +1,125 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+import { JsonValue } from '@angular-devkit/core';
+import { readFileSync, writeFileSync } from 'fs';
+import {
+ Node, ParseError, applyEdits, findNodeAtLocation,
+ getNodeValue, modify, parse, parseTree, printParseErrorCode,
+} from 'jsonc-parser';
+
+export type InsertionIndex = (properties: string[]) => number;
+export type JSONPath = (string | number)[];
+
+/** @internal */
+export class JSONFile {
+ content: string;
+
+ constructor(
+ private readonly path: string,
+ ) {
+ const buffer = readFileSync(this.path);
+ if (buffer) {
+ this.content = buffer.toString();
+ } else {
+ throw new Error(`Could not read '${path}'.`);
+ }
+ }
+
+ private _jsonAst: Node | undefined;
+ private get JsonAst(): Node | undefined {
+ if (this._jsonAst) {
+ return this._jsonAst;
+ }
+
+ const errors: ParseError[] = [];
+ this._jsonAst = parseTree(this.content, errors, { allowTrailingComma: true });
+ if (errors.length) {
+ formatError(this.path, errors);
+ }
+
+ return this._jsonAst;
+ }
+
+ get(jsonPath: JSONPath): unknown {
+ const jsonAstNode = this.JsonAst;
+ if (!jsonAstNode) {
+ return undefined;
+ }
+
+ if (jsonPath.length === 0) {
+ return getNodeValue(jsonAstNode);
+ }
+
+ const node = findNodeAtLocation(jsonAstNode, jsonPath);
+
+ return node === undefined ? undefined : getNodeValue(node);
+ }
+
+ modify(jsonPath: JSONPath, value: JsonValue | undefined, insertInOrder?: InsertionIndex | false): boolean {
+ if (value === undefined && this.get(jsonPath) === undefined) {
+ // Cannot remove a value which doesn't exist.
+ return false;
+ }
+
+ let getInsertionIndex: InsertionIndex | undefined;
+ if (insertInOrder === undefined) {
+ const property = jsonPath.slice(-1)[0];
+ getInsertionIndex = properties => [...properties, property].sort().findIndex(p => p === property);
+ } else if (insertInOrder !== false) {
+ getInsertionIndex = insertInOrder;
+ }
+
+ const edits = modify(
+ this.content,
+ jsonPath,
+ value,
+ {
+ getInsertionIndex,
+ formattingOptions: {
+ insertSpaces: true,
+ tabSize: 2,
+ },
+ },
+ );
+
+ if (edits.length === 0) {
+ return false;
+ }
+
+ this.content = applyEdits(this.content, edits);
+ this._jsonAst = undefined;
+
+ return true;
+ }
+
+ save(): void {
+ writeFileSync(this.path, this.content);
+ }
+}
+
+// tslint:disable-next-line: no-any
+export function readAndParseJson(path: string): any {
+ const errors: ParseError[] = [];
+ const content = parse(readFileSync(path, 'utf-8'), errors, { allowTrailingComma: true });
+ if (errors.length) {
+ formatError(path, errors);
+ }
+
+ return content;
+}
+
+function formatError(path: string, errors: ParseError[]): never {
+ const { error, offset } = errors[0];
+ throw new Error(`Failed to parse "${path}" as JSON AST Object. ${printParseErrorCode(error)} at location: ${offset}.`);
+}
+
+// tslint:disable-next-line: no-any
+export function parseJson(content: string): any {
+ return parse(content, undefined, { allowTrailingComma: true });
+}
diff --git a/packages/angular/cli/utilities/json-schema.ts b/packages/angular/cli/utilities/json-schema.ts
index abd856c4196a..d50fd7fbabd4 100644
--- a/packages/angular/cli/utilities/json-schema.ts
+++ b/packages/angular/cli/utilities/json-schema.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
@@ -251,6 +251,11 @@ export async function parseJsonSchemaToOptions(
const xUserAnalytics = current['x-user-analytics'];
const userAnalytics = typeof xUserAnalytics == 'number' ? xUserAnalytics : undefined;
+ // Deprecated is set only if it's true or a string.
+ const xDeprecated = current['x-deprecated'];
+ const deprecated = (xDeprecated === true || typeof xDeprecated === 'string')
+ ? xDeprecated : undefined;
+
const option: Option = {
name,
description: '' + (current.description === undefined ? '' : current.description),
@@ -262,6 +267,7 @@ export async function parseJsonSchemaToOptions(
...format !== undefined ? { format } : {},
hidden,
...userAnalytics ? { userAnalytics } : {},
+ ...deprecated !== undefined ? { deprecated } : {},
...positional !== undefined ? { positional } : {},
};
diff --git a/packages/angular/cli/utilities/json-schema_spec.ts b/packages/angular/cli/utilities/json-schema_spec.ts
index 09822cef7730..727ce7836065 100644
--- a/packages/angular/cli/utilities/json-schema_spec.ts
+++ b/packages/angular/cli/utilities/json-schema_spec.ts
@@ -1,10 +1,9 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
- *
*/
import { schema } from '@angular-devkit/core';
import { readFileSync } from 'fs';
diff --git a/packages/angular/cli/utilities/log-file.ts b/packages/angular/cli/utilities/log-file.ts
index 4e5d33bb4571..41dc036fc028 100644
--- a/packages/angular/cli/utilities/log-file.ts
+++ b/packages/angular/cli/utilities/log-file.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
diff --git a/packages/angular/cli/utilities/package-manager.ts b/packages/angular/cli/utilities/package-manager.ts
index 77d1318750f9..3163060db231 100644
--- a/packages/angular/cli/utilities/package-manager.ts
+++ b/packages/angular/cli/utilities/package-manager.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
@@ -8,6 +8,7 @@
import { execSync } from 'child_process';
import { existsSync } from 'fs';
import { join } from 'path';
+import { satisfies, valid } from 'semver';
import { PackageManager } from '../lib/config/schema';
import { getConfiguredPackageManager } from './config';
@@ -54,3 +55,30 @@ export async function getPackageManager(root: string): Promise {
// Potentially with a prompt to choose and optionally set as the default.
return packageManager || PackageManager.Npm;
}
+
+/**
+ * Checks if the npm version is a supported 7.x version. If not, display a warning.
+ */
+export async function ensureCompatibleNpm(root: string): Promise {
+ if ((await getPackageManager(root)) !== PackageManager.Npm) {
+ return;
+ }
+
+ try {
+ const versionText = execSync('npm --version', {encoding: 'utf8', stdio: 'pipe'}).trim();
+ const version = valid(versionText);
+ if (!version) {
+ return;
+ }
+
+ if (satisfies(version, '>=7 <7.5.6')) {
+ // tslint:disable-next-line: no-console
+ console.warn(
+ `npm version ${version} detected.` +
+ ' When using npm 7 with the Angular CLI, npm version 7.5.6 or higher is recommended.',
+ );
+ }
+ } catch {
+ // npm is not installed
+ }
+}
diff --git a/packages/angular/cli/utilities/package-metadata.ts b/packages/angular/cli/utilities/package-metadata.ts
index 34e5b4e0bf6f..e5c728ec76fb 100644
--- a/packages/angular/cli/utilities/package-metadata.ts
+++ b/packages/angular/cli/utilities/package-metadata.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
@@ -57,18 +57,20 @@ export interface PackageMetadata {
'dist-tags'?: unknown;
}
-let npmrc: { [key: string]: string };
+type PackageManagerOptions = Record;
+
+let npmrc: PackageManagerOptions;
function ensureNpmrc(logger: logging.LoggerApi, usingYarn: boolean, verbose: boolean): void {
if (!npmrc) {
try {
npmrc = readOptions(logger, false, verbose);
- } catch {}
+ } catch { }
if (usingYarn) {
try {
npmrc = { ...npmrc, ...readOptions(logger, true, verbose) };
- } catch {}
+ } catch { }
}
}
}
@@ -77,7 +79,7 @@ function readOptions(
logger: logging.LoggerApi,
yarn = false,
showPotentials = false,
-): Record {
+): PackageManagerOptions {
const cwd = process.cwd();
const baseFilename = yarn ? 'yarnrc' : 'npmrc';
const dotFilename = '.' + baseFilename;
@@ -107,7 +109,7 @@ function readOptions(
logger.info(`Locating potential ${baseFilename} files:`);
}
- let options: { [key: string]: string } = {};
+ const options: PackageManagerOptions = {};
for (const location of [...defaultConfigLocations, ...projectConfigLocations]) {
if (existsSync(location)) {
if (showPotentials) {
@@ -115,17 +117,40 @@ function readOptions(
}
const data = readFileSync(location, 'utf8');
- options = {
- ...options,
- ...(yarn ? lockfile.parse(data) : ini.parse(data)),
- };
-
- if (options.cafile) {
- const cafile = path.resolve(path.dirname(location), options.cafile);
- delete options.cafile;
- try {
- options.ca = readFileSync(cafile, 'utf8').replace(/\r?\n/, '\\n');
- } catch {}
+ // Normalize RC options that are needed by 'npm-registry-fetch'.
+ // See: https://github.com/npm/npm-registry-fetch/blob/ebddbe78a5f67118c1f7af2e02c8a22bcaf9e850/index.js#L99-L126
+ const rcConfig: PackageManagerOptions = yarn ? lockfile.parse(data) : ini.parse(data);
+ for (const [key, value] of Object.entries(rcConfig)) {
+ switch (key) {
+ case 'noproxy':
+ case 'no-proxy':
+ options['noProxy'] = value;
+ break;
+ case 'maxsockets':
+ options['maxSockets'] = value;
+ break;
+ case 'https-proxy':
+ case 'proxy':
+ options['proxy'] = value;
+ break;
+ case 'strict-ssl':
+ options['strictSSL'] = value;
+ break;
+ case 'local-address':
+ options['localAddress'] = value;
+ break;
+ case 'cafile':
+ if (typeof value === 'string') {
+ const cafile = path.resolve(path.dirname(location), value);
+ try {
+ options['ca'] = readFileSync(cafile, 'utf8').replace(/\r?\n/g, '\n');
+ } catch { }
+ }
+ break;
+ default:
+ options[key] = value;
+ break;
+ }
}
} else if (showPotentials) {
logger.info(`Trying '${location}'...not found.`);
@@ -134,8 +159,9 @@ function readOptions(
// Substitute any environment variable references
for (const key in options) {
- if (typeof options[key] === 'string') {
- options[key] = options[key].replace(/\$\{([^\}]+)\}/, (_, name) => process.env[name] || '');
+ const value = options[key];
+ if (typeof value === 'string') {
+ options[key] = value.replace(/\$\{([^\}]+)\}/, (_, name) => process.env[name] || '');
}
}
diff --git a/packages/angular/cli/utilities/package-tree.ts b/packages/angular/cli/utilities/package-tree.ts
index 85505e16531b..d4b7bba35e0b 100644
--- a/packages/angular/cli/utilities/package-tree.ts
+++ b/packages/angular/cli/utilities/package-tree.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
diff --git a/packages/angular/cli/utilities/project.ts b/packages/angular/cli/utilities/project.ts
index cd133cd0323a..d7a4f8bc1ca8 100644
--- a/packages/angular/cli/utilities/project.ts
+++ b/packages/angular/cli/utilities/project.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
diff --git a/packages/angular/cli/utilities/spinner.ts b/packages/angular/cli/utilities/spinner.ts
new file mode 100644
index 000000000000..21f3494a98da
--- /dev/null
+++ b/packages/angular/cli/utilities/spinner.ts
@@ -0,0 +1,59 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+import * as ora from 'ora';
+import { colors } from './color';
+
+export class Spinner {
+ private readonly spinner: ora.Ora;
+
+ /** When false, only fail messages will be displayed. */
+ enabled = true;
+
+ constructor(text?: string) {
+ this.spinner = ora({
+ text,
+ // The below 2 options are needed because otherwise CTRL+C will be delayed
+ // when the underlying process is sync.
+ hideCursor: false,
+ discardStdin: false,
+ });
+ }
+
+ set text(text: string) {
+ this.spinner.text = text;
+ }
+
+ succeed(text?: string): void {
+ if (this.enabled) {
+ this.spinner.succeed(text);
+ }
+ }
+
+ info(text?: string): void {
+ this.spinner.info(text);
+ }
+
+ fail(text?: string): void {
+ this.spinner.fail(text && colors.redBright(text));
+ }
+
+ warn(text?: string): void {
+ this.spinner.fail(text && colors.yellowBright(text));
+ }
+
+ stop(): void {
+ this.spinner.stop();
+ }
+
+ start(text?: string): void {
+ if (this.enabled) {
+ this.spinner.start(text);
+ }
+ }
+}
diff --git a/packages/angular/cli/utilities/tty.ts b/packages/angular/cli/utilities/tty.ts
index dd5931e26fb6..1e5658ebfd57 100644
--- a/packages/angular/cli/utilities/tty.ts
+++ b/packages/angular/cli/utilities/tty.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
diff --git a/packages/angular/pwa/README.md b/packages/angular/pwa/README.md
new file mode 100644
index 000000000000..9a2d8181fb8a
--- /dev/null
+++ b/packages/angular/pwa/README.md
@@ -0,0 +1,22 @@
+# `@angular/pwa`
+
+This is a [schematic](https://angular.io/guide/schematics) for adding
+[Progress Web App](https://web.dev/progressive-web-apps/) support to an Angular app. Run the
+schematic with the [Angular CLI](https://angular.io/cli):
+
+```shell
+ng add @angular/pwa
+```
+
+This makes a few changes to your project:
+
+1. Adds [`@angular/service-worker`](https://npmjs.com/@angular/service-worker) as a dependency.
+1. Enables service worker builds in the Angular CLI.
+1. Imports and registers the service worker in the app module.
+1. Adds a [web app manifest](https://developer.mozilla.org/en-US/docs/Web/Manifest).
+1. Updates the `index.html` file to link to the manifest and set theme colors.
+1. Adds required icons for the manifest.
+1. Creates a config file `ngsw-config.json`, specifying caching behaviors and other settings.
+
+See [Getting started with service workers](https://angular.io/guide/service-worker-getting-started)
+for more information.
diff --git a/packages/angular/pwa/pwa/index.ts b/packages/angular/pwa/pwa/index.ts
index 3fac23935f01..17cd5da7d46b 100644
--- a/packages/angular/pwa/pwa/index.ts
+++ b/packages/angular/pwa/pwa/index.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
diff --git a/packages/angular/pwa/pwa/index_spec.ts b/packages/angular/pwa/pwa/index_spec.ts
index b3a54fffcbcc..95bef3167bd0 100644
--- a/packages/angular/pwa/pwa/index_spec.ts
+++ b/packages/angular/pwa/pwa/index_spec.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
diff --git a/packages/angular_devkit/architect/builders/all-of.ts b/packages/angular_devkit/architect/builders/all-of.ts
index 0b9d958b1f7c..f846fcd32f21 100644
--- a/packages/angular_devkit/architect/builders/all-of.ts
+++ b/packages/angular_devkit/architect/builders/all-of.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
diff --git a/packages/angular_devkit/architect/builders/concat.ts b/packages/angular_devkit/architect/builders/concat.ts
index 816cb2028091..68d21b297968 100644
--- a/packages/angular_devkit/architect/builders/concat.ts
+++ b/packages/angular_devkit/architect/builders/concat.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
diff --git a/packages/angular_devkit/architect/builders/false.ts b/packages/angular_devkit/architect/builders/false.ts
index 3ae22b054e59..7cdb9b9d56eb 100644
--- a/packages/angular_devkit/architect/builders/false.ts
+++ b/packages/angular_devkit/architect/builders/false.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
diff --git a/packages/angular_devkit/architect/builders/true.ts b/packages/angular_devkit/architect/builders/true.ts
index ea617c33fd2b..6b110c012a3c 100644
--- a/packages/angular_devkit/architect/builders/true.ts
+++ b/packages/angular_devkit/architect/builders/true.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
diff --git a/packages/angular_devkit/architect/node/index.ts b/packages/angular_devkit/architect/node/index.ts
index 721047f44093..ab719a3f8ba3 100644
--- a/packages/angular_devkit/architect/node/index.ts
+++ b/packages/angular_devkit/architect/node/index.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
diff --git a/packages/angular_devkit/architect/node/node-modules-architect-host.ts b/packages/angular_devkit/architect/node/node-modules-architect-host.ts
index 8562b8e07ea6..d4cff7208631 100644
--- a/packages/angular_devkit/architect/node/node-modules-architect-host.ts
+++ b/packages/angular_devkit/architect/node/node-modules-architect-host.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
@@ -28,17 +28,86 @@ function clone(obj: unknown): unknown {
}
}
-// TODO: create a base class for all workspace related hosts.
-export class WorkspaceNodeModulesArchitectHost implements ArchitectHost {
- constructor(protected _workspace: workspaces.WorkspaceDefinition, protected _root: string) {}
+export interface WorkspaceHost {
+ getBuilderName(project: string, target: string): Promise;
+ getMetadata(project: string): Promise;
+ getOptions(project: string, target: string, configuration?: string): Promise;
+ hasTarget(project: string, target: string): Promise;
+}
- async getBuilderNameForTarget(target: Target) {
- const targetDefinition = this.findProjectTarget(target);
- if (!targetDefinition) {
- throw new Error('Project target does not exist.');
+function findProjectTarget(
+ workspace: workspaces.WorkspaceDefinition,
+ project: string,
+ target: string,
+): workspaces.TargetDefinition {
+ const projectDefinition = workspace.projects.get(project);
+ if (!projectDefinition) {
+ throw new Error(`Project "${project}" does not exist.`);
+ }
+
+ const targetDefinition = projectDefinition.targets.get(target);
+ if (!targetDefinition) {
+ throw new Error('Project target does not exist.');
+ }
+
+ return targetDefinition;
+}
+
+export class WorkspaceNodeModulesArchitectHost implements ArchitectHost {
+ private workspaceHost: WorkspaceHost;
+
+ constructor(workspaceHost: WorkspaceHost, _root: string);
+
+ constructor(workspace: workspaces.WorkspaceDefinition, _root: string);
+
+ constructor(
+ workspaceOrHost: workspaces.WorkspaceDefinition | WorkspaceHost,
+ protected _root: string,
+ ) {
+ if ('getBuilderName' in workspaceOrHost) {
+ this.workspaceHost = workspaceOrHost;
+ } else {
+ this.workspaceHost = {
+ async getBuilderName(project, target) {
+ const targetDefinition = findProjectTarget(workspaceOrHost, project, target);
+
+ return targetDefinition.builder;
+ },
+ async getOptions(project, target, configuration) {
+ const targetDefinition = findProjectTarget(workspaceOrHost, project, target);
+
+ if (configuration === undefined) {
+ return (targetDefinition.options ?? {}) as json.JsonObject;
+ }
+
+ if (!targetDefinition.configurations?.[configuration]) {
+ throw new Error(`Configuration '${configuration}' is not set in the workspace.`);
+ }
+
+ return (targetDefinition.configurations?.[configuration] ?? {}) as json.JsonObject;
+ },
+ async getMetadata(project) {
+ const projectDefinition = workspaceOrHost.projects.get(project);
+ if (!projectDefinition) {
+ throw new Error(`Project "${project}" does not exist.`);
+ }
+
+ return ({
+ root: projectDefinition.root,
+ sourceRoot: projectDefinition.sourceRoot,
+ prefix: projectDefinition.prefix,
+ ...(clone(projectDefinition.extensions) as {}),
+ } as unknown) as json.JsonObject;
+ },
+ async hasTarget(project, target) {
+ return !!workspaceOrHost.projects.get(project)?.targets.has(target);
+ },
+ };
}
+ }
- return targetDefinition.builder;
+ async getBuilderNameForTarget(target: Target) {
+ return this.workspaceHost.getBuilderName(target.project, target.target);
}
/**
@@ -95,49 +164,28 @@ export class WorkspaceNodeModulesArchitectHost implements ArchitectHost {
- const targetSpec = this.findProjectTarget(target);
- if (targetSpec === undefined) {
+ if (!(await this.workspaceHost.hasTarget(target.project, target.target))) {
return null;
}
- let additionalOptions = {};
+ let options = await this.workspaceHost.getOptions(target.project, target.target);
if (target.configuration) {
- const configurations = target.configuration.split(',').map(c => c.trim());
+ const configurations = target.configuration.split(',').map((c) => c.trim());
for (const configuration of configurations) {
- if (!(targetSpec['configurations'] && targetSpec['configurations'][configuration])) {
- throw new Error(`Configuration '${configuration}' is not set in the workspace.`);
- } else {
- additionalOptions = {
- ...additionalOptions,
- ...targetSpec['configurations'][configuration],
- };
- }
+ options = {
+ ...options,
+ ...await this.workspaceHost.getOptions(target.project, target.target, configuration),
+ };
}
}
- const options = {
- ...targetSpec['options'],
- ...additionalOptions,
- };
-
return clone(options) as json.JsonObject;
}
async getProjectMetadata(target: Target | string): Promise {
const projectName = typeof target === 'string' ? target : target.project;
-
- const projectDefinition = this._workspace.projects.get(projectName);
- if (!projectDefinition) {
- throw new Error(`Project "${projectName}" does not exist.`);
- }
-
- const metadata = ({
- root: projectDefinition.root,
- sourceRoot: projectDefinition.sourceRoot,
- prefix: projectDefinition.prefix,
- ...clone(projectDefinition.extensions) as {},
- } as unknown) as json.JsonObject;
+ const metadata = this.workspaceHost.getMetadata(projectName);
return metadata;
}
@@ -150,13 +198,4 @@ export class WorkspaceNodeModulesArchitectHost implements ArchitectHost=3.9 < 4.1"
+ "typescript": "~4.0.0 || ~4.1.0"
},
"peerDependenciesMeta": {
"@angular/localize": {
"optional": true
},
+ "@angular/service-worker": {
+ "optional": true
+ },
"karma": {
"optional": true
},
@@ -99,6 +108,9 @@
"protractor": {
"optional": true
},
+ "tailwindcss": {
+ "optional": true
+ },
"tslint": {
"optional": true
}
diff --git a/packages/angular_devkit/build_angular/plugins/karma.ts b/packages/angular_devkit/build_angular/plugins/karma.ts
index 7a5803a55286..18409cffe9a9 100644
--- a/packages/angular_devkit/build_angular/plugins/karma.ts
+++ b/packages/angular_devkit/build_angular/plugins/karma.ts
@@ -1,9 +1,9 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
-module.exports = require('../src/webpack/plugins/karma');
+module.exports = require('../src/webpack/plugins/karma/karma');
diff --git a/packages/angular_devkit/build_angular/src/app-shell/app-shell_spec.ts b/packages/angular_devkit/build_angular/src/app-shell/app-shell_spec.ts
index 89d054d43e7d..4e4ef3d53296 100644
--- a/packages/angular_devkit/build_angular/src/app-shell/app-shell_spec.ts
+++ b/packages/angular_devkit/build_angular/src/app-shell/app-shell_spec.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
@@ -9,6 +9,8 @@
import { Architect } from '@angular-devkit/architect';
import { getSystemPath, join, normalize, virtualFs } from '@angular-devkit/core';
import * as express from 'express'; // tslint:disable-line:no-implicit-dependencies
+import * as http from 'http';
+import { AddressInfo } from 'net';
import { createArchitect, host } from '../test-utils';
describe('AppShell Builder', () => {
@@ -22,6 +24,9 @@ describe('AppShell Builder', () => {
afterEach(async () => host.restore().toPromise());
const appShellRouteFiles = {
+ 'src/styles.css': `
+ p { color: #000 }
+ `,
'src/app/app-shell/app-shell.component.html': `
app-shell works!
@@ -248,18 +253,44 @@ describe('AppShell Builder', () => {
// Serve the app using a simple static server.
const app = express();
app.use('/', express.static(getSystemPath(join(host.root(), 'dist')) + '/'));
- const server = app.listen(4200);
+ const server = await new Promise((resolve) => {
+ const innerServer = app.listen(0, 'localhost', () => resolve(innerServer));
+ });
+ try {
+ const serverPort = (server.address() as AddressInfo).port;
+ // Load app in protractor, then check service worker status.
+ const protractorRun = await architect.scheduleTarget(
+ { project: 'app-e2e', target: 'e2e' },
+ { baseUrl: `http://localhost:${serverPort}/`, devServerTarget: '' },
+ );
+
+ const protractorOutput = await protractorRun.result;
+ await protractorRun.stop();
+
+ expect(protractorOutput.success).toBe(true);
+ } finally {
+ // Close the express server.
+ await new Promise((resolve) => server.close(() => resolve()));
+ }
+ });
- // Load app in protractor, then check service worker status.
- const protractorRun = await architect.scheduleTarget(
- { project: 'app-e2e', target: 'e2e' },
- { devServerTarget: undefined } as {},
- );
- const protractorOutput = await protractorRun.result;
- await protractorRun.stop();
- expect(protractorOutput.success).toBe(true);
+ it('critical CSS is inlined', async () => {
+ host.writeMultipleFiles(appShellRouteFiles);
+ const overrides = {
+ route: 'shell',
+ browserTarget: 'app:build:production,inline-critical-css',
+ };
+
+ const run = await architect.scheduleTarget(target, overrides);
+ const output = await run.result;
+ await run.stop();
- // Close the express server.
- server.close();
+ expect(output.success).toBe(true);
+ const fileName = 'dist/index.html';
+ const content = virtualFs.fileBufferToString(host.scopedSync().read(normalize(fileName)));
+
+ expect(content).toContain('app-shell works!');
+ expect(content).toContain('p{color:#000}');
+ expect(content).toMatch(/ /);
});
});
diff --git a/packages/angular_devkit/build_angular/src/app-shell/index.ts b/packages/angular_devkit/build_angular/src/app-shell/index.ts
index f7996f0e68af..e20d01dad734 100644
--- a/packages/angular_devkit/build_angular/src/app-shell/index.ts
+++ b/packages/angular_devkit/build_angular/src/app-shell/index.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
@@ -12,13 +12,16 @@ import {
targetFromTargetString,
} from '@angular-devkit/architect';
import { JsonObject, normalize, resolve } from '@angular-devkit/core';
-import { NodeJsSyncHost } from '@angular-devkit/core/node';
import * as fs from 'fs';
import * as path from 'path';
import { BrowserBuilderOutput } from '../browser';
import { Schema as BrowserBuilderSchema } from '../browser/schema';
import { ServerBuilderOutput } from '../server';
+import { normalizeOptimization } from '../utils';
+import { readFile, writeFile } from '../utils/fs';
+import { InlineCriticalCssProcessor } from '../utils/index-file/inline-critical-css';
import { augmentAppWithServiceWorker } from '../utils/service-worker';
+import { Spinner } from '../utils/spinner';
import { Schema as BuildWebpackAppShellSchema } from './schema';
async function _renderUniversal(
@@ -26,22 +29,23 @@ async function _renderUniversal(
context: BuilderContext,
browserResult: BrowserBuilderOutput,
serverResult: ServerBuilderOutput,
+ spinner: Spinner,
): Promise {
// Get browser target options.
const browserTarget = targetFromTargetString(options.browserTarget);
- const rawBrowserOptions = await context.getTargetOptions(browserTarget);
+ const rawBrowserOptions = (await context.getTargetOptions(browserTarget)) as JsonObject & BrowserBuilderSchema;
const browserBuilderName = await context.getBuilderNameForTarget(browserTarget);
const browserOptions = await context.validateOptions(
rawBrowserOptions,
browserBuilderName,
);
+
// Initialize zone.js
const root = context.workspaceRoot;
const zonePackage = require.resolve('zone.js', { paths: [root] });
await import(zonePackage);
- const host = new NodeJsSyncHost();
const projectName = context.target && context.target.project;
if (!projectName) {
throw new Error('The builder requires a target.');
@@ -53,10 +57,18 @@ async function _renderUniversal(
normalize((projectMetadata.root as string) || ''),
);
+ const { styles } = normalizeOptimization(browserOptions.optimization);
+ const inlineCriticalCssProcessor = styles.inlineCritical
+ ? new InlineCriticalCssProcessor({
+ minify: styles.minify,
+ deployUrl: browserOptions.deployUrl,
+ })
+ : undefined;
+
for (const outputPath of browserResult.outputPaths) {
const localeDirectory = path.relative(browserResult.baseOutputPath, outputPath);
const browserIndexOutputPath = path.join(outputPath, 'index.html');
- const indexHtml = fs.readFileSync(browserIndexOutputPath, 'utf8');
+ const indexHtml = await readFile(browserIndexOutputPath, 'utf8');
const serverBundlePath = await _getServerModuleBundlePath(options, context, serverResult, localeDirectory);
const {
@@ -85,17 +97,28 @@ async function _renderUniversal(
url: options.route,
};
- const html = await renderModuleFn(AppServerModuleDef, renderOpts);
+ let html = await renderModuleFn(AppServerModuleDef, renderOpts);
// Overwrite the client index file.
const outputIndexPath = options.outputIndexPath
? path.join(root, options.outputIndexPath)
: browserIndexOutputPath;
- fs.writeFileSync(outputIndexPath, html);
+ if (inlineCriticalCssProcessor) {
+ const { content, warnings, errors } = await inlineCriticalCssProcessor.process(html, { outputPath });
+ html = content;
+
+ if (warnings.length || errors.length) {
+ spinner.stop();
+ warnings.forEach(m => context.logger.warn(m));
+ errors.forEach(m => context.logger.error(m));
+ spinner.start();
+ }
+ }
+
+ await writeFile(outputIndexPath, html);
if (browserOptions.serviceWorker) {
await augmentAppWithServiceWorker(
- host,
normalize(root),
projectRoot,
normalize(outputPath),
@@ -116,24 +139,23 @@ async function _getServerModuleBundlePath(
) {
if (options.appModuleBundle) {
return path.join(context.workspaceRoot, options.appModuleBundle);
- } else {
- const { baseOutputPath = '' } = serverResult;
- const outputPath = path.join(baseOutputPath, browserLocaleDirectory);
+ }
- if (!fs.existsSync(outputPath)) {
- throw new Error(`Could not find server output directory: ${outputPath}.`);
- }
+ const { baseOutputPath = '' } = serverResult;
+ const outputPath = path.join(baseOutputPath, browserLocaleDirectory);
- const files = fs.readdirSync(outputPath, 'utf8');
- const re = /^main\.(?:[a-zA-Z0-9]{20}\.)?(?:bundle\.)?js$/;
- const maybeMain = files.filter(x => re.test(x))[0];
+ if (!fs.existsSync(outputPath)) {
+ throw new Error(`Could not find server output directory: ${outputPath}.`);
+ }
- if (!maybeMain) {
- throw new Error('Could not find the main bundle.');
- } else {
- return path.join(outputPath, maybeMain);
- }
+ const re = /^main\.(?:[a-zA-Z0-9]{20}\.)?js$/;
+ const maybeMain = fs.readdirSync(outputPath).find(x => re.test(x));
+
+ if (!maybeMain) {
+ throw new Error('Could not find the main bundle.');
}
+
+ return path.join(outputPath, maybeMain);
}
async function _appShellBuilder(
@@ -145,14 +167,22 @@ async function _appShellBuilder(
// Never run the browser target in watch mode.
// If service worker is needed, it will be added in _renderUniversal();
+ const browserOptions = (await context.getTargetOptions(browserTarget)) as JsonObject & BrowserBuilderSchema;
+
+ const optimization = normalizeOptimization(browserOptions.optimization);
+ optimization.styles.inlineCritical = false;
+
const browserTargetRun = await context.scheduleTarget(browserTarget, {
watch: false,
serviceWorker: false,
+ optimization: (optimization as unknown as JsonObject),
});
const serverTargetRun = await context.scheduleTarget(serverTarget, {
watch: false,
});
+ let spinner: Spinner | undefined;
+
try {
const [browserResult, serverResult] = await Promise.all([
browserTargetRun.result as unknown as BrowserBuilderOutput,
@@ -165,8 +195,15 @@ async function _appShellBuilder(
return serverResult;
}
- return await _renderUniversal(options, context, browserResult, serverResult);
+ spinner = new Spinner();
+ spinner.start('Generating application shell...');
+ const result = await _renderUniversal(options, context, browserResult, serverResult, spinner);
+ spinner.succeed('Application shell generation complete.');
+
+ return result;
} catch (err) {
+ spinner?.fail('Application shell generation failed.');
+
return { success: false, error: err.message };
} finally {
// Just be good citizens and stop those jobs.
diff --git a/packages/angular_devkit/build_angular/src/app-shell/schema.json b/packages/angular_devkit/build_angular/src/app-shell/schema.json
index b89b795265ab..3adcc32a7521 100644
--- a/packages/angular_devkit/build_angular/src/app-shell/schema.json
+++ b/packages/angular_devkit/build_angular/src/app-shell/schema.json
@@ -6,12 +6,12 @@
"properties": {
"browserTarget": {
"type": "string",
- "description": "Target to build.",
+ "description": "A browser builder target use for rendering the app shell in the format of `project:target[:configuration]`. You can also pass in more than one configuration name as a comma-separated list. Example: `project:target:production,staging`.",
"pattern": "^[^:\\s]+:[^:\\s]+(:[^\\s]+)?$"
},
"serverTarget": {
"type": "string",
- "description": "Server target to use for rendering the app shell.",
+ "description": "A server builder target use for rendering the app shell in the format of `project:target[:configuration]`. You can also pass in more than one configuration name as a comma-separated list. Example: `project:target:production,staging`.",
"pattern": "^[^:\\s]+:[^:\\s]+(:[^\\s]+)?$"
},
"appModuleBundle": {
diff --git a/packages/angular_devkit/build_angular/src/babel-bazel.d.ts b/packages/angular_devkit/build_angular/src/babel-bazel.d.ts
new file mode 100644
index 000000000000..fe265e4d3b23
--- /dev/null
+++ b/packages/angular_devkit/build_angular/src/babel-bazel.d.ts
@@ -0,0 +1,22 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+// Workaround for https://github.com/bazelbuild/rules_nodejs/issues/1033
+// Alternative approach instead of https://github.com/angular/angular/pull/33226
+declare module '@babel/core' {
+ export * from '@types/babel__core';
+}
+declare module '@babel/generator' {
+ export { default } from '@types/babel__generator';
+}
+declare module '@babel/traverse' {
+ export { default } from '@types/babel__traverse';
+}
+declare module '@babel/template' {
+ export { default } from '@types/babel__template';
+}
diff --git a/packages/angular_devkit/build_angular/src/babel/babel-loader.d.ts b/packages/angular_devkit/build_angular/src/babel/babel-loader.d.ts
new file mode 100644
index 000000000000..1bc5380f631f
--- /dev/null
+++ b/packages/angular_devkit/build_angular/src/babel/babel-loader.d.ts
@@ -0,0 +1,24 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+declare module 'babel-loader' {
+ type BabelLoaderCustomizer = (
+ babel: typeof import('@babel/core'),
+ ) => {
+ customOptions?(
+ this: import('webpack').loader.LoaderContext,
+ loaderOptions: Record,
+ loaderArguments: { source: string; map?: unknown },
+ ): Promise<{ custom?: T; loader: Record }>;
+ config?(
+ this: import('webpack').loader.LoaderContext,
+ configuration: import('@babel/core').PartialConfig,
+ loaderArguments: { source: string; map?: unknown; customOptions: T },
+ ): import('@babel/core').TransformOptions;
+ };
+ function custom(customizer: BabelLoaderCustomizer): import('webpack').loader.Loader;
+}
diff --git a/packages/angular_devkit/build_angular/src/babel/presets/application.ts b/packages/angular_devkit/build_angular/src/babel/presets/application.ts
new file mode 100644
index 000000000000..98f2d67863d4
--- /dev/null
+++ b/packages/angular_devkit/build_angular/src/babel/presets/application.ts
@@ -0,0 +1,190 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+import * as fs from 'fs';
+import * as path from 'path';
+
+export type DiagnosticReporter = (type: 'error' | 'warning' | 'info', message: string) => void;
+export interface ApplicationPresetOptions {
+ i18n?: {
+ locale: string;
+ missingTranslationBehavior?: 'error' | 'warning' | 'ignore';
+ translation?: unknown;
+ };
+
+ angularLinker?: boolean;
+
+ forceES5?: boolean;
+ forceAsyncTransformation?: boolean;
+
+ diagnosticReporter?: DiagnosticReporter;
+}
+
+type I18nDiagnostics = import('@angular/localize/src/tools/src/diagnostics').Diagnostics;
+function createI18nDiagnostics(reporter: DiagnosticReporter | undefined): I18nDiagnostics {
+ // Babel currently is synchronous so import cannot be used
+ const diagnostics: I18nDiagnostics = new (require('@angular/localize/src/tools/src/diagnostics').Diagnostics)();
+
+ if (!reporter) {
+ return diagnostics;
+ }
+
+ const baseAdd = diagnostics.add;
+ diagnostics.add = function (type, message, ...args) {
+ if (type !== 'ignore') {
+ baseAdd.call(diagnostics, type, message, ...args);
+ reporter(type, message);
+ }
+ };
+
+ const baseError = diagnostics.error;
+ diagnostics.error = function (message, ...args) {
+ baseError.call(diagnostics, message, ...args);
+ reporter('error', message);
+ };
+
+ const baseWarn = diagnostics.warn;
+ diagnostics.warn = function (message, ...args) {
+ baseWarn.call(diagnostics, message, ...args);
+ reporter('warning', message);
+ };
+
+ const baseMerge = diagnostics.merge;
+ diagnostics.merge = function (other, ...args) {
+ baseMerge.call(diagnostics, other, ...args);
+ for (const diagnostic of other.messages) {
+ reporter(diagnostic.type, diagnostic.message);
+ }
+ };
+
+ return diagnostics;
+}
+
+function createI18nPlugins(
+ locale: string,
+ translation: unknown | undefined,
+ missingTranslationBehavior: 'error' | 'warning' | 'ignore',
+ diagnosticReporter: DiagnosticReporter | undefined,
+) {
+ const diagnostics = createI18nDiagnostics(diagnosticReporter);
+ const plugins = [];
+
+ if (translation) {
+ const {
+ makeEs2015TranslatePlugin,
+ } = require('@angular/localize/src/tools/src/translate/source_files/es2015_translate_plugin');
+ plugins.push(
+ makeEs2015TranslatePlugin(diagnostics, translation, {
+ missingTranslation: missingTranslationBehavior,
+ }),
+ );
+
+ const {
+ makeEs5TranslatePlugin,
+ } = require('@angular/localize/src/tools/src/translate/source_files/es5_translate_plugin');
+ plugins.push(
+ makeEs5TranslatePlugin(diagnostics, translation, {
+ missingTranslation: missingTranslationBehavior,
+ }),
+ );
+ }
+
+ const {
+ makeLocalePlugin,
+ } = require('@angular/localize/src/tools/src/translate/source_files/locale_plugin');
+ plugins.push(makeLocalePlugin(locale));
+
+ return plugins;
+}
+
+function createNgtscLogger(
+ reporter: DiagnosticReporter | undefined,
+): import('@angular/compiler-cli/src/ngtsc/logging').Logger {
+ return {
+ level: 1, // Info level
+ debug(...args: string[]) {},
+ info(...args: string[]) {
+ reporter?.('info', args.join());
+ },
+ warn(...args: string[]) {
+ reporter?.('warning', args.join());
+ },
+ error(...args: string[]) {
+ reporter?.('error', args.join());
+ },
+ };
+}
+
+export default function (api: unknown, options: ApplicationPresetOptions) {
+ const presets = [];
+ const plugins = [];
+ let needRuntimeTransform = false;
+
+ if (options.angularLinker) {
+ // Babel currently is synchronous so import cannot be used
+ const {
+ createEs2015LinkerPlugin,
+ } = require('@angular/compiler-cli/linker/babel');
+
+ plugins.push(createEs2015LinkerPlugin({
+ logger: createNgtscLogger(options.diagnosticReporter),
+ fileSystem: {
+ resolve: path.resolve,
+ exists: fs.existsSync,
+ dirname: path.dirname,
+ relative: path.relative,
+ readFile: fs.readFileSync,
+ },
+ }));
+ }
+
+ if (options.forceES5) {
+ presets.push([
+ require('@babel/preset-env').default,
+ {
+ bugfixes: true,
+ modules: false,
+ // Comparable behavior to tsconfig target of ES5
+ targets: { ie: 9 },
+ exclude: ['transform-typeof-symbol'],
+ },
+ ]);
+ needRuntimeTransform = true;
+ }
+
+ if (options.i18n) {
+ const { locale, missingTranslationBehavior, translation } = options.i18n;
+ const i18nPlugins = createI18nPlugins(
+ locale,
+ translation,
+ missingTranslationBehavior || 'ignore',
+ options.diagnosticReporter,
+ );
+
+ plugins.push(...i18nPlugins);
+ }
+
+ if (options.forceAsyncTransformation) {
+ // Always transform async/await to support Zone.js
+ plugins.push(require('@babel/plugin-transform-async-to-generator').default);
+ needRuntimeTransform = true;
+ }
+
+ if (needRuntimeTransform) {
+ // Babel equivalent to TypeScript's `importHelpers` option
+ plugins.push([
+ require('@babel/plugin-transform-runtime').default,
+ {
+ useESModules: true,
+ version: require('@babel/runtime/package.json').version,
+ absoluteRuntime: path.dirname(require.resolve('@babel/runtime/package.json')),
+ },
+ ]);
+ }
+
+ return { presets, plugins };
+}
diff --git a/packages/angular_devkit/build_angular/src/babel/webpack-loader.ts b/packages/angular_devkit/build_angular/src/babel/webpack-loader.ts
new file mode 100644
index 000000000000..964103e89a62
--- /dev/null
+++ b/packages/angular_devkit/build_angular/src/babel/webpack-loader.ts
@@ -0,0 +1,169 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+import { custom } from 'babel-loader';
+import { ScriptTarget } from 'typescript';
+import { ApplicationPresetOptions } from './presets/application';
+
+interface AngularCustomOptions {
+ forceAsyncTransformation: boolean;
+ forceES5: boolean;
+ shouldLink: boolean;
+ i18n: ApplicationPresetOptions['i18n'];
+}
+
+/**
+ * Cached linker check utility function
+ *
+ * If undefined, not yet been imported
+ * If null, attempted import failed and no linker support
+ * If function, import succeeded and linker supported
+ */
+let needsLinking: undefined | null | typeof import('@angular/compiler-cli/linker').needsLinking;
+
+async function checkLinking(
+ path: string,
+ source: string,
+): Promise<{ hasLinkerSupport?: boolean; requiresLinking: boolean }> {
+ // @angular/core and @angular/compiler will cause false positives
+ // Also, TypeScript files do not require linking
+ if (/[\\\/]@angular[\\\/](?:compiler|core)|\.tsx?$/.test(path)) {
+ return { requiresLinking: false };
+ }
+
+ if (needsLinking !== null) {
+ try {
+ if (needsLinking === undefined) {
+ needsLinking = (await import('@angular/compiler-cli/linker')).needsLinking;
+ }
+
+ // If the linker entry point is present then there is linker support
+ return { hasLinkerSupport: true, requiresLinking: needsLinking(path, source) };
+ } catch {
+ needsLinking = null;
+ }
+ }
+
+ // Fallback for Angular versions less than 11.1.0 with no linker support.
+ // This information is used to issue errors if a partially compiled library is used when unsupported.
+ return {
+ hasLinkerSupport: false,
+ requiresLinking:
+ source.includes('ɵɵngDeclareDirective') || source.includes('ɵɵngDeclareComponent'),
+ };
+}
+
+export default custom(() => {
+ const baseOptions = Object.freeze({
+ babelrc: false,
+ configFile: false,
+ compact: false,
+ cacheCompression: false,
+ sourceType: 'unambiguous',
+ inputSourceMap: false,
+ });
+
+ return {
+ async customOptions({ i18n, scriptTarget, ...rawOptions }, { source }) {
+ // Must process file if plugins are added
+ let shouldProcess = Array.isArray(rawOptions.plugins) && rawOptions.plugins.length > 0;
+
+ const customOptions: AngularCustomOptions = {
+ forceAsyncTransformation: false,
+ forceES5: false,
+ shouldLink: false,
+ i18n: undefined,
+ };
+
+ // Analyze file for linking
+ const { hasLinkerSupport, requiresLinking } = await checkLinking(this.resourcePath, source);
+ if (requiresLinking && !hasLinkerSupport) {
+ // Cannot link if there is no linker support
+ this.emitError(
+ 'File requires the Angular linker. "@angular/compiler-cli" version 11.1.0 or greater is needed.',
+ );
+ } else {
+ customOptions.shouldLink = requiresLinking;
+ }
+ shouldProcess ||= customOptions.shouldLink;
+
+ // Analyze for ES target processing
+ const esTarget = scriptTarget as ScriptTarget | undefined;
+ if (esTarget !== undefined) {
+ if (esTarget < ScriptTarget.ES2015) {
+ // TypeScript files will have already been downlevelled
+ customOptions.forceES5 = !/\.tsx?$/.test(this.resourcePath);
+ } else if (esTarget >= ScriptTarget.ES2017) {
+ customOptions.forceAsyncTransformation = !/[\\\/]fesm2015[\\\/]/.test(this.resourcePath) && source.includes('async');
+ }
+ shouldProcess ||= customOptions.forceAsyncTransformation || customOptions.forceES5;
+ }
+
+ // Analyze for i18n inlining
+ if (
+ i18n &&
+ !/[\\\/]@angular[\\\/](?:compiler|localize)/.test(this.resourcePath) &&
+ source.includes('$localize')
+ ) {
+ customOptions.i18n = i18n as ApplicationPresetOptions['i18n'];
+ shouldProcess = true;
+ }
+
+ // Add provided loader options to default base options
+ const loaderOptions: Record = {
+ ...baseOptions,
+ ...rawOptions,
+ cacheIdentifier: JSON.stringify({
+ buildAngular: require('../../package.json').version,
+ customOptions,
+ baseOptions,
+ rawOptions,
+ }),
+ };
+
+ // Skip babel processing if no actions are needed
+ if (!shouldProcess) {
+ // Force the current file to be ignored
+ loaderOptions.ignore = [() => true];
+ }
+
+ return { custom: customOptions, loader: loaderOptions };
+ },
+ config(configuration, { customOptions }) {
+ return {
+ ...configuration.options,
+ // Workaround for https://github.com/babel/babel-loader/pull/896 is available
+ // Delete once the above PR is released
+ inputSourceMap: (configuration.options.inputSourceMap || false as {}), // Typings are not correct
+ presets: [
+ ...(configuration.options.presets || []),
+ [
+ require('./presets/application').default,
+ {
+ angularLinker: customOptions.shouldLink,
+ forceES5: customOptions.forceES5,
+ forceAsyncTransformation: customOptions.forceAsyncTransformation,
+ i18n: customOptions.i18n,
+ diagnosticReporter: (type, message) => {
+ switch (type) {
+ case 'error':
+ this.emitError(message);
+ break;
+ case 'info':
+ // Webpack does not currently have an informational diagnostic
+ case 'warning':
+ this.emitWarning(message);
+ break;
+ }
+ },
+ } as ApplicationPresetOptions,
+ ],
+ ],
+ };
+ },
+ };
+});
diff --git a/packages/angular_devkit/build_angular/src/browser/index.ts b/packages/angular_devkit/build_angular/src/browser/index.ts
index f71185d4644b..f406a666fa4c 100644
--- a/packages/angular_devkit/build_angular/src/browser/index.ts
+++ b/packages/angular_devkit/build_angular/src/browser/index.ts
@@ -1,16 +1,14 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import { BuilderContext, BuilderOutput, createBuilder } from '@angular-devkit/architect';
import { EmittedFiles, WebpackLoggingCallback, runWebpack } from '@angular-devkit/build-webpack';
-import { getSystemPath, json, normalize, resolve, tags, virtualFs } from '@angular-devkit/core';
-import { NodeJsSyncHost } from '@angular-devkit/core/node';
+import { getSystemPath, json, normalize, resolve, tags } from '@angular-devkit/core';
import * as fs from 'fs';
-import * as ora from 'ora';
import * as path from 'path';
import { Observable, from } from 'rxjs';
import { concatMap, map, switchMap } from 'rxjs/operators';
@@ -19,7 +17,6 @@ import * as webpack from 'webpack';
import { ExecutionTransformer } from '../transforms';
import {
BuildBrowserFeatures,
- NormalizedBrowserBuilderSchema,
deleteOutputDir,
normalizeAssetPatterns,
normalizeOptimization,
@@ -33,14 +30,13 @@ import { findCachePath } from '../utils/cache-path';
import { colors } from '../utils/color';
import { copyAssets } from '../utils/copy-assets';
import { cachingDisabled } from '../utils/environment-options';
+import { mkdir, writeFile } from '../utils/fs';
import { i18nInlineEmittedFiles } from '../utils/i18n-inlining';
import { I18nOptions } from '../utils/i18n-options';
-import { getHtmlTransforms } from '../utils/index-file/transforms';
-import {
- IndexHtmlTransform,
- writeIndexHtml,
-} from '../utils/index-file/write-index-html';
+import { FileInfo } from '../utils/index-file/augment-index-html';
+import { IndexHtmlGenerator, IndexHtmlTransform } from '../utils/index-file/index-html-generator';
import { ensureOutputPaths } from '../utils/output-paths';
+import { generateEntryPoints } from '../utils/package-chunk-sort';
import {
InlineOptions,
ProcessBundleFile,
@@ -49,14 +45,13 @@ import {
} from '../utils/process-bundle';
import { readTsconfig } from '../utils/read-tsconfig';
import { augmentAppWithServiceWorker } from '../utils/service-worker';
+import { Spinner } from '../utils/spinner';
import { assertCompatibleAngularVersion } from '../utils/version';
import {
- BrowserWebpackConfigOptions,
generateI18nBrowserWebpackConfigFromContext,
getIndexInputFile,
getIndexOutputFile,
} from '../utils/webpack-browser-config';
-import { isWebpackFiveOrHigher } from '../utils/webpack-version';
import {
getAotConfig,
getBrowserConfig,
@@ -65,20 +60,19 @@ import {
getStatsConfig,
getStylesConfig,
getWorkerConfig,
- normalizeExtraEntryPoints,
} from '../webpack/configs';
import { NgBuildAnalyticsPlugin } from '../webpack/plugins/analytics';
import { markAsyncChunksNonInitial } from '../webpack/utils/async-chunks';
+import { normalizeExtraEntryPoints } from '../webpack/utils/helpers';
import {
BundleStats,
- createWebpackLoggingCallback,
- generateBuildStats,
- generateBuildStatsTable,
+ ChunkType,
generateBundleStats,
statsErrorsToString,
statsHasErrors,
statsHasWarnings,
statsWarningsToString,
+ webpackStatsLogger,
} from '../webpack/utils/stats';
import { Schema as BrowserBuilderSchema } from './schema';
@@ -94,40 +88,7 @@ export type BrowserBuilderOutput = json.JsonObject &
outputPath: string;
};
-// todo: the below should be cleaned once dev-server support the new i18n
-interface ConfigFromContextReturn {
- config: webpack.Configuration;
- projectRoot: string;
- projectSourceRoot?: string;
- i18n: I18nOptions;
-}
-
-export async function buildBrowserWebpackConfigFromContext(
- options: BrowserBuilderSchema,
- context: BuilderContext,
- host: virtualFs.Host = new NodeJsSyncHost(),
- extraBuildOptions: Partial = {},
-): Promise {
- const webpackPartialGenerator = (wco: BrowserWebpackConfigOptions) => [
- getCommonConfig(wco),
- getBrowserConfig(wco),
- getStylesConfig(wco),
- getStatsConfig(wco),
- getAnalyticsConfig(wco, context),
- getCompilerConfig(wco),
- wco.buildOptions.webWorkerTsConfig ? getWorkerConfig(wco) : {},
- ];
-
- return generateI18nBrowserWebpackConfigFromContext(
- options,
- context,
- webpackPartialGenerator,
- host,
- extraBuildOptions,
- );
-}
-
-function getAnalyticsConfig(
+export function getAnalyticsConfig(
wco: WebpackConfigOptions,
context: BuilderContext,
): webpack.Configuration {
@@ -154,7 +115,7 @@ function getAnalyticsConfig(
return {};
}
-function getCompilerConfig(wco: WebpackConfigOptions): webpack.Configuration {
+export function getCompilerConfig(wco: WebpackConfigOptions): webpack.Configuration {
if (wco.buildOptions.main || wco.buildOptions.polyfills) {
return wco.buildOptions.aot ? getAotConfig(wco) : getNonAotConfig(wco);
}
@@ -165,8 +126,7 @@ function getCompilerConfig(wco: WebpackConfigOptions): webpack.Configuration {
async function initialize(
options: BrowserBuilderSchema,
context: BuilderContext,
- host: virtualFs.Host,
- differentialLoadingMode: boolean,
+ differentialLoadingNeeded: boolean,
webpackConfigurationTransform?: ExecutionTransformer,
): Promise<{
config: webpack.Configuration;
@@ -179,27 +139,30 @@ async function initialize(
// Assets are processed directly by the builder except when watching
const adjustedOptions = options.watch ? options : { ...options, assets: [] };
- // TODO_WEBPACK_5: Investigate build/serve issues with the `license-webpack-plugin` package
- if (adjustedOptions.extractLicenses && isWebpackFiveOrHigher()) {
- adjustedOptions.extractLicenses = false;
- context.logger.warn(
- 'Warning: License extraction is currently disabled when using Webpack 5. ' +
- 'This is temporary and will be corrected in a future update.',
- );
- }
-
const {
config,
projectRoot,
projectSourceRoot,
i18n,
- } = await buildBrowserWebpackConfigFromContext(adjustedOptions, context, host, { differentialLoadingMode });
+ } = await generateI18nBrowserWebpackConfigFromContext(
+ adjustedOptions,
+ context,
+ wco => [
+ getCommonConfig(wco),
+ getBrowserConfig(wco),
+ getStylesConfig(wco),
+ getStatsConfig(wco),
+ getAnalyticsConfig(wco, context),
+ getCompilerConfig(wco),
+ wco.buildOptions.webWorkerTsConfig ? getWorkerConfig(wco) : {},
+ ],
+ { differentialLoadingNeeded },
+ );
// Validate asset option values if processed directly
if (options.assets?.length && !adjustedOptions.assets?.length) {
normalizeAssetPatterns(
options.assets,
- new virtualFs.SyncDelegateHost(host),
normalize(context.workspaceRoot),
normalize(projectRoot),
projectSourceRoot === undefined ? undefined : normalize(projectSourceRoot),
@@ -232,7 +195,6 @@ export function buildWebpackBrowser(
indexHtml?: IndexHtmlTransform;
} = {},
): Observable {
- const host = new NodeJsSyncHost();
const root = normalize(context.workspaceRoot);
const projectName = context.target?.project;
@@ -251,14 +213,13 @@ export function buildWebpackBrowser(
switchMap(async projectMetadata => {
const sysProjectRoot = getSystemPath(
resolve(normalize(context.workspaceRoot),
- normalize((projectMetadata.root as string) ?? '')),
+ normalize((projectMetadata.root as string) ?? '')),
);
const { options: compilerOptions } = readTsconfig(options.tsConfig, context.workspaceRoot);
const target = compilerOptions.target || ScriptTarget.ES5;
const buildBrowserFeatures = new BuildBrowserFeatures(sysProjectRoot);
const isDifferentialLoadingNeeded = buildBrowserFeatures.isDifferentialLoadingNeeded(target);
- const differentialLoadingMode = !options.watch && isDifferentialLoadingNeeded;
if (target > ScriptTarget.ES2015 && isDifferentialLoadingNeeded) {
context.logger.warn(tags.stripIndent`
@@ -275,14 +236,14 @@ export function buildWebpackBrowser(
(hasIE9 ? 'IE 9' + (hasIE10 ? ' & ' : '') : '') + (hasIE10 ? 'IE 10' : '');
context.logger.warn(
`Warning: Support was requested for ${browsers} in the project's browserslist configuration. ` +
- (hasIE9 && hasIE10 ? 'These browsers are' : 'This browser is') +
- ' no longer officially supported with Angular v11 and higher.' +
- '\nFor additional information: https://v10.angular.io/guide/deprecations#ie-9-10-and-mobile',
+ (hasIE9 && hasIE10 ? 'These browsers are' : 'This browser is') +
+ ' no longer officially supported with Angular v11 and higher.' +
+ '\nFor additional information: https://v10.angular.io/guide/deprecations#ie-9-10-and-mobile',
);
}
return {
- ...(await initialize(options, context, host, differentialLoadingMode, transforms.webpackConfiguration)),
+ ...(await initialize(options, context, isDifferentialLoadingNeeded, transforms.webpackConfiguration)),
buildBrowserFeatures,
isDifferentialLoadingNeeded,
target,
@@ -290,39 +251,40 @@ export function buildWebpackBrowser(
}),
// tslint:disable-next-line: no-big-function
switchMap(({ config, projectRoot, projectSourceRoot, i18n, buildBrowserFeatures, isDifferentialLoadingNeeded, target }) => {
- const useBundleDownleveling = isDifferentialLoadingNeeded && !options.watch;
- const startTime = Date.now();
const normalizedOptimization = normalizeOptimization(options.optimization);
- const indexTransforms = getHtmlTransforms(
- normalizedOptimization,
- buildBrowserFeatures,
- transforms.indexHtml,
- );
return runWebpack(config, context, {
webpackFactory: require('webpack') as typeof webpack,
- logging:
- transforms.logging ||
- (useBundleDownleveling
- ? () => { }
- : createWebpackLoggingCallback(!!options.verbose, context.logger)),
+ logging: transforms.logging || (
+ (stats, config) => {
+ if (options.verbose) {
+ context.logger.info(stats.toString(config.stats));
+ }
+ }
+ ),
}).pipe(
// tslint:disable-next-line: no-big-function
concatMap(async buildEvent => {
+ const spinner = new Spinner();
+ spinner.enabled = options.progress !== false;
+
const { webpackStats: webpackRawStats, success, emittedFiles = [] } = buildEvent;
if (!webpackRawStats) {
throw new Error('Webpack stats build result is required.');
}
// Fix incorrectly set `initial` value on chunks.
- const extraEntryPoints = normalizeExtraEntryPoints(options.styles || [], 'styles')
- .concat(normalizeExtraEntryPoints(options.scripts || [], 'scripts'));
+ const extraEntryPoints = [
+ ...normalizeExtraEntryPoints(options.styles || [], 'styles'),
+ ...normalizeExtraEntryPoints(options.scripts || [], 'scripts'),
+ ];
+
const webpackStats = {
...webpackRawStats,
chunks: markAsyncChunksNonInitial(webpackRawStats, extraEntryPoints),
};
- if (!success && useBundleDownleveling) {
+ if (!success) {
// If using bundle downleveling then there is only one build
// If it fails show any diagnostic messages and bail
if (statsHasWarnings(webpackStats)) {
@@ -333,7 +295,9 @@ export function buildWebpackBrowser(
}
return { success };
- } else if (success) {
+ } else {
+ const processResults: ProcessBundleResult[] = [];
+ const bundleInfoStats: BundleStats[] = [];
outputPaths = ensureOutputPaths(baseOutputPath, i18n);
let noModuleFiles: EmittedFiles[] | undefined;
@@ -482,7 +446,6 @@ export function buildWebpackBrowser(
const processActions: typeof actions = [];
let processRuntimeAction: ProcessBundleOptions | undefined;
- const processResults: ProcessBundleResult[] = [];
for (const action of actions) {
// If SRI is enabled always process the runtime bundle
// Lazy route integrity values are stored in the runtime bundle
@@ -500,7 +463,7 @@ export function buildWebpackBrowser(
// Execute the bundle processing actions
try {
- const dlSpinner = ora('Generating ES5 bundles for differential loading...').start();
+ spinner.start('Generating ES5 bundles for differential loading...');
for await (const result of executor.processAll(processActions)) {
processResults.push(result);
}
@@ -517,11 +480,10 @@ export function buildWebpackBrowser(
);
}
- dlSpinner.succeed('ES5 bundle generation complete.');
+ spinner.succeed('ES5 bundle generation complete.');
if (i18n.shouldInline) {
- const spinner = ora('Generating localized bundles...').start();
-
+ spinner.start('Generating localized bundles...');
const inlineActions: InlineOptions[] = [];
const processedFiles = new Set();
for (const result of processResults) {
@@ -599,13 +561,13 @@ export function buildWebpackBrowser(
'',
);
} catch (err) {
- spinner.fail(colors.redBright('Localized bundle generation failed.'));
+ spinner.fail('Localized bundle generation failed.');
return { success: false, error: mapErrorToMessage(err) };
}
if (hasErrors) {
- spinner.fail(colors.redBright('Localized bundle generation failed.'));
+ spinner.fail('Localized bundle generation failed.');
} else {
spinner.succeed('Localized bundle generation complete.');
}
@@ -617,35 +579,15 @@ export function buildWebpackBrowser(
} finally {
executor.stop();
}
-
- type ArrayElement = A extends ReadonlyArray ? T : never;
- function generateBundleInfoStats(
- bundle: ProcessBundleFile,
- chunk: ArrayElement | undefined,
- ): BundleStats {
- return generateBundleStats(
- {
- size: bundle.size,
- files: bundle.map ? [bundle.filename, bundle.map.filename] : [bundle.filename],
- names: chunk?.names,
- entry: !!chunk?.names.includes('runtime'),
- initial: !!chunk?.initial,
- rendered: true,
- },
- true,
- );
- }
-
- const bundleInfoStats: BundleStats[] = [];
for (const result of processResults) {
const chunk = webpackStats.chunks?.find((chunk) => chunk.id.toString() === result.name);
if (result.original) {
- bundleInfoStats.push(generateBundleInfoStats(result.original, chunk));
+ bundleInfoStats.push(generateBundleInfoStats(result.original, chunk, 'modern'));
}
if (result.downlevel) {
- bundleInfoStats.push(generateBundleInfoStats(result.downlevel, chunk));
+ bundleInfoStats.push(generateBundleInfoStats(result.downlevel, chunk, 'legacy'));
}
}
@@ -654,43 +596,7 @@ export function buildWebpackBrowser(
) || [];
for (const chunk of unprocessedChunks) {
const asset = webpackStats.assets?.find(a => a.name === chunk.files[0]);
- bundleInfoStats.push(generateBundleStats({ ...chunk, size: asset?.size }, true));
- }
-
- context.logger.info(
- '\n' +
- generateBuildStatsTable(bundleInfoStats, colors.enabled) +
- '\n\n' +
- generateBuildStats(
- webpackStats?.hash || '',
- Date.now() - startTime,
- true,
- ),
- );
-
- // Check for budget errors and display them to the user.
- const budgets = options.budgets || [];
- const budgetFailures = checkBudgets(budgets, webpackStats, processResults);
- for (const { severity, message } of budgetFailures) {
- switch (severity) {
- case ThresholdSeverity.Warning:
- webpackStats.warnings.push(message);
- break;
- case ThresholdSeverity.Error:
- webpackStats.errors.push(message);
- break;
- default:
- assertNever(severity);
- }
- }
-
- if (statsHasWarnings(webpackStats)) {
- context.logger.warn(statsWarningsToString(webpackStats, { colors: true }));
- }
- if (statsHasErrors(webpackStats)) {
- context.logger.error(statsErrorsToString(webpackStats, { colors: true }));
-
- return { success: false };
+ bundleInfoStats.push(generateBundleStats({ ...chunk, size: asset?.size }));
}
} else {
files = emittedFiles.filter(x => x.name !== 'polyfills-es5');
@@ -714,72 +620,126 @@ export function buildWebpackBrowser(
}
}
- // Copy assets
- if (!options.watch && options.assets?.length) {
- try {
- await copyAssets(
- normalizeAssetPatterns(
- options.assets,
- new virtualFs.SyncDelegateHost(host),
- root,
- normalize(projectRoot),
- projectSourceRoot === undefined ? undefined : normalize(projectSourceRoot),
- ),
- Array.from(outputPaths.values()),
- context.workspaceRoot,
- );
- } catch (err) {
- return { success: false, error: 'Unable to copy assets: ' + err.message };
+ // Check for budget errors and display them to the user.
+ const budgets = options.budgets;
+ if (budgets?.length) {
+ const budgetFailures = checkBudgets(budgets, webpackStats, processResults);
+ for (const { severity, message } of budgetFailures) {
+ switch (severity) {
+ case ThresholdSeverity.Warning:
+ webpackStats.warnings.push(message);
+ break;
+ case ThresholdSeverity.Error:
+ webpackStats.errors.push(message);
+ break;
+ default:
+ assertNever(severity);
+ }
}
}
- for (const [locale, outputPath] of outputPaths.entries()) {
- let localeBaseHref;
- if (i18n.locales[locale] && i18n.locales[locale].baseHref !== '') {
- localeBaseHref = urlJoin(
- options.baseHref || '',
- i18n.locales[locale].baseHref ?? `/${locale}/`,
- );
+ const buildSuccess = success && !statsHasErrors(webpackStats);
+ if (buildSuccess) {
+ // Copy assets
+ if (!options.watch && options.assets?.length) {
+ spinner.start('Copying assets...');
+ try {
+ await copyAssets(
+ normalizeAssetPatterns(
+ options.assets,
+ root,
+ normalize(projectRoot),
+ projectSourceRoot === undefined ? undefined : normalize(projectSourceRoot),
+ ),
+ Array.from(outputPaths.values()),
+ context.workspaceRoot,
+ );
+ spinner.succeed('Copying assets complete.');
+ } catch (err) {
+ spinner.fail(colors.redBright('Copying of assets failed.'));
+
+ return { success: false, error: 'Unable to copy assets: ' + err.message };
+ }
}
- try {
- if (options.index) {
- await writeIndexHtml({
- host,
- outputPath: path.join(outputPath, getIndexOutputFile(options)),
- indexPath: path.join(context.workspaceRoot, getIndexInputFile(options)),
- files,
- noModuleFiles,
- moduleFiles,
- baseHref: localeBaseHref || options.baseHref,
- deployUrl: options.deployUrl,
- sri: options.subresourceIntegrity,
- scripts: options.scripts,
- styles: options.styles,
- postTransforms: indexTransforms,
- crossOrigin: options.crossOrigin,
- // i18nLocale is used when Ivy is disabled
- lang: locale || options.i18nLocale,
- });
+ if (options.index) {
+ spinner.start('Generating index html...');
+
+ const WOFFSupportNeeded = !buildBrowserFeatures.isFeatureSupported('woff2');
+ const entrypoints = generateEntryPoints({
+ scripts: options.scripts ?? [],
+ styles: options.styles ?? [],
+ });
+
+ const indexHtmlGenerator = new IndexHtmlGenerator({
+ indexPath: path.join(context.workspaceRoot, getIndexInputFile(options.index)),
+ entrypoints,
+ deployUrl: options.deployUrl,
+ sri: options.subresourceIntegrity,
+ WOFFSupportNeeded,
+ optimization: normalizedOptimization,
+ crossOrigin: options.crossOrigin,
+ postTransform: transforms.indexHtml,
+ });
+
+ for (const [locale, outputPath] of outputPaths.entries()) {
+ try {
+ const { content, warnings, errors } = await indexHtmlGenerator.process({
+ baseHref: getLocaleBaseHref(i18n, locale) || options.baseHref,
+ // i18nLocale is used when Ivy is disabled
+ lang: locale || options.i18nLocale,
+ outputPath,
+ files: mapEmittedFilesToFileInfo(files),
+ noModuleFiles: mapEmittedFilesToFileInfo(noModuleFiles),
+ moduleFiles: mapEmittedFilesToFileInfo(moduleFiles),
+ });
+
+ if (warnings.length || errors.length) {
+ spinner.stop();
+ warnings.forEach(m => context.logger.warn(m));
+ errors.forEach(m => context.logger.error(m));
+ spinner.start();
+ }
+
+ const indexOutput = path.join(outputPath, getIndexOutputFile(options.index));
+ await mkdir(path.dirname(indexOutput), { recursive: true });
+ await writeFile(indexOutput, content);
+ } catch (error) {
+ spinner.fail('Index html generation failed.');
+
+ return { success: false, error: mapErrorToMessage(error) };
+ }
}
- if (options.serviceWorker) {
- await augmentAppWithServiceWorker(
- host,
- root,
- normalize(projectRoot),
- normalize(outputPath),
- localeBaseHref || options.baseHref || '/',
- options.ngswConfigPath,
- );
+ spinner.succeed('Index html generation complete.');
+ }
+
+ if (options.serviceWorker) {
+ spinner.start('Generating service worker...');
+ for (const [locale, outputPath] of outputPaths.entries()) {
+ try {
+ await augmentAppWithServiceWorker(
+ root,
+ normalize(projectRoot),
+ normalize(outputPath),
+ getLocaleBaseHref(i18n, locale) || options.baseHref || '/',
+ options.ngswConfigPath,
+ );
+ } catch (error) {
+ spinner.fail('Service worker generation failed.');
+
+ return { success: false, error: mapErrorToMessage(error) };
+ }
}
- } catch (err) {
- return { success: false, error: mapErrorToMessage(err) };
+
+ spinner.succeed('Service worker generation complete.');
}
}
- }
- return { success };
+ webpackStatsLogger(context.logger, webpackStats, config, bundleInfoStats);
+
+ return { success: buildSuccess };
+ }
}),
map(
event =>
@@ -791,8 +751,19 @@ export function buildWebpackBrowser(
} as BrowserBuilderOutput),
),
);
- }),
- );
+ }),
+ );
+
+ function getLocaleBaseHref(i18n: I18nOptions, locale: string): string | undefined {
+ if (i18n.locales[locale] && i18n.locales[locale]?.baseHref !== '') {
+ return urlJoin(
+ options.baseHref || '',
+ i18n.locales[locale].baseHref ?? `/${locale}/`,
+ );
+ }
+
+ return undefined;
+ }
}
function mapErrorToMessage(error: unknown): string | undefined {
@@ -808,8 +779,37 @@ function mapErrorToMessage(error: unknown): string | undefined {
}
function assertNever(input: never): never {
- throw new Error(`Unexpected call to assertNever() with input: ${
- JSON.stringify(input, null /* replacer */, 4 /* tabSize */)}`);
+ throw new Error(`Unexpected call to assertNever() with input: ${JSON.stringify(input, null /* replacer */, 4 /* tabSize */)}`);
+}
+
+type ArrayElement = A extends ReadonlyArray ? T : never;
+function generateBundleInfoStats(
+ bundle: ProcessBundleFile,
+ chunk: ArrayElement | undefined,
+ chunkType: ChunkType,
+): BundleStats {
+ return generateBundleStats(
+ {
+ size: bundle.size,
+ files: bundle.map ? [bundle.filename, bundle.map.filename] : [bundle.filename],
+ names: chunk?.names,
+ entry: !!chunk?.names.includes('runtime'),
+ initial: !!chunk?.initial,
+ rendered: true,
+ chunkType,
+ },
+ );
+}
+
+function mapEmittedFilesToFileInfo(files: EmittedFiles[] = []): FileInfo[] {
+ const filteredFiles: FileInfo[] = [];
+ for (const { file, name, extension, initial } of files) {
+ if (name && initial) {
+ filteredFiles.push({ file, extension, name });
+ }
+ }
+
+ return filteredFiles;
}
export default createBuilder(buildWebpackBrowser);
diff --git a/packages/angular_devkit/build_angular/src/browser/schema.json b/packages/angular_devkit/build_angular/src/browser/schema.json
index 628b5dcd8dd8..ef16e332d1a3 100644
--- a/packages/angular_devkit/build_angular/src/browser/schema.json
+++ b/packages/angular_devkit/build_angular/src/browser/schema.json
@@ -57,7 +57,7 @@
"additionalProperties": false
},
"optimization": {
- "description": "Enables optimization of the build output.",
+ "description": "Enables optimization of the build output. Including minification of scripts and styles, tree-shaking, dead-code elimination, inlining of critical CSS and fonts inlining. For more information, see https://angular.io/guide/workspace-config#optimization-configuration.",
"x-user-analytics": 16,
"default": false,
"oneOf": [
@@ -70,12 +70,32 @@
"default": true
},
"styles": {
- "type": "boolean",
"description": "Enables optimization of the styles output.",
- "default": true
+ "default": true,
+ "oneOf": [
+ {
+ "type": "object",
+ "properties": {
+ "minify": {
+ "type": "boolean",
+ "description": "Minify CSS definitions by removing extraneous whitespace and comments, merging identifiers and minimizing values.",
+ "default": true
+ },
+ "inlineCritical": {
+ "type": "boolean",
+ "description": "Extract and inline critical CSS definitions to improve first paint time.",
+ "default": false
+ }
+ },
+ "additionalProperties": false
+ },
+ {
+ "type": "boolean"
+ }
+ ]
},
"fonts": {
- "description": "Enables optimization for fonts. This requires internet access.",
+ "description": "Enables optimization for fonts. This option requires internet access. `HTTPS_PROXY` environment variable can be used to specify a proxy server.",
"default": true,
"oneOf": [
{
@@ -83,7 +103,7 @@
"properties": {
"inline": {
"type": "boolean",
- "description": "Reduce render blocking requests by inlining external fonts in the application's HTML index file. This requires internet access.",
+ "description": "Reduce render blocking requests by inlining external Google fonts and icons CSS definitions in the application's HTML index file. This option requires internet access. `HTTPS_PROXY` environment variable can be used to specify a proxy server.",
"default": true
}
},
@@ -126,7 +146,7 @@
"default": false
},
"sourceMap": {
- "description": "Output sourcemaps.",
+ "description": "Output source maps for scripts and styles. For more information, see https://angular.io/guide/workspace-config#source-map-configuration.",
"default": true,
"oneOf": [
{
@@ -134,22 +154,22 @@
"properties": {
"scripts": {
"type": "boolean",
- "description": "Output sourcemaps for all scripts.",
+ "description": "Output source maps for all scripts.",
"default": true
},
"styles": {
"type": "boolean",
- "description": "Output sourcemaps for all styles.",
+ "description": "Output source maps for all styles.",
"default": true
},
"hidden": {
"type": "boolean",
- "description": "Output sourcemaps used for error reporting tools.",
+ "description": "Output source maps used for error reporting tools.",
"default": false
},
"vendor": {
"type": "boolean",
- "description": "Resolve vendor packages sourcemaps.",
+ "description": "Resolve vendor packages source maps.",
"default": false
}
},
@@ -162,12 +182,12 @@
},
"vendorChunk": {
"type": "boolean",
- "description": "Use a separate bundle containing only vendor libraries.",
+ "description": "Generate a seperate bundle containing only vendor libraries. This option should only used for development.",
"default": true
},
"commonChunk": {
"type": "boolean",
- "description": "Use a separate bundle containing code used across multiple bundles.",
+ "description": "Generate a seperate bundle containing code used across multiple bundles.",
"default": true
},
"baseHref": {
@@ -185,7 +205,8 @@
},
"progress": {
"type": "boolean",
- "description": "Log progress to the console while building."
+ "description": "Log progress to the console while building.",
+ "default": true
},
"i18nFile": {
"type": "string",
@@ -209,6 +230,7 @@
"default": "warning"
},
"localize": {
+ "description": "Translate the bundles in one or more locales.",
"oneOf": [
{
"type": "boolean",
@@ -348,12 +370,6 @@
},
"default": []
},
- "rebaseRootRelativeCssUrls": {
- "description": "Change root relative URLs in stylesheets to include base HREF and deploy URL. Use only for compatibility and transition. The behavior of this option is non-standard and will be removed in the next major release.",
- "type": "boolean",
- "default": false,
- "x-deprecated": true
- },
"webWorkerTsConfig": {
"type": "string",
"description": "TypeScript configuration for Web Worker modules."
@@ -395,6 +411,11 @@
{
"type": "object",
"properties": {
+ "followSymlinks": {
+ "type": "boolean",
+ "default": false,
+ "description": "Allow glob patterns to follow symlink directories. This allows subdirectories of the symlink to be searched."
+ },
"glob": {
"type": "string",
"description": "The pattern to match."
@@ -433,10 +454,12 @@
"type": "object",
"properties": {
"src": {
- "type": "string"
+ "type": "string",
+ "pattern": "\\.(([cm]?j|t)sx?|json)$"
},
"replaceWith": {
- "type": "string"
+ "type": "string",
+ "pattern": "\\.(([cm]?j|t)sx?|json)$"
}
},
"additionalProperties": false,
@@ -449,10 +472,12 @@
"type": "object",
"properties": {
"replace": {
- "type": "string"
+ "type": "string",
+ "pattern": "\\.(([cm]?j|t)sx?|json)$"
},
"with": {
- "type": "string"
+ "type": "string",
+ "pattern": "\\.(([cm]?j|t)sx?|json)$"
}
},
"additionalProperties": false,
@@ -474,6 +499,7 @@
},
"bundleName": {
"type": "string",
+ "pattern": "^[\\w\\-.]*$",
"description": "The bundle name for this extra entry point."
},
"inject": {
diff --git a/packages/angular_devkit/build_angular/src/browser/specs/allow-js_spec.ts b/packages/angular_devkit/build_angular/src/browser/specs/allow-js_spec.ts
index 0952c61b3f0a..e118ea340896 100644
--- a/packages/angular_devkit/build_angular/src/browser/specs/allow-js_spec.ts
+++ b/packages/angular_devkit/build_angular/src/browser/specs/allow-js_spec.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
diff --git a/packages/angular_devkit/build_angular/src/browser/specs/aot_spec.ts b/packages/angular_devkit/build_angular/src/browser/specs/aot_spec.ts
index 51e855b10f71..5f6ab2a32012 100644
--- a/packages/angular_devkit/build_angular/src/browser/specs/aot_spec.ts
+++ b/packages/angular_devkit/build_angular/src/browser/specs/aot_spec.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
diff --git a/packages/angular_devkit/build_angular/src/browser/specs/assets_spec.ts b/packages/angular_devkit/build_angular/src/browser/specs/assets_spec.ts
index 1e2483bd4ec6..2ec91d854c14 100644
--- a/packages/angular_devkit/build_angular/src/browser/specs/assets_spec.ts
+++ b/packages/angular_devkit/build_angular/src/browser/specs/assets_spec.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
diff --git a/packages/angular_devkit/build_angular/src/browser/specs/base-href_spec.ts b/packages/angular_devkit/build_angular/src/browser/specs/base-href_spec.ts
index 5f1db812ab0d..7dcf3d1ac438 100644
--- a/packages/angular_devkit/build_angular/src/browser/specs/base-href_spec.ts
+++ b/packages/angular_devkit/build_angular/src/browser/specs/base-href_spec.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
diff --git a/packages/angular_devkit/build_angular/src/browser/specs/browser-support_spec.ts b/packages/angular_devkit/build_angular/src/browser/specs/browser-support_spec.ts
index 58ff90668a6e..f2935305cd2e 100644
--- a/packages/angular_devkit/build_angular/src/browser/specs/browser-support_spec.ts
+++ b/packages/angular_devkit/build_angular/src/browser/specs/browser-support_spec.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
diff --git a/packages/angular_devkit/build_angular/src/browser/specs/build-optimizer_spec.ts b/packages/angular_devkit/build_angular/src/browser/specs/build-optimizer_spec.ts
index e5c88b01f68e..c80d88910ff1 100644
--- a/packages/angular_devkit/build_angular/src/browser/specs/build-optimizer_spec.ts
+++ b/packages/angular_devkit/build_angular/src/browser/specs/build-optimizer_spec.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
diff --git a/packages/angular_devkit/build_angular/src/browser/specs/bundle-budgets_spec.ts b/packages/angular_devkit/build_angular/src/browser/specs/bundle-budgets_spec.ts
index 3eaed7bb7d63..3016a7881c88 100644
--- a/packages/angular_devkit/build_angular/src/browser/specs/bundle-budgets_spec.ts
+++ b/packages/angular_devkit/build_angular/src/browser/specs/bundle-budgets_spec.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
@@ -99,7 +99,6 @@ describe('Browser Builder bundle budgets', () => {
const run = await architect.scheduleTarget(targetSpec, overrides, { logger });
const output = await run.result;
expect(output.success).toBe(true);
- expect(logs.length).toBe(2);
expect(logs.join()).toMatch(`Warning.+app\.component\.${ext}`);
await run.stop();
});
@@ -139,7 +138,6 @@ describe('Browser Builder bundle budgets', () => {
const run = await architect.scheduleTarget(targetSpec, overrides, { logger });
const output = await run.result;
expect(output.success).toBe(false);
- expect(logs.length).toBe(2);
expect(logs.join()).toMatch(`Error.+app\.component\.${ext}`);
await run.stop();
});
diff --git a/packages/angular_devkit/build_angular/src/browser/specs/circular-dependency_spec.ts b/packages/angular_devkit/build_angular/src/browser/specs/circular-dependency_spec.ts
index ecd710e25841..474203bb5361 100644
--- a/packages/angular_devkit/build_angular/src/browser/specs/circular-dependency_spec.ts
+++ b/packages/angular_devkit/build_angular/src/browser/specs/circular-dependency_spec.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
diff --git a/packages/angular_devkit/build_angular/src/browser/specs/common-js-warning_spec.ts b/packages/angular_devkit/build_angular/src/browser/specs/common-js-warning_spec.ts
deleted file mode 100644
index 9d469531a7c6..000000000000
--- a/packages/angular_devkit/build_angular/src/browser/specs/common-js-warning_spec.ts
+++ /dev/null
@@ -1,123 +0,0 @@
-/**
- * @license
- * Copyright Google Inc. All Rights Reserved.
- *
- * Use of this source code is governed by an MIT-style license that can be
- * found in the LICENSE file at https://angular.io/license
- */
-import { Architect } from '@angular-devkit/architect';
-import { logging } from '@angular-devkit/core';
-import { createArchitect, host } from '../../test-utils';
-
-describe('Browser Builder commonjs warning', () => {
- const targetSpec = { project: 'app', target: 'build' };
-
- let architect: Architect;
- let logger: logging.Logger;
- let logs: string[];
-
- beforeEach(async () => {
- await host.initialize().toPromise();
- architect = (await createArchitect(host.root())).architect;
-
- // Create logger
- logger = new logging.Logger('');
- logs = [];
- logger.subscribe(e => logs.push(e.message));
- });
-
- afterEach(async () => host.restore().toPromise());
-
- for (const aot of [true, false]) {
- it(`should not show warning for styles import in ${aot ? 'AOT' : 'JIT'} Mode`, async () => {
- // Add a Common JS dependency
- host.appendToFile('src/app/app.component.ts', `
- import '../../test.css';
- `);
-
- host.writeMultipleFiles({
- './test.css': `
- body {
- color: red;
- };
- `,
- });
-
- const run = await architect.scheduleTarget(targetSpec, { aot }, { logger });
- const output = await run.result;
- expect(output.success).toBe(true);
- expect(logs.join()).not.toContain('Warning');
- await run.stop();
- });
-
- it(`should show warning when depending on a Common JS bundle in ${aot ? 'AOT' : 'JIT'} Mode`, async () => {
- // Add a Common JS dependency
- host.appendToFile('src/app/app.component.ts', `
- import 'bootstrap';
- `);
-
- const run = await architect.scheduleTarget(targetSpec, { aot }, { logger });
- const output = await run.result;
- expect(output.success).toBe(true);
- const logMsg = logs.join();
- expect(logMsg).toMatch(/Warning: .+app\.component\.ts depends on 'bootstrap'\. CommonJS or AMD dependencies/);
- expect(logMsg).not.toContain('jquery', 'Should not warn on transitive CommonJS packages which parent is also CommonJS.');
- await run.stop();
- });
- }
-
- it('should not show warning when depending on a Common JS bundle which is allowed', async () => {
- // Add a Common JS dependency
- host.appendToFile('src/app/app.component.ts', `
- import 'bootstrap';
- import 'zone.js/dist/zone-error';
- `);
-
- const overrides = {
- allowedCommonJsDependencies: [
- 'bootstrap',
- 'zone.js',
- ],
- };
-
- const run = await architect.scheduleTarget(targetSpec, overrides, { logger });
- const output = await run.result;
- expect(output.success).toBe(true);
- expect(logs.join()).not.toContain('Warning');
- await run.stop();
- });
-
- it(`should not show warning when importing non global local data '@angular/common/locale/fr'`, async () => {
- // Add a Common JS dependency
- host.appendToFile('src/app/app.component.ts', `
- import '@angular/common/locales/fr';
- `);
-
- const run = await architect.scheduleTarget(targetSpec, undefined, { logger });
- const output = await run.result;
- expect(output.success).toBe(true);
-
- expect(logs.join()).not.toContain('Warning');
- await run.stop();
- });
-
- it('should not show warning in JIT for templateUrl and styleUrl when using paths', async () => {
- host.replaceInFile('tsconfig.json', /"baseUrl": ".\/",/, `
- "baseUrl": "./",
- "paths": {
- "@app/*": [
- "src/app/*"
- ]
- },
- `);
-
- host.replaceInFile('src/app/app.module.ts', './app.component', '@app/app.component');
-
- const run = await architect.scheduleTarget(targetSpec, { aot: false }, { logger });
- const output = await run.result;
- expect(output.success).toBe(true);
-
- expect(logs.join()).not.toContain('WARNING');
- await run.stop();
- });
-});
diff --git a/packages/angular_devkit/build_angular/src/browser/specs/cross-origin_spec.ts b/packages/angular_devkit/build_angular/src/browser/specs/cross-origin_spec.ts
index 65f4684caf48..bf83f20cfb18 100644
--- a/packages/angular_devkit/build_angular/src/browser/specs/cross-origin_spec.ts
+++ b/packages/angular_devkit/build_angular/src/browser/specs/cross-origin_spec.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
diff --git a/packages/angular_devkit/build_angular/src/browser/specs/deploy-url_spec.ts b/packages/angular_devkit/build_angular/src/browser/specs/deploy-url_spec.ts
index 11205efaca1b..6392bc351317 100644
--- a/packages/angular_devkit/build_angular/src/browser/specs/deploy-url_spec.ts
+++ b/packages/angular_devkit/build_angular/src/browser/specs/deploy-url_spec.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
diff --git a/packages/angular_devkit/build_angular/src/browser/specs/differential_loading_spec.ts b/packages/angular_devkit/build_angular/src/browser/specs/differential_loading_spec.ts
index 293c8d3a11b7..f8039bfe57e7 100644
--- a/packages/angular_devkit/build_angular/src/browser/specs/differential_loading_spec.ts
+++ b/packages/angular_devkit/build_angular/src/browser/specs/differential_loading_spec.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
@@ -50,7 +50,9 @@ describe('Browser Builder with differential loading', () => {
'runtime-es5.js.map',
'vendor-es2015.js',
+ 'vendor-es2015.js.map',
'vendor-es5.js',
+ 'vendor-es5.js.map',
'styles.css',
'styles.css.map',
@@ -88,7 +90,9 @@ describe('Browser Builder with differential loading', () => {
'runtime-es5.js.map',
'vendor-es2016.js',
+ 'vendor-es2016.js.map',
'vendor-es5.js',
+ 'vendor-es5.js.map',
'styles.css',
'styles.css.map',
@@ -126,7 +130,9 @@ describe('Browser Builder with differential loading', () => {
'runtime-es5.js.map',
'vendor-esnext.js',
+ 'vendor-esnext.js.map',
'vendor-es5.js',
+ 'vendor-es5.js.map',
'styles.css',
'styles.css.map',
@@ -142,16 +148,17 @@ describe('Browser Builder with differential loading', () => {
'favicon.ico',
'index.html',
- 'main.js',
- 'main.js.map',
+ 'main-es2015.js',
+ 'main-es2015.js.map',
- 'polyfills.js',
- 'polyfills.js.map',
+ 'polyfills-es2015.js',
+ 'polyfills-es2015.js.map',
- 'runtime.js',
- 'runtime.js.map',
+ 'runtime-es2015.js',
+ 'runtime-es2015.js.map',
- 'vendor.js',
+ 'vendor-es2015.js',
+ 'vendor-es2015.js.map',
'styles.css',
'styles.css.map',
@@ -195,10 +202,10 @@ describe('Browser Builder with differential loading', () => {
const { files } = await browserBuild(architect, host, target, { watch: true });
expect(await files['index.html']).toContain(
- '' +
- '' +
- '' +
- '',
+ '' +
+ '' +
+ '' +
+ '',
);
});
});
diff --git a/packages/angular_devkit/build_angular/src/browser/specs/errors_spec.ts b/packages/angular_devkit/build_angular/src/browser/specs/errors_spec.ts
index 8d9a44a36b41..67dd8d71276d 100644
--- a/packages/angular_devkit/build_angular/src/browser/specs/errors_spec.ts
+++ b/packages/angular_devkit/build_angular/src/browser/specs/errors_spec.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
diff --git a/packages/angular_devkit/build_angular/src/browser/specs/font-optimization_spec.ts b/packages/angular_devkit/build_angular/src/browser/specs/font-optimization_spec.ts
index 8075059db024..e448cca3a8bd 100644
--- a/packages/angular_devkit/build_angular/src/browser/specs/font-optimization_spec.ts
+++ b/packages/angular_devkit/build_angular/src/browser/specs/font-optimization_spec.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
diff --git a/packages/angular_devkit/build_angular/src/browser/specs/i18n_spec.ts b/packages/angular_devkit/build_angular/src/browser/specs/i18n_spec.ts
index 669a3bc6384c..12f6203726f1 100644
--- a/packages/angular_devkit/build_angular/src/browser/specs/i18n_spec.ts
+++ b/packages/angular_devkit/build_angular/src/browser/specs/i18n_spec.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
diff --git a/packages/angular_devkit/build_angular/src/browser/specs/index_spec.ts b/packages/angular_devkit/build_angular/src/browser/specs/index_spec.ts
index f74d864b4ab2..b8a5ecd61d49 100644
--- a/packages/angular_devkit/build_angular/src/browser/specs/index_spec.ts
+++ b/packages/angular_devkit/build_angular/src/browser/specs/index_spec.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
diff --git a/packages/angular_devkit/build_angular/src/browser/specs/inline-critical-css-optimization_spec.ts b/packages/angular_devkit/build_angular/src/browser/specs/inline-critical-css-optimization_spec.ts
new file mode 100644
index 000000000000..7782b288d61d
--- /dev/null
+++ b/packages/angular_devkit/build_angular/src/browser/specs/inline-critical-css-optimization_spec.ts
@@ -0,0 +1,58 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+import { Architect } from '@angular-devkit/architect';
+import { browserBuild, createArchitect, host } from '../../test-utils';
+
+describe('Browser Builder inline critical CSS optimization', () => {
+ const target = { project: 'app', target: 'build' };
+ const overrides = {
+ optimization: {
+ scripts: false,
+ styles: {
+ minify: true,
+ inlineCritical: true,
+ },
+ fonts: false,
+ },
+ };
+
+ let architect: Architect;
+
+ beforeEach(async () => {
+ await host.initialize().toPromise();
+ architect = (await createArchitect(host.root())).architect;
+ host.writeMultipleFiles({
+ 'src/styles.css': `
+ body { color: #000 }
+ `,
+ });
+ });
+
+ afterEach(async () => host.restore().toPromise());
+
+ it('works', async () => {
+ const { files } = await browserBuild(architect, host, target, overrides);
+ const html = await files['index.html'];
+ expect(html).toContain(` `);
+ expect(html).toContain(`body{color:#000}`);
+ });
+
+ it('works with deployUrl', async () => {
+ const { files } = await browserBuild(architect, host, target, { ...overrides, deployUrl: 'http://cdn.com/' });
+ const html = await files['index.html'];
+ expect(html).toContain(` `);
+ expect(html).toContain(`body{color:#000}`);
+ });
+
+ it('should not inline critical css when option is disabled', async () => {
+ const { files } = await browserBuild(architect, host, target, { optimization: false });
+ const html = await files['index.html'];
+ expect(html).toContain(` `);
+ expect(html).not.toContain(`body{color:#000}`);
+ });
+});
diff --git a/packages/angular_devkit/build_angular/src/browser/specs/lazy-module_spec.ts b/packages/angular_devkit/build_angular/src/browser/specs/lazy-module_spec.ts
index ffb07abf2026..b00f7796e1c4 100644
--- a/packages/angular_devkit/build_angular/src/browser/specs/lazy-module_spec.ts
+++ b/packages/angular_devkit/build_angular/src/browser/specs/lazy-module_spec.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
@@ -9,7 +9,7 @@
import { Architect } from '@angular-devkit/architect';
import { TestProjectHost } from '@angular-devkit/architect/testing';
import { logging } from '@angular-devkit/core';
-import { take, tap, timeout } from 'rxjs/operators';
+import { debounceTime, take, tap } from 'rxjs/operators';
import {
browserBuild,
createArchitect,
@@ -117,7 +117,7 @@ describe('Browser Builder lazy modules', () => {
const run = await architect.scheduleTarget(target, overrides);
await run.output
.pipe(
- timeout(15000),
+ debounceTime(3000),
tap(buildEvent => {
buildNumber++;
switch (buildNumber) {
diff --git a/packages/angular_devkit/build_angular/src/browser/specs/license-extraction_spec.ts b/packages/angular_devkit/build_angular/src/browser/specs/license-extraction_spec.ts
deleted file mode 100644
index 056d6449a6fd..000000000000
--- a/packages/angular_devkit/build_angular/src/browser/specs/license-extraction_spec.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-/**
- * @license
- * Copyright Google Inc. All Rights Reserved.
- *
- * Use of this source code is governed by an MIT-style license that can be
- * found in the LICENSE file at https://angular.io/license
- */
-import { Architect } from '@angular-devkit/architect';
-import { browserBuild, createArchitect, host } from '../../test-utils';
-
-
-describe('Browser Builder license extraction', () => {
- const target = { project: 'app', target: 'build' };
- let architect: Architect;
-
- beforeEach(async () => {
- await host.initialize().toPromise();
- architect = (await createArchitect(host.root())).architect;
- });
- afterEach(async () => host.restore().toPromise());
-
- // Ignored because license works when trying manually on a project, but doesn't work here.
- it('works', async () => {
- // TODO: make license extraction independent from optimization level.
- const overrides = { extractLicenses: true, optimization: true };
-
- const { files } = await browserBuild(architect, host, target, overrides);
- expect(await files['3rdpartylicenses.txt']).not.toBeUndefined();
- });
-});
diff --git a/packages/angular_devkit/build_angular/src/browser/specs/no-entry-module_spec.ts b/packages/angular_devkit/build_angular/src/browser/specs/no-entry-module_spec.ts
index 705a035eb49b..d9c8dfd17a59 100644
--- a/packages/angular_devkit/build_angular/src/browser/specs/no-entry-module_spec.ts
+++ b/packages/angular_devkit/build_angular/src/browser/specs/no-entry-module_spec.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
diff --git a/packages/angular_devkit/build_angular/src/browser/specs/optimization-level_spec.ts b/packages/angular_devkit/build_angular/src/browser/specs/optimization-level_spec.ts
index f5025f2d9dcb..7fe49602081f 100644
--- a/packages/angular_devkit/build_angular/src/browser/specs/optimization-level_spec.ts
+++ b/packages/angular_devkit/build_angular/src/browser/specs/optimization-level_spec.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
diff --git a/packages/angular_devkit/build_angular/src/browser/specs/output-hashing_spec.ts b/packages/angular_devkit/build_angular/src/browser/specs/output-hashing_spec.ts
deleted file mode 100644
index a557117f7614..000000000000
--- a/packages/angular_devkit/build_angular/src/browser/specs/output-hashing_spec.ts
+++ /dev/null
@@ -1,193 +0,0 @@
-/**
- * @license
- * Copyright Google Inc. All Rights Reserved.
- *
- * Use of this source code is governed by an MIT-style license that can be
- * found in the LICENSE file at https://angular.io/license
- */
-
-import { Architect } from '@angular-devkit/architect';
-import { normalize } from '@angular-devkit/core';
-import {
- browserBuild,
- createArchitect,
- host,
- lazyModuleFiles,
- lazyModuleFnImport,
-} from '../../test-utils';
-
-describe('Browser Builder output hashing', () => {
- const target = { project: 'app', target: 'build' };
- let architect: Architect;
-
- beforeEach(async () => {
- await host.initialize().toPromise();
- architect = (await createArchitect(host.root())).architect;
- });
- afterEach(async () => host.restore().toPromise());
-
- it('updates hash as content changes', async () => {
- const OUTPUT_RE = /(main|styles|lazy\.module)\.([a-z0-9]+)\.(chunk|bundle)\.(js|css)$/;
-
- function generateFileHashMap(): Map {
- const hashes = new Map();
-
- host
- .scopedSync()
- .list(normalize('./dist'))
- .forEach(name => {
- const matches = name.match(OUTPUT_RE);
- if (matches) {
- const [, module, hash] = matches;
- hashes.set(module, hash);
- }
- });
-
- return hashes;
- }
-
- function validateHashes(
- oldHashes: Map,
- newHashes: Map,
- shouldChange: Array,
- ): void {
- newHashes.forEach((hash, module) => {
- if (hash == oldHashes.get(module)) {
- if (shouldChange.includes(module)) {
- throw new Error(
- `Module "${module}" did not change hash (${hash}), but was expected to.`,
- );
- }
- } else if (!shouldChange.includes(module)) {
- throw new Error(`Module "${module}" changed hash (${hash}), but was not expected to.`);
- }
- });
- }
-
- let oldHashes: Map;
- let newHashes: Map;
-
- host.writeMultipleFiles(lazyModuleFiles);
- host.writeMultipleFiles(lazyModuleFnImport);
-
- const overrides = { outputHashing: 'all', extractCss: true };
-
- // We must do several builds instead of a single one in watch mode, so that the output
- // path is deleted on each run and only contains the most recent files.
- await browserBuild(architect, host, target, overrides);
-
- // Save the current hashes.
- oldHashes = generateFileHashMap();
- host.writeMultipleFiles(lazyModuleFiles);
- host.writeMultipleFiles(lazyModuleFnImport);
-
- await browserBuild(architect, host, target, overrides);
- newHashes = generateFileHashMap();
- validateHashes(oldHashes, newHashes, []);
- oldHashes = newHashes;
- host.writeMultipleFiles({ 'src/styles.css': 'body { background: blue; }' });
-
- // Style hash should change.
- await browserBuild(architect, host, target, overrides);
- newHashes = generateFileHashMap();
- validateHashes(oldHashes, newHashes, ['styles']);
- oldHashes = newHashes;
- host.writeMultipleFiles({ 'src/app/app.component.css': 'h1 { margin: 10px; }' });
-
- // Main hash should change, since inline styles go in the main bundle.
- await browserBuild(architect, host, target, overrides);
- newHashes = generateFileHashMap();
- validateHashes(oldHashes, newHashes, ['main']);
- oldHashes = newHashes;
- host.appendToFile('src/app/lazy/lazy.module.ts', `console.log(1);`);
-
- // Lazy loaded bundle should change, and so should inline.
- await browserBuild(architect, host, target, overrides);
- newHashes = generateFileHashMap();
- validateHashes(oldHashes, newHashes, ['lazy.module']);
- oldHashes = newHashes;
- host.appendToFile('src/main.ts', '');
-
- // Nothing should have changed.
- await browserBuild(architect, host, target, overrides);
- newHashes = generateFileHashMap();
- validateHashes(oldHashes, newHashes, []);
- });
-
- it('supports options', async () => {
- host.writeMultipleFiles({ 'src/styles.css': `h1 { background: url('./spectrum.png')}` });
- host.writeMultipleFiles(lazyModuleFiles);
- host.writeMultipleFiles(lazyModuleFnImport);
-
- // We must do several builds instead of a single one in watch mode, so that the output
- // path is deleted on each run and only contains the most recent files.
- // 'all' should hash everything.
- await browserBuild(architect, host, target, { outputHashing: 'all', extractCss: true });
-
- expect(host.fileMatchExists('dist', /runtime\.[0-9a-f]{20}\.js/)).toBeTruthy();
- expect(host.fileMatchExists('dist', /main\.[0-9a-f]{20}\.js/)).toBeTruthy();
- expect(host.fileMatchExists('dist', /polyfills\.[0-9a-f]{20}\.js/)).toBeTruthy();
- expect(host.fileMatchExists('dist', /vendor\.[0-9a-f]{20}\.js/)).toBeTruthy();
- expect(host.fileMatchExists('dist', /styles\.[0-9a-f]{20}\.css/)).toBeTruthy();
- expect(host.fileMatchExists('dist', /spectrum\.[0-9a-f]{20}\.png/)).toBeTruthy();
-
- // 'none' should hash nothing.
- await browserBuild(architect, host, target, { outputHashing: 'none', extractCss: true });
-
- expect(host.fileMatchExists('dist', /runtime\.[0-9a-f]{20}\.js/)).toBeFalsy();
- expect(host.fileMatchExists('dist', /main\.[0-9a-f]{20}\.js/)).toBeFalsy();
- expect(host.fileMatchExists('dist', /polyfills\.[0-9a-f]{20}\.js/)).toBeFalsy();
- expect(host.fileMatchExists('dist', /vendor\.[0-9a-f]{20}\.js/)).toBeFalsy();
- expect(host.fileMatchExists('dist', /styles\.[0-9a-f]{20}\.css/)).toBeFalsy();
- expect(host.fileMatchExists('dist', /spectrum\.[0-9a-f]{20}\.png/)).toBeFalsy();
-
- // 'media' should hash css resources only.
- await browserBuild(architect, host, target, { outputHashing: 'media', extractCss: true });
-
- expect(host.fileMatchExists('dist', /runtime\.[0-9a-f]{20}\.js/)).toBeFalsy();
- expect(host.fileMatchExists('dist', /main\.[0-9a-f]{20}\.js/)).toBeFalsy();
- expect(host.fileMatchExists('dist', /polyfills\.[0-9a-f]{20}\.js/)).toBeFalsy();
- expect(host.fileMatchExists('dist', /vendor\.[0-9a-f]{20}\.js/)).toBeFalsy();
- expect(host.fileMatchExists('dist', /styles\.[0-9a-f]{20}\.css/)).toBeFalsy();
- expect(host.fileMatchExists('dist', /spectrum\.[0-9a-f]{20}\.png/)).toBeTruthy();
-
- // 'bundles' should hash bundles only.
- await browserBuild(architect, host, target, { outputHashing: 'bundles', extractCss: true });
- expect(host.fileMatchExists('dist', /runtime\.[0-9a-f]{20}\.js/)).toBeTruthy();
- expect(host.fileMatchExists('dist', /main\.[0-9a-f]{20}\.js/)).toBeTruthy();
- expect(host.fileMatchExists('dist', /polyfills\.[0-9a-f]{20}\.js/)).toBeTruthy();
- expect(host.fileMatchExists('dist', /vendor\.[0-9a-f]{20}\.js/)).toBeTruthy();
- expect(host.fileMatchExists('dist', /styles\.[0-9a-f]{20}\.css/)).toBeTruthy();
- expect(host.fileMatchExists('dist', /spectrum\.[0-9a-f]{20}\.png/)).toBeFalsy();
- });
-
- it('does not hash non injected styles', async () => {
- const overrides = {
- outputHashing: 'all',
- extractCss: true,
- styles: [{ input: 'src/styles.css', inject: false }],
- };
-
- await browserBuild(architect, host, target, overrides);
-
- expect(host.fileMatchExists('dist', /styles\.[0-9a-f]{20}\.js/)).toBeFalsy();
- expect(host.fileMatchExists('dist', /styles\.[0-9a-f]{20}\.js.map/)).toBeFalsy();
- expect(host.scopedSync().exists(normalize('dist/styles.css'))).toBe(true);
- expect(host.scopedSync().exists(normalize('dist/styles.css.map'))).toBe(true);
- });
-
- it('does not hash non injected styles when optimization is enabled', async () => {
- const overrides = {
- outputHashing: 'all',
- extractCss: true,
- optimization: true,
- styles: [{ input: 'src/styles.css', inject: false }],
- };
-
- await browserBuild(architect, host, target, overrides);
- expect(host.fileMatchExists('dist', /styles\.[0-9a-f]{20}\.js/)).toBeFalsy();
- expect(host.fileMatchExists('dist', /styles\.[0-9a-f]{20}\.js.map/)).toBeFalsy();
- expect(host.scopedSync().exists(normalize('dist/styles.css'))).toBe(true);
- expect(host.scopedSync().exists(normalize('dist/styles.css.map'))).toBe(true);
- });
-});
diff --git a/packages/angular_devkit/build_angular/src/browser/specs/output-path_spec.ts b/packages/angular_devkit/build_angular/src/browser/specs/output-path_spec.ts
index beb600d77adc..e27ae3fc63b2 100644
--- a/packages/angular_devkit/build_angular/src/browser/specs/output-path_spec.ts
+++ b/packages/angular_devkit/build_angular/src/browser/specs/output-path_spec.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
diff --git a/packages/angular_devkit/build_angular/src/browser/specs/poll_spec.ts b/packages/angular_devkit/build_angular/src/browser/specs/poll_spec.ts
index 9f330fc759f5..8deb9abbee41 100644
--- a/packages/angular_devkit/build_angular/src/browser/specs/poll_spec.ts
+++ b/packages/angular_devkit/build_angular/src/browser/specs/poll_spec.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
diff --git a/packages/angular_devkit/build_angular/src/browser/specs/rebuild_spec.ts b/packages/angular_devkit/build_angular/src/browser/specs/rebuild_spec.ts
index 9162cd0cda6b..10f209f5483b 100644
--- a/packages/angular_devkit/build_angular/src/browser/specs/rebuild_spec.ts
+++ b/packages/angular_devkit/build_angular/src/browser/specs/rebuild_spec.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
diff --git a/packages/angular_devkit/build_angular/src/browser/specs/replacements_spec.ts b/packages/angular_devkit/build_angular/src/browser/specs/replacements_spec.ts
index 795cbadaf918..b0adf56849fd 100644
--- a/packages/angular_devkit/build_angular/src/browser/specs/replacements_spec.ts
+++ b/packages/angular_devkit/build_angular/src/browser/specs/replacements_spec.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
diff --git a/packages/angular_devkit/build_angular/src/browser/specs/resolve-json-module_spec.ts b/packages/angular_devkit/build_angular/src/browser/specs/resolve-json-module_spec.ts
index 373ac3419984..4eb458472e36 100644
--- a/packages/angular_devkit/build_angular/src/browser/specs/resolve-json-module_spec.ts
+++ b/packages/angular_devkit/build_angular/src/browser/specs/resolve-json-module_spec.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
diff --git a/packages/angular_devkit/build_angular/src/browser/specs/resources-output-path_spec.ts b/packages/angular_devkit/build_angular/src/browser/specs/resources-output-path_spec.ts
index fb9e0a87b6ae..b0c027bb2427 100644
--- a/packages/angular_devkit/build_angular/src/browser/specs/resources-output-path_spec.ts
+++ b/packages/angular_devkit/build_angular/src/browser/specs/resources-output-path_spec.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
diff --git a/packages/angular_devkit/build_angular/src/browser/specs/rollup_spec.ts b/packages/angular_devkit/build_angular/src/browser/specs/rollup_spec.ts
index 127e406cea24..d460055302e8 100644
--- a/packages/angular_devkit/build_angular/src/browser/specs/rollup_spec.ts
+++ b/packages/angular_devkit/build_angular/src/browser/specs/rollup_spec.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
diff --git a/packages/angular_devkit/build_angular/src/browser/specs/scripts-array_spec.ts b/packages/angular_devkit/build_angular/src/browser/specs/scripts-array_spec.ts
index 29e004bf8428..a2569242ac6b 100644
--- a/packages/angular_devkit/build_angular/src/browser/specs/scripts-array_spec.ts
+++ b/packages/angular_devkit/build_angular/src/browser/specs/scripts-array_spec.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
@@ -81,20 +81,20 @@ describe('Browser Builder scripts array', () => {
'lazy-script.js': 'lazy-script',
'renamed-script.js': 'pre-rename-script',
'renamed-lazy-script.js': 'pre-rename-lazy-script',
- 'main.js': 'input-script',
- 'index.html': ''
- + ''
+ 'main-es2015.js': 'input-script',
+ 'index.html': ''
+ + ''
+ ''
+ ''
- + ''
- + '',
+ + ''
+ + '',
};
host.writeMultipleFiles(scripts);
host.appendToFile('src/main.ts', '\nimport \'./input-script.js\';');
// Enable differential loading
- host.appendToFile('.browserslistrc', '\nIE 10');
+ host.appendToFile('.browserslistrc', '\nIE 11');
// Remove styles so we don't have to account for them in the index.html order check.
const { files } = await browserBuild(architect, host, target, {
diff --git a/packages/angular_devkit/build_angular/src/browser/specs/service-worker_spec.ts b/packages/angular_devkit/build_angular/src/browser/specs/service-worker_spec.ts
index 7ce6b377b740..15ca52b5d050 100644
--- a/packages/angular_devkit/build_angular/src/browser/specs/service-worker_spec.ts
+++ b/packages/angular_devkit/build_angular/src/browser/specs/service-worker_spec.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
diff --git a/packages/angular_devkit/build_angular/src/browser/specs/source-map_spec.ts b/packages/angular_devkit/build_angular/src/browser/specs/source-map_spec.ts
index b7ea20ef94a9..5e8981d9ac09 100644
--- a/packages/angular_devkit/build_angular/src/browser/specs/source-map_spec.ts
+++ b/packages/angular_devkit/build_angular/src/browser/specs/source-map_spec.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
@@ -158,4 +158,20 @@ describe('Browser Builder source map', () => {
expect(await files['styles.css']).not.toContain('sourceMappingURL=styles.css.map');
expect(await files['styles.css']).not.toContain('sourceMappingURL=data:application/json');
});
+
+ it('should resolve sources to partial SCSS files', async () => {
+ const overrides = {
+ sourceMap: true,
+ extractCss: true,
+ styles: ['src/styles.scss'],
+ };
+
+ host.writeMultipleFiles({
+ 'src/styles.scss': `@import './partial';`,
+ 'src/_partial.scss': `p { color: red; }`,
+ });
+
+ const { files } = await browserBuild(architect, host, target, overrides);
+ expect(await files['styles.css.map']).toContain('_partial.scss');
+ });
});
diff --git a/packages/angular_devkit/build_angular/src/browser/specs/stats-json_spec.ts b/packages/angular_devkit/build_angular/src/browser/specs/stats-json_spec.ts
index 4ef7c3a5c0b6..116828fe4bbe 100644
--- a/packages/angular_devkit/build_angular/src/browser/specs/stats-json_spec.ts
+++ b/packages/angular_devkit/build_angular/src/browser/specs/stats-json_spec.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
diff --git a/packages/angular_devkit/build_angular/src/browser/specs/styles_spec.ts b/packages/angular_devkit/build_angular/src/browser/specs/styles_spec.ts
index 7da64970b00e..176b3b241df3 100644
--- a/packages/angular_devkit/build_angular/src/browser/specs/styles_spec.ts
+++ b/packages/angular_devkit/build_angular/src/browser/specs/styles_spec.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
@@ -354,7 +354,7 @@ describe('Browser Builder styles', () => {
});
// TODO: consider making this a unit test in the url processing plugins.
- it(`supports baseHref/deployUrl in resource urls without rebaseRootRelativeCssUrls`, async () => {
+ it(`supports baseHref/deployUrl in resource urls`, async () => {
// Use a large image for the relative ref so it cannot be inlined.
host.copyFile('src/spectrum.png', './src/assets/global-img-relative.png');
host.copyFile('src/spectrum.png', './src/assets/component-img-relative.png');
@@ -458,118 +458,6 @@ describe('Browser Builder styles', () => {
expect(main).toContain(`url('/assets/component-img-absolute.svg')`);
}, 90000);
- it(`supports baseHref/deployUrl in resource urls with rebaseRootRelativeCssUrls`, async () => {
- // Use a large image for the relative ref so it cannot be inlined.
- host.copyFile('src/spectrum.png', './src/assets/global-img-relative.png');
- host.copyFile('src/spectrum.png', './src/assets/component-img-relative.png');
- host.writeMultipleFiles({
- 'src/styles.css': `
- h1 { background: url('/assets/global-img-absolute.svg'); }
- h2 { background: url('./assets/global-img-relative.png'); }
- `,
- 'src/app/app.component.css': `
- h3 { background: url('/assets/component-img-absolute.svg'); }
- h4 { background: url('../assets/component-img-relative.png'); }
- `,
- 'src/assets/global-img-absolute.svg': imgSvg,
- 'src/assets/component-img-absolute.svg': imgSvg,
- });
-
- // Check base paths are correctly generated.
- const overrides = {
- extractCss: true,
- rebaseRootRelativeCssUrls: true,
- };
- let { files } = await browserBuild(architect, host, target, {
- ...overrides,
- aot: true,
- });
-
- let styles = await files['styles.css'];
- let main = await files['main.js'];
- expect(styles).toContain(`url('/assets/global-img-absolute.svg')`);
- expect(styles).toContain(`url('global-img-relative.png')`);
- expect(main).toContain(`url('/assets/component-img-absolute.svg')`);
- expect(main).toContain(`url('component-img-relative.png')`);
- expect(host.scopedSync().exists(normalize('dist/assets/global-img-absolute.svg'))).toBe(true);
- expect(host.scopedSync().exists(normalize('dist/global-img-relative.png'))).toBe(true);
- expect(host.scopedSync().exists(normalize('dist/assets/component-img-absolute.svg'))).toBe(
- true,
- );
- expect(host.scopedSync().exists(normalize('dist/component-img-relative.png'))).toBe(true);
-
- // Check urls with deploy-url scheme are used as is.
- files = (await browserBuild(architect, host, target, {
- ...overrides,
- baseHref: '/base/',
- deployUrl: 'http://deploy.url/',
- })).files;
-
- styles = await files['styles.css'];
- main = await files['main.js'];
- expect(styles).toContain(`url('http://deploy.url/assets/global-img-absolute.svg')`);
- expect(main).toContain(`url('http://deploy.url/assets/component-img-absolute.svg')`);
-
- // Check urls with base-href scheme are used as is (with deploy-url).
- files = (await browserBuild(architect, host, target, {
- ...overrides,
- baseHref: 'http://base.url/',
- deployUrl: 'deploy/',
- })).files;
-
- styles = await files['styles.css'];
- main = await files['main.js'];
- expect(styles).toContain(`url('http://base.url/deploy/assets/global-img-absolute.svg')`);
- expect(main).toContain(`url('http://base.url/deploy/assets/component-img-absolute.svg')`);
-
- // Check urls with deploy-url and base-href scheme only use deploy-url.
- files = (await browserBuild(architect, host, target, {
- ...overrides,
- baseHref: 'http://base.url/',
- deployUrl: 'http://deploy.url/',
- })).files;
-
- styles = await files['styles.css'];
- main = await files['main.js'];
- expect(styles).toContain(`url('http://deploy.url/assets/global-img-absolute.svg')`);
- expect(main).toContain(`url('http://deploy.url/assets/component-img-absolute.svg')`);
-
- // Check with schemeless base-href and deploy-url flags.
- files = (await browserBuild(architect, host, target, {
- ...overrides,
- baseHref: '/base/',
- deployUrl: 'deploy/',
- })).files;
-
- styles = await files['styles.css'];
- main = await files['main.js'];
- expect(styles).toContain(`url('/base/deploy/assets/global-img-absolute.svg')`);
- expect(main).toContain(`url('/base/deploy/assets/component-img-absolute.svg')`);
-
- // Check with identical base-href and deploy-url flags.
- files = (await browserBuild(architect, host, target, {
- ...overrides,
- baseHref: '/base/',
- deployUrl: '/base/',
- })).files;
-
- styles = await files['styles.css'];
- main = await files['main.js'];
- expect(styles).toContain(`url('/base/assets/global-img-absolute.svg')`);
- expect(main).toContain(`url('/base/assets/component-img-absolute.svg')`);
-
- // Check with only base-href flag.
- files = (await browserBuild(architect, host, target, {
- ...overrides,
- baseHref: '/base/',
- })).files;
-
- styles = await files['styles.css'];
- main = await files['main.js'];
- expect(styles).toContain(`url('/base/assets/global-img-absolute.svg')`);
- expect(main).toContain(`url('/base/assets/component-img-absolute.svg')`);
- }, 90000);
-
it(`supports bootstrap@4 with full path`, async () => {
const bootstrapPath = dirname(require.resolve('bootstrap/package.json'));
diff --git a/packages/angular_devkit/build_angular/src/browser/specs/subresource-integrity_spec.ts b/packages/angular_devkit/build_angular/src/browser/specs/subresource-integrity_spec.ts
deleted file mode 100644
index 274d89889a71..000000000000
--- a/packages/angular_devkit/build_angular/src/browser/specs/subresource-integrity_spec.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-/**
- * @license
- * Copyright Google Inc. All Rights Reserved.
- *
- * Use of this source code is governed by an MIT-style license that can be
- * found in the LICENSE file at https://angular.io/license
- */
-import { Architect } from '@angular-devkit/architect';
-import { browserBuild, createArchitect, host } from '../../test-utils';
-
-
-describe('Browser Builder subresource integrity', () => {
- const target = { project: 'app', target: 'build' };
- let architect: Architect;
-
- beforeEach(async () => {
- await host.initialize().toPromise();
- architect = (await createArchitect(host.root())).architect;
- });
- afterEach(async () => host.restore().toPromise());
-
- it('works', async () => {
- host.writeMultipleFiles({
- 'src/my-js-file.js': `console.log(1); export const a = 2;`,
- 'src/main.ts': `import { a } from './my-js-file'; console.log(a);`,
- });
-
- const overrides = { subresourceIntegrity: true };
- const { files } = await browserBuild(architect, host, target, overrides);
- expect(await files['index.html']).toMatch(/integrity="\w+-[A-Za-z0-9\/\+=]+"/);
- });
-});
diff --git a/packages/angular_devkit/build_angular/src/browser/specs/svg_spec.ts b/packages/angular_devkit/build_angular/src/browser/specs/svg_spec.ts
index 3e32677c00ff..d416c7c0a2da 100644
--- a/packages/angular_devkit/build_angular/src/browser/specs/svg_spec.ts
+++ b/packages/angular_devkit/build_angular/src/browser/specs/svg_spec.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
diff --git a/packages/angular_devkit/build_angular/src/browser/specs/tsconfig-paths_spec.ts b/packages/angular_devkit/build_angular/src/browser/specs/tsconfig-paths_spec.ts
index 8e31ff766798..21eeaf44ea06 100644
--- a/packages/angular_devkit/build_angular/src/browser/specs/tsconfig-paths_spec.ts
+++ b/packages/angular_devkit/build_angular/src/browser/specs/tsconfig-paths_spec.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
diff --git a/packages/angular_devkit/build_angular/src/browser/specs/unused-files-warning_spec.ts b/packages/angular_devkit/build_angular/src/browser/specs/unused-files-warning_spec.ts
index e8b920ab1fdc..7f62ebc53e81 100644
--- a/packages/angular_devkit/build_angular/src/browser/specs/unused-files-warning_spec.ts
+++ b/packages/angular_devkit/build_angular/src/browser/specs/unused-files-warning_spec.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
diff --git a/packages/angular_devkit/build_angular/src/browser/specs/vendor-chunk_spec.ts b/packages/angular_devkit/build_angular/src/browser/specs/vendor-chunk_spec.ts
index e78237b25186..6fe4f211fd55 100644
--- a/packages/angular_devkit/build_angular/src/browser/specs/vendor-chunk_spec.ts
+++ b/packages/angular_devkit/build_angular/src/browser/specs/vendor-chunk_spec.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
diff --git a/packages/angular_devkit/build_angular/src/browser/specs/vendor-source-map_spec.ts b/packages/angular_devkit/build_angular/src/browser/specs/vendor-source-map_spec.ts
index 11b8fc0dc9ed..31e2124ec985 100644
--- a/packages/angular_devkit/build_angular/src/browser/specs/vendor-source-map_spec.ts
+++ b/packages/angular_devkit/build_angular/src/browser/specs/vendor-source-map_spec.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
@@ -34,7 +34,7 @@ describe('Browser Builder external source map', () => {
expect(hasTsSourcePaths).toBe(true, `vendor.js.map should have '.ts' extentions`);
});
- it(`does not generate 'vendor.js.map' when vendor sourcemap is disabled`, async () => {
+ it('does not map sourcemaps from external library when disabled', async () => {
const overrides = {
sourceMap: {
scripts: true,
@@ -44,6 +44,8 @@ describe('Browser Builder external source map', () => {
};
const { files } = await browserBuild(architect, host, target, overrides);
- expect(files['vendor.js.map']).toBeUndefined();
+ const sourcePaths: string[] = JSON.parse(await files['vendor.js.map']).sources;
+ const hasTsSourcePaths = sourcePaths.some(p => path.extname(p) == '.ts');
+ expect(hasTsSourcePaths).toBe(false, `vendor.js.map not should have '.ts' extentions`);
});
});
diff --git a/packages/angular_devkit/build_angular/src/browser/specs/web-worker_spec.ts b/packages/angular_devkit/build_angular/src/browser/specs/web-worker_spec.ts
index 9d8392c430ed..a9897e965889 100644
--- a/packages/angular_devkit/build_angular/src/browser/specs/web-worker_spec.ts
+++ b/packages/angular_devkit/build_angular/src/browser/specs/web-worker_spec.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
diff --git a/packages/angular_devkit/build_angular/src/browser/specs/works_spec.ts b/packages/angular_devkit/build_angular/src/browser/specs/works_spec.ts
index 75b2eda17272..50bbce771bef 100644
--- a/packages/angular_devkit/build_angular/src/browser/specs/works_spec.ts
+++ b/packages/angular_devkit/build_angular/src/browser/specs/works_spec.ts
@@ -1,37 +1,45 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
+import { describeBuilder } from '../../testing';
+import { buildWebpackBrowser } from '../index';
-import { Architect } from '@angular-devkit/architect';
-import { normalize } from '@angular-devkit/core';
-import { browserBuild, createArchitect, host } from '../../test-utils';
+const BROWSER_BUILDER_INFO = {
+ name: '@angular-devkit/build-angular:browser',
+ schemaPath: __dirname + '/../schema.json',
+};
+describeBuilder(buildWebpackBrowser, BROWSER_BUILDER_INFO, (harness) => {
+ describe('basic test', () => {
+ it('works', async () => {
+ // Provide a target and options for builder execution
+ harness.useTarget('build', {
+ outputPath: 'dist',
+ index: 'src/index.html',
+ main: 'src/main.ts',
+ polyfills: 'src/polyfills.ts',
+ tsConfig: 'src/tsconfig.app.json',
+ progress: false,
+ assets: ['src/favicon.ico', 'src/assets'],
+ styles: ['src/styles.css'],
+ scripts: [],
+ });
-describe('Browser Builder basic test', () => {
- const target = { project: 'app', target: 'build' };
- let architect: Architect;
+ // Execute builder with above provided project, target, and options
+ await harness.executeOnce();
- beforeEach(async () => {
- await host.initialize().toPromise();
- architect = (await createArchitect(host.root())).architect;
- });
-
- afterEach(async () => host.restore().toPromise());
-
- it('works', async () => {
- await browserBuild(architect, host, target);
-
- // Default files should be in outputPath.
- expect(await host.scopedSync().exists(normalize('dist/runtime.js'))).toBe(true);
- expect(await host.scopedSync().exists(normalize('dist/main.js'))).toBe(true);
- expect(await host.scopedSync().exists(normalize('dist/polyfills.js'))).toBe(true);
- expect(await host.scopedSync().exists(normalize('dist/vendor.js'))).toBe(true);
- expect(await host.scopedSync().exists(normalize('dist/favicon.ico'))).toBe(true);
- expect(await host.scopedSync().exists(normalize('dist/styles.css'))).toBe(true);
- expect(await host.scopedSync().exists(normalize('dist/index.html'))).toBe(true);
+ // Default files should be in outputPath.
+ expect(harness.hasFile('dist/runtime.js')).toBe(true);
+ expect(harness.hasFile('dist/main.js')).toBe(true);
+ expect(harness.hasFile('dist/polyfills.js')).toBe(true);
+ expect(harness.hasFile('dist/vendor.js')).toBe(true);
+ expect(harness.hasFile('dist/favicon.ico')).toBe(true);
+ expect(harness.hasFile('dist/styles.css')).toBe(true);
+ expect(harness.hasFile('dist/index.html')).toBe(true);
+ });
});
});
diff --git a/packages/angular_devkit/build_angular/src/browser/tests/behavior/rebuild-errors_spec.ts b/packages/angular_devkit/build_angular/src/browser/tests/behavior/rebuild-errors_spec.ts
new file mode 100644
index 000000000000..3cc82124298f
--- /dev/null
+++ b/packages/angular_devkit/build_angular/src/browser/tests/behavior/rebuild-errors_spec.ts
@@ -0,0 +1,329 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+// tslint:disable: no-big-function
+import { logging } from '@angular-devkit/core';
+import { concatMap, count, take, timeout } from 'rxjs/operators';
+import { buildWebpackBrowser } from '../../index';
+import { BASE_OPTIONS, BROWSER_BUILDER_INFO, describeBuilder } from '../setup';
+
+describeBuilder(buildWebpackBrowser, BROWSER_BUILDER_INFO, (harness) => {
+ describe('Behavior: "Rebuild Error"', () => {
+
+ it('detects template errors with no AOT codegen or TS emit differences', async () => {
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ aot: true,
+ watch: true,
+ });
+
+ const goodDirectiveContents = `
+ import { Directive, Input } from '@angular/core';
+ @Directive({ selector: 'dir' })
+ export class Dir {
+ @Input() foo: number;
+ }
+ `;
+
+ const typeErrorText = `Type 'number' is not assignable to type 'string'.`;
+
+ // Create a directive and add to application
+ await harness.writeFile('src/app/dir.ts', goodDirectiveContents);
+ await harness.writeFile('src/app/app.module.ts', `
+ import { NgModule } from '@angular/core';
+ import { BrowserModule } from '@angular/platform-browser';
+ import { AppComponent } from './app.component';
+ import { Dir } from './dir';
+ @NgModule({
+ declarations: [
+ AppComponent,
+ Dir,
+ ],
+ imports: [
+ BrowserModule
+ ],
+ providers: [],
+ bootstrap: [AppComponent]
+ })
+ export class AppModule { }
+ `);
+
+ // Create app component that uses the directive
+ await harness.writeFile('src/app/app.component.ts', `
+ import { Component } from '@angular/core'
+ @Component({
+ selector: 'app-root',
+ template: '',
+ })
+ export class AppComponent { }
+ `);
+
+ const buildCount = await harness
+ .execute({ outputLogsOnFailure: false })
+ .pipe(
+ timeout(60000),
+ concatMap(async ({ result, logs }, index) => {
+ switch (index) {
+ case 0:
+ expect(result?.success).toBeTrue();
+
+ // Update directive to use a different input type for 'foo' (number -> string)
+ // Should cause a template error
+ await harness.writeFile('src/app/dir.ts', `
+ import { Directive, Input } from '@angular/core';
+ @Directive({ selector: 'dir' })
+ export class Dir {
+ @Input() foo: string;
+ }
+ `);
+
+ break;
+ case 1:
+ expect(result?.success).toBeFalse();
+ expect(logs).toContain(
+ jasmine.objectContaining({
+ message: jasmine.stringMatching(typeErrorText),
+ }),
+ );
+
+ // Make an unrelated change to verify error cache was updated
+ // Should persist error in the next rebuild
+ await harness.modifyFile('src/main.ts', (content) => content + '\n');
+
+ break;
+ case 2:
+ expect(result?.success).toBeFalse();
+ expect(logs).toContain(
+ jasmine.objectContaining({
+ message: jasmine.stringMatching(typeErrorText),
+ }),
+ );
+
+ // Revert the directive change that caused the error
+ // Should remove the error
+ await harness.writeFile('src/app/dir.ts', goodDirectiveContents);
+
+ break;
+ case 3:
+ expect(result?.success).toBeTrue();
+ expect(logs).not.toContain(
+ jasmine.objectContaining({
+ message: jasmine.stringMatching(typeErrorText),
+ }),
+ );
+
+ // Make an unrelated change to verify error cache was updated
+ // Should continue showing no error
+ await harness.modifyFile('src/main.ts', (content) => content + '\n');
+
+ break;
+ case 4:
+ expect(result?.success).toBeTrue();
+ expect(logs).not.toContain(
+ jasmine.objectContaining({
+ message: jasmine.stringMatching(typeErrorText),
+ }),
+ );
+
+ break;
+ }
+ }),
+ take(5),
+ count(),
+ )
+ .toPromise();
+
+ expect(buildCount).toBe(5);
+ });
+
+ it('detects template errors with AOT codegen differences', async () => {
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ aot: true,
+ watch: true,
+ });
+
+ const typeErrorText = `Type 'number' is not assignable to type 'string'.`;
+
+ // Create two directives and add to application
+ await harness.writeFile('src/app/dir.ts', `
+ import { Directive, Input } from '@angular/core';
+ @Directive({ selector: 'dir' })
+ export class Dir {
+ @Input() foo: number;
+ }
+ `);
+
+ // Same selector with a different type on the `foo` property but initially no `@Input`
+ const goodDirectiveContents = `
+ import { Directive } from '@angular/core';
+ @Directive({ selector: 'dir' })
+ export class Dir2 {
+ foo: string;
+ }
+ `;
+ await harness.writeFile('src/app/dir2.ts', goodDirectiveContents);
+
+ await harness.writeFile('src/app/app.module.ts', `
+ import { NgModule } from '@angular/core';
+ import { BrowserModule } from '@angular/platform-browser';
+ import { AppComponent } from './app.component';
+ import { Dir } from './dir';
+ import { Dir2 } from './dir2';
+ @NgModule({
+ declarations: [
+ AppComponent,
+ Dir,
+ Dir2,
+ ],
+ imports: [
+ BrowserModule
+ ],
+ providers: [],
+ bootstrap: [AppComponent]
+ })
+ export class AppModule { }
+ `);
+
+ // Create app component that uses the directive
+ await harness.writeFile('src/app/app.component.ts', `
+ import { Component } from '@angular/core'
+ @Component({
+ selector: 'app-root',
+ template: '',
+ })
+ export class AppComponent { }
+ `);
+
+ const buildCount = await harness
+ .execute({ outputLogsOnFailure: false })
+ .pipe(
+ timeout(60000),
+ concatMap(async ({ result, logs }, index) => {
+ switch (index) {
+ case 0:
+ expect(result?.success).toBeTrue();
+
+ // Update second directive to use string property `foo` as an Input
+ // Should cause a template error
+ await harness.writeFile('src/app/dir2.ts', `
+ import { Directive, Input } from '@angular/core';
+ @Directive({ selector: 'dir' })
+ export class Dir2 {
+ @Input() foo: string;
+ }
+ `);
+
+ break;
+ case 1:
+ expect(result?.success).toBeFalse();
+ expect(logs).toContain(
+ jasmine.objectContaining({
+ message: jasmine.stringMatching(typeErrorText),
+ }),
+ );
+
+ // Make an unrelated change to verify error cache was updated
+ // Should persist error in the next rebuild
+ await harness.modifyFile('src/main.ts', (content) => content + '\n');
+
+ break;
+ case 2:
+ expect(result?.success).toBeFalse();
+ expect(logs).toContain(
+ jasmine.objectContaining({
+ message: jasmine.stringMatching(typeErrorText),
+ }),
+ );
+
+ // Revert the directive change that caused the error
+ // Should remove the error
+ await harness.writeFile('src/app/dir2.ts', goodDirectiveContents);
+
+ break;
+ case 3:
+ expect(result?.success).toBeTrue();
+ expect(logs).not.toContain(
+ jasmine.objectContaining({
+ message: jasmine.stringMatching(typeErrorText),
+ }),
+ );
+
+ // Make an unrelated change to verify error cache was updated
+ // Should continue showing no error
+ await harness.modifyFile('src/main.ts', (content) => content + '\n');
+
+ break;
+ case 4:
+ expect(result?.success).toBeTrue();
+ expect(logs).not.toContain(
+ jasmine.objectContaining({
+ message: jasmine.stringMatching(typeErrorText),
+ }),
+ );
+
+ break;
+ }
+ }),
+ take(5),
+ count(),
+ )
+ .toPromise();
+
+ expect(buildCount).toBe(5);
+ });
+
+ it('recovers from component stylesheet error', async () => {
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ watch: true,
+ });
+
+ const buildCount = await harness
+ .execute({ outputLogsOnFailure: false })
+ .pipe(
+ timeout(30000),
+ concatMap(async ({ result, logs }, index) => {
+ switch (index) {
+ case 0:
+ expect(result?.success).toBeTrue();
+ await harness.writeFile('src/app/app.component.css', 'invalid-css-content');
+
+ break;
+ case 1:
+ expect(result?.success).toBeFalse();
+ expect(logs).toContain(
+ jasmine.objectContaining({
+ message: jasmine.stringMatching('invalid-css-content'),
+ }),
+ );
+
+ await harness.writeFile('src/app/app.component.css', 'p { color: green }');
+
+ break;
+ case 2:
+ expect(result?.success).toBeTrue();
+ expect(logs).not.toContain(
+ jasmine.objectContaining({
+ message: jasmine.stringMatching('invalid-css-content'),
+ }),
+ );
+
+ harness.expectFile('dist/main.js').content.toContain('p { color: green }');
+
+ break;
+ }
+ }),
+ take(3),
+ count(),
+ )
+ .toPromise();
+
+ expect(buildCount).toBe(3);
+ });
+ });
+});
diff --git a/packages/angular_devkit/build_angular/src/browser/tests/behavior/typescript-target_spec.ts b/packages/angular_devkit/build_angular/src/browser/tests/behavior/typescript-target_spec.ts
new file mode 100644
index 000000000000..b9c7ba230e08
--- /dev/null
+++ b/packages/angular_devkit/build_angular/src/browser/tests/behavior/typescript-target_spec.ts
@@ -0,0 +1,137 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+import { buildWebpackBrowser } from '../../index';
+import { BASE_OPTIONS, BROWSER_BUILDER_INFO, describeBuilder } from '../setup';
+
+describeBuilder(buildWebpackBrowser, BROWSER_BUILDER_INFO, (harness) => {
+ describe('Behavior: "TypeScript Configuration - target"', () => {
+ it('downlevels async functions when targetting ES2017', async () => {
+ // Set TypeScript configuration target to ES2017 to enable native async
+ await harness.modifyFile('src/tsconfig.app.json', (content) => {
+ const tsconfig = JSON.parse(content);
+ if (!tsconfig.compilerOptions) {
+ tsconfig.compilerOptions = {};
+ }
+ tsconfig.compilerOptions.target = 'es2017';
+
+ return JSON.stringify(tsconfig);
+ });
+
+ // Add a JavaScript file with async code
+ await harness.writeFile(
+ 'src/async-test.js',
+ 'async function testJs() { console.log("from-async-js-function"); }',
+ );
+
+ // Add an async function to the project as well as JavaScript file
+ await harness.modifyFile(
+ 'src/main.ts',
+ (content) =>
+ 'import "./async-test";\n' +
+ content +
+ `\nasync function testApp(): Promise { console.log("from-async-app-function"); }`,
+ );
+
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ });
+
+ const { result, logs } = await harness.executeOnce();
+
+ expect(result?.success).toBe(true);
+ expect(logs).not.toContain(
+ jasmine.objectContaining({
+ message: jasmine.stringMatching('Zone.js does not support native async/await in ES2017+'),
+ }),
+ );
+
+ harness.expectFile('dist/main.js').content.not.toMatch(/\sasync\s/);
+ harness.expectFile('dist/main.js').content.toContain('"from-async-app-function"');
+ harness.expectFile('dist/main.js').content.toContain('"from-async-js-function"');
+ });
+
+ it('creates correct sourcemaps when downleveling async functions', async () => {
+ // Set TypeScript configuration target to ES2017 to enable native async
+ await harness.modifyFile('src/tsconfig.app.json', (content) => {
+ const tsconfig = JSON.parse(content);
+ if (!tsconfig.compilerOptions) {
+ tsconfig.compilerOptions = {};
+ }
+ tsconfig.compilerOptions.target = 'es2017';
+
+ return JSON.stringify(tsconfig);
+ });
+
+ // Add a JavaScript file with async code
+ await harness.writeFile(
+ 'src/async-test.js',
+ 'async function testJs() { console.log("from-async-js-function"); }',
+ );
+
+ // Add an async function to the project as well as JavaScript file
+ // The type `Void123` is used as a unique identifier for the final sourcemap
+ // If sourcemaps are not properly propagated then it will not be in the final sourcemap
+ await harness.modifyFile(
+ 'src/main.ts',
+ (content) =>
+ 'import "./async-test";\n' +
+ content +
+ '\ntype Void123 = void;' +
+ `\nasync function testApp(): Promise { console.log("from-async-app-function"); }`,
+ );
+
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ sourceMap: {
+ scripts: true,
+ },
+ });
+
+ const { result } = await harness.executeOnce();
+
+ expect(result?.success).toBe(true);
+ harness.expectFile('dist/main.js').content.not.toMatch(/\sasync\s/);
+ harness.expectFile('dist/main.js.map').content.toContain('Promise');
+ });
+
+ it('downlevels async functions when targetting greater than ES2017', async () => {
+ // Set TypeScript configuration target greater than ES2017 to enable native async
+ await harness.modifyFile('src/tsconfig.app.json', (content) => {
+ const tsconfig = JSON.parse(content);
+ if (!tsconfig.compilerOptions) {
+ tsconfig.compilerOptions = {};
+ }
+ tsconfig.compilerOptions.target = 'es2020';
+
+ return JSON.stringify(tsconfig);
+ });
+
+ // Add an async function to the project
+ await harness.writeFile(
+ 'src/main.ts',
+ 'async function test(): Promise { console.log("from-async-function"); }',
+ );
+
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ });
+
+ const { result, logs } = await harness.executeOnce();
+
+ expect(result?.success).toBe(true);
+ expect(logs).not.toContain(
+ jasmine.objectContaining({
+ message: jasmine.stringMatching('Zone.js does not support native async/await in ES2017+'),
+ }),
+ );
+
+ harness.expectFile('dist/main.js').content.not.toMatch(/\sasync\s/);
+ harness.expectFile('dist/main.js').content.toContain('"from-async-function"');
+ });
+ });
+});
diff --git a/packages/angular_devkit/build_angular/src/browser/tests/options/allowed-common-js-dependencies_spec.ts b/packages/angular_devkit/build_angular/src/browser/tests/options/allowed-common-js-dependencies_spec.ts
new file mode 100644
index 000000000000..8da71c1b3315
--- /dev/null
+++ b/packages/angular_devkit/build_angular/src/browser/tests/options/allowed-common-js-dependencies_spec.ts
@@ -0,0 +1,139 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+import { logging } from '@angular-devkit/core';
+import { buildWebpackBrowser } from '../../index';
+import { BASE_OPTIONS, BROWSER_BUILDER_INFO, describeBuilder } from '../setup';
+
+describeBuilder(buildWebpackBrowser, BROWSER_BUILDER_INFO, harness => {
+ describe('Option: "allowedCommonJsDependencies"', () => {
+ describe('given option is not set', () => {
+ for (const aot of [true, false]) {
+ it(`should not show warning for styles import in ${aot ? 'AOT' : 'JIT'} Mode`, async () => {
+ await harness.writeFile('./test.css', `body { color: red; };`);
+ await harness.appendToFile('src/app/app.component.ts', `import '../../test.css';`);
+
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ allowedCommonJsDependencies: [],
+ aot,
+ });
+
+ const { result, logs } = await harness.executeOnce();
+
+ expect(result?.success).toBe(true);
+ expect(logs).not.toContain(
+ jasmine.objectContaining({
+ message: jasmine.stringMatching(/CommonJS or AMD dependencies/),
+ }),
+ );
+ });
+
+ it(`should show warning when depending on a Common JS bundle in ${aot ? 'AOT' : 'JIT'} Mode`, async () => {
+ // Add a Common JS dependency
+ await harness.appendToFile('src/app/app.component.ts', `import 'bootstrap';`);
+
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ allowedCommonJsDependencies: [],
+ aot,
+ });
+
+ const { result, logs } = await harness.executeOnce();
+
+ expect(result?.success).toBe(true);
+ expect(logs).toContain(
+ jasmine.objectContaining({
+ message: jasmine.stringMatching(/Warning: .+app\.component\.ts depends on 'bootstrap'\. CommonJS or AMD dependencies/),
+ }),
+ );
+ expect(logs).not.toContain(
+ jasmine.objectContaining({ message: jasmine.stringMatching('jquery') }),
+ 'Should not warn on transitive CommonJS packages which parent is also CommonJS.',
+ );
+ });
+ }
+ });
+
+ it('should not show warning when depending on a Common JS bundle which is allowed', async () => {
+ // Add a Common JS dependency
+ await harness.appendToFile('src/app/app.component.ts', `
+ import 'bootstrap';
+ import 'zone.js/dist/zone-error';
+ `);
+
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ allowedCommonJsDependencies: [
+ 'bootstrap',
+ 'zone.js',
+ ],
+ });
+
+ const { result, logs } = await harness.executeOnce();
+
+ expect(result?.success).toBe(true);
+ expect(logs).not.toContain(
+ jasmine.objectContaining({
+ message: jasmine.stringMatching(/CommonJS or AMD dependencies/),
+ }),
+ );
+ });
+
+ it(`should not show warning when importing non global local data '@angular/common/locale/fr'`, async () => {
+ await harness.appendToFile('src/app/app.component.ts', `import '@angular/common/locales/fr';`);
+
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ allowedCommonJsDependencies: [],
+ });
+
+ const { result, logs } = await harness.executeOnce();
+
+ expect(result?.success).toBe(true);
+ expect(logs).not.toContain(
+ jasmine.objectContaining({
+ message: jasmine.stringMatching(/CommonJS or AMD dependencies/),
+ }),
+ );
+ });
+
+ it('should not show warning in JIT for templateUrl and styleUrl when using paths', async () => {
+ await harness.modifyFile(
+ 'tsconfig.json', content => {
+ return content.replace(/"baseUrl": ".\/",/, `
+ "baseUrl": "./",
+ "paths": {
+ "@app/*": [
+ "src/app/*"
+ ]
+ },
+ `);
+ });
+
+ await harness.modifyFile(
+ 'src/app/app.module.ts',
+ content => content.replace('./app.component', '@app/app.component'),
+ );
+
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ allowedCommonJsDependencies: [],
+ aot: false,
+ });
+
+ const { result, logs } = await harness.executeOnce();
+
+ expect(result?.success).toBe(true);
+ expect(logs).not.toContain(
+ jasmine.objectContaining({
+ message: jasmine.stringMatching(/CommonJS or AMD dependencies/),
+ }),
+ );
+ });
+ });
+});
diff --git a/packages/angular_devkit/build_angular/src/browser/tests/options/assets_spec.ts b/packages/angular_devkit/build_angular/src/browser/tests/options/assets_spec.ts
new file mode 100644
index 000000000000..cf9a6a97a9c1
--- /dev/null
+++ b/packages/angular_devkit/build_angular/src/browser/tests/options/assets_spec.ts
@@ -0,0 +1,394 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+// tslint:disable:no-big-function
+import { buildWebpackBrowser } from '../../index';
+import { BASE_OPTIONS, BROWSER_BUILDER_INFO, describeBuilder } from '../setup';
+
+describeBuilder(buildWebpackBrowser, BROWSER_BUILDER_INFO, (harness) => {
+ describe('Option: "assets"', () => {
+ beforeEach(async () => {
+ // Application code is not needed for asset tests
+ await harness.writeFile('src/main.ts', '');
+ });
+
+ it('supports an empty array value', async () => {
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ assets: [],
+ });
+
+ const { result } = await harness.executeOnce();
+
+ expect(result?.success).toBe(true);
+ });
+
+ it('supports mixing shorthand and longhand syntax', async () => {
+ await harness.writeFile('src/files/test.svg', ' ');
+ await harness.writeFile('src/files/another.file', 'asset file');
+ await harness.writeFile('src/extra.file', 'extra file');
+
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ assets: ['src/extra.file', { glob: '*', input: 'src/files', output: '.' }],
+ });
+
+ const { result } = await harness.executeOnce();
+
+ expect(result?.success).toBe(true);
+
+ harness.expectFile('dist/extra.file').content.toBe('extra file');
+ harness.expectFile('dist/test.svg').content.toBe(' ');
+ harness.expectFile('dist/another.file').content.toBe('asset file');
+ });
+
+ describe('shorthand syntax', () => {
+ it('copies a single asset', async () => {
+ await harness.writeFile('src/test.svg', ' ');
+
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ assets: ['src/test.svg'],
+ });
+
+ const { result } = await harness.executeOnce();
+
+ expect(result?.success).toBe(true);
+
+ harness.expectFile('dist/test.svg').content.toBe(' ');
+ });
+
+ it('copies multiple assets', async () => {
+ await harness.writeFile('src/test.svg', ' ');
+ await harness.writeFile('src/another.file', 'asset file');
+
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ assets: ['src/test.svg', 'src/another.file'],
+ });
+
+ const { result } = await harness.executeOnce();
+
+ expect(result?.success).toBe(true);
+
+ harness.expectFile('dist/test.svg').content.toBe(' ');
+ harness.expectFile('dist/another.file').content.toBe('asset file');
+ });
+
+ it('copies an asset with directory and maintains directory in output', async () => {
+ await harness.writeFile('src/subdirectory/test.svg', ' ');
+
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ assets: ['src/subdirectory/test.svg'],
+ });
+
+ const { result } = await harness.executeOnce();
+
+ expect(result?.success).toBe(true);
+
+ harness.expectFile('dist/subdirectory/test.svg').content.toBe(' ');
+ });
+
+ it('does not fail if asset does not exist', async () => {
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ assets: ['src/test.svg'],
+ });
+
+ const { result } = await harness.executeOnce();
+
+ expect(result?.success).toBe(true);
+
+ harness.expectFile('dist/test.svg').toNotExist();
+ });
+
+ it('throws exception if asset path is not within project source root', async () => {
+ await harness.writeFile('test.svg', ' ');
+
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ assets: ['test.svg'],
+ });
+
+ const { result, error } = await harness.executeOnce({ outputLogsOnException: false });
+
+ expect(result).toBeUndefined();
+ expect(error).toEqual(
+ jasmine.objectContaining({
+ message: jasmine.stringMatching('path must start with the project source root'),
+ }),
+ );
+
+ harness.expectFile('dist/test.svg').toNotExist();
+ });
+ });
+
+ describe('longhand syntax', () => {
+ it('copies a single asset', async () => {
+ await harness.writeFile('src/test.svg', ' ');
+
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ assets: [{ glob: 'test.svg', input: 'src', output: '.' }],
+ });
+
+ const { result } = await harness.executeOnce();
+
+ expect(result?.success).toBe(true);
+
+ harness.expectFile('dist/test.svg').content.toBe(' ');
+ });
+
+ it('copies multiple assets as separate entries', async () => {
+ await harness.writeFile('src/test.svg', ' ');
+ await harness.writeFile('src/another.file', 'asset file');
+
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ assets: [
+ { glob: 'test.svg', input: 'src', output: '.' },
+ { glob: 'another.file', input: 'src', output: '.' },
+ ],
+ });
+
+ const { result } = await harness.executeOnce();
+
+ expect(result?.success).toBe(true);
+
+ harness.expectFile('dist/test.svg').content.toBe(' ');
+ harness.expectFile('dist/another.file').content.toBe('asset file');
+ });
+
+ it('copies multiple assets with a single entry glob pattern', async () => {
+ await harness.writeFile('src/test.svg', ' ');
+ await harness.writeFile('src/another.file', 'asset file');
+
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ assets: [{ glob: '{test.svg,another.file}', input: 'src', output: '.' }],
+ });
+
+ const { result } = await harness.executeOnce();
+
+ expect(result?.success).toBe(true);
+
+ harness.expectFile('dist/test.svg').content.toBe(' ');
+ harness.expectFile('dist/another.file').content.toBe('asset file');
+ });
+
+ it('copies multiple assets with a wildcard glob pattern', async () => {
+ await harness.writeFile('src/files/test.svg', ' ');
+ await harness.writeFile('src/files/another.file', 'asset file');
+
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ assets: [{ glob: '*', input: 'src/files', output: '.' }],
+ });
+
+ const { result } = await harness.executeOnce();
+
+ expect(result?.success).toBe(true);
+
+ harness.expectFile('dist/test.svg').content.toBe(' ');
+ harness.expectFile('dist/another.file').content.toBe('asset file');
+ });
+
+ it('copies multiple assets with a recursive wildcard glob pattern', async () => {
+ await harness.writeFiles({
+ 'src/files/test.svg': ' ',
+ 'src/files/another.file': 'asset file',
+ 'src/files/nested/extra.file': 'extra file',
+ });
+
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ assets: [{ glob: '**/*', input: 'src/files', output: '.' }],
+ });
+
+ const { result } = await harness.executeOnce();
+
+ expect(result?.success).toBe(true);
+
+ harness.expectFile('dist/test.svg').content.toBe(' ');
+ harness.expectFile('dist/another.file').content.toBe('asset file');
+ harness.expectFile('dist/nested/extra.file').content.toBe('extra file');
+ });
+
+ it('automatically ignores "." prefixed files when using wildcard glob pattern', async () => {
+ await harness.writeFile('src/files/.gitkeep', '');
+
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ assets: [{ glob: '*', input: 'src/files', output: '.' }],
+ });
+
+ const { result } = await harness.executeOnce();
+
+ expect(result?.success).toBe(true);
+
+ harness.expectFile('dist/.gitkeep').toNotExist();
+ });
+
+ it('supports ignoring a specific file when using a glob pattern', async () => {
+ await harness.writeFiles({
+ 'src/files/test.svg': ' ',
+ 'src/files/another.file': 'asset file',
+ 'src/files/nested/extra.file': 'extra file',
+ });
+
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ assets: [
+ { glob: '**/*', input: 'src/files', output: '.', ignore: ['another.file'] },
+ ],
+ });
+
+ const { result } = await harness.executeOnce();
+
+ expect(result?.success).toBe(true);
+
+ harness.expectFile('dist/test.svg').content.toBe(' ');
+ harness.expectFile('dist/another.file').toNotExist();
+ harness.expectFile('dist/nested/extra.file').content.toBe('extra file');
+ });
+
+ it('supports ignoring with a glob pattern when using a glob pattern', async () => {
+ await harness.writeFiles({
+ 'src/files/test.svg': ' ',
+ 'src/files/another.file': 'asset file',
+ 'src/files/nested/extra.file': 'extra file',
+ });
+
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ assets: [
+ { glob: '**/*', input: 'src/files', output: '.', ignore: ['**/*.file'] },
+ ],
+ });
+
+ const { result } = await harness.executeOnce();
+
+ expect(result?.success).toBe(true);
+
+ harness.expectFile('dist/test.svg').content.toBe(' ');
+ harness.expectFile('dist/another.file').toNotExist();
+ harness.expectFile('dist/nested/extra.file').toNotExist();
+ });
+
+ it('copies an asset with directory and maintains directory in output', async () => {
+ await harness.writeFile('src/subdirectory/test.svg', ' ');
+
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ assets: [{ glob: 'subdirectory/test.svg', input: 'src', output: '.' }],
+ });
+
+ const { result } = await harness.executeOnce();
+
+ expect(result?.success).toBe(true);
+
+ harness.expectFile('dist/subdirectory/test.svg').content.toBe(' ');
+ });
+
+ it('does not fail if asset does not exist', async () => {
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ assets: [{ glob: 'test.svg', input: 'src', output: '.' }],
+ });
+
+ const { result } = await harness.executeOnce();
+
+ expect(result?.success).toBe(true);
+
+ harness.expectFile('dist/test.svg').toNotExist();
+ });
+
+ it('uses project output path when output option is empty string', async () => {
+ await harness.writeFile('src/test.svg', ' ');
+
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ assets: [{ glob: 'test.svg', input: 'src', output: '' }],
+ });
+
+ const { result } = await harness.executeOnce();
+
+ expect(result?.success).toBe(true);
+
+ harness.expectFile('dist/test.svg').content.toBe(' ');
+ });
+
+ it('uses project output path when output option is "."', async () => {
+ await harness.writeFile('src/test.svg', ' ');
+
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ assets: [{ glob: 'test.svg', input: 'src', output: '.' }],
+ });
+
+ const { result } = await harness.executeOnce();
+
+ expect(result?.success).toBe(true);
+
+ harness.expectFile('dist/test.svg').content.toBe(' ');
+ });
+
+ it('uses project output path when output option is "/"', async () => {
+ await harness.writeFile('src/test.svg', ' ');
+
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ assets: [{ glob: 'test.svg', input: 'src', output: '/' }],
+ });
+
+ const { result } = await harness.executeOnce();
+
+ expect(result?.success).toBe(true);
+
+ harness.expectFile('dist/test.svg').content.toBe(' ');
+ });
+
+ it('creates a project output sub-path when output option path does not exist', async () => {
+ await harness.writeFile('src/test.svg', ' ');
+
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ assets: [{ glob: 'test.svg', input: 'src', output: 'subdirectory' }],
+ });
+
+ const { result } = await harness.executeOnce();
+
+ expect(result?.success).toBe(true);
+
+ harness.expectFile('dist/subdirectory/test.svg').content.toBe(' ');
+ });
+
+ it('throws exception if output option is not within project output path', async () => {
+ await harness.writeFile('test.svg', ' ');
+
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ assets: [{ glob: 'test.svg', input: 'src', output: '..' }],
+ });
+
+ const { result, error } = await harness.executeOnce({ outputLogsOnException: false });
+
+ expect(result).toBeUndefined();
+ expect(error).toEqual(
+ jasmine.objectContaining({
+ message: jasmine.stringMatching(
+ 'An asset cannot be written to a location outside of the output path',
+ ),
+ }),
+ );
+
+ harness.expectFile('dist/test.svg').toNotExist();
+ });
+ });
+ });
+});
diff --git a/packages/angular_devkit/build_angular/src/browser/tests/options/extract-licenses_spec.ts b/packages/angular_devkit/build_angular/src/browser/tests/options/extract-licenses_spec.ts
new file mode 100644
index 000000000000..5a978435e76b
--- /dev/null
+++ b/packages/angular_devkit/build_angular/src/browser/tests/options/extract-licenses_spec.ts
@@ -0,0 +1,45 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+import { buildWebpackBrowser } from '../../index';
+import { BASE_OPTIONS, BROWSER_BUILDER_INFO, describeBuilder } from '../setup';
+
+describeBuilder(buildWebpackBrowser, BROWSER_BUILDER_INFO, (harness) => {
+ describe('Option: "extractLicenses"', () => {
+ it(`should generate '3rdpartylicenses.txt' when 'extractLicenses' is true`, async () => {
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ extractLicenses: true,
+ });
+
+ const { result } = await harness.executeOnce();
+ expect(result?.success).toBe(true);
+ harness.expectFile('dist/3rdpartylicenses.txt').content.toContain('MIT');
+ });
+
+ it(`should not generate '3rdpartylicenses.txt' when 'extractLicenses' is false`, async () => {
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ extractLicenses: false,
+ });
+
+ const { result } = await harness.executeOnce();
+ expect(result?.success).toBe(true);
+ harness.expectFile('dist/3rdpartylicenses.txt').toNotExist();
+ });
+
+ it(`should not generate '3rdpartylicenses.txt' when 'extractLicenses' is not set`, async () => {
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ });
+
+ const { result } = await harness.executeOnce();
+ expect(result?.success).toBe(true);
+ harness.expectFile('dist/3rdpartylicenses.txt').toNotExist();
+ });
+ });
+});
diff --git a/packages/angular_devkit/build_angular/src/browser/tests/options/main_spec.ts b/packages/angular_devkit/build_angular/src/browser/tests/options/main_spec.ts
new file mode 100644
index 000000000000..62d8d64ab3fc
--- /dev/null
+++ b/packages/angular_devkit/build_angular/src/browser/tests/options/main_spec.ts
@@ -0,0 +1,68 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+import { buildWebpackBrowser } from '../../index';
+import { BASE_OPTIONS, BROWSER_BUILDER_INFO, describeBuilder } from '../setup';
+
+describeBuilder(buildWebpackBrowser, BROWSER_BUILDER_INFO, (harness) => {
+ describe('Option: "main"', () => {
+ it('uses a provided TypeScript file', async () => {
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ main: 'src/main.ts',
+ });
+
+ const { result } = await harness.executeOnce();
+
+ expect(result?.success).toBe(true);
+
+ harness.expectFile('dist/runtime.js').toExist();
+ harness.expectFile('dist/main.js').toExist();
+ harness.expectFile('dist/vendor.js').toExist();
+ harness.expectFile('dist/index.html').toExist();
+ });
+
+ it('uses a provided JavaScript file', async () => {
+ await harness.writeFile('src/main.js', `console.log('main');`);
+
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ main: 'src/main.js',
+ });
+
+ const { result } = await harness.executeOnce();
+
+ expect(result?.success).toBe(true);
+
+ harness.expectFile('dist/runtime.js').toExist();
+ harness.expectFile('dist/main.js').toExist();
+ harness.expectFile('dist/vendor.js').toNotExist();
+ harness.expectFile('dist/index.html').toExist();
+
+ harness.expectFile('dist/main.js').content.toContain(`console.log('main')`);
+ });
+
+ it('fails and shows an error when file does not exist', async () => {
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ main: 'src/missing.ts',
+ });
+
+ const { result, logs } = await harness.executeOnce({ outputLogsOnFailure: false });
+
+ expect(result?.success).toBe(false);
+ expect(logs).toContain(
+ jasmine.objectContaining({ message: jasmine.stringMatching('Module not found:') }),
+ );
+
+ harness.expectFile('dist/runtime.js').toNotExist();
+ harness.expectFile('dist/main.js').toNotExist();
+ harness.expectFile('dist/vendor.js').toNotExist();
+ harness.expectFile('dist/index.html').toNotExist();
+ });
+ });
+});
diff --git a/packages/angular_devkit/build_angular/src/browser/tests/options/output-hashing_spec.ts b/packages/angular_devkit/build_angular/src/browser/tests/options/output-hashing_spec.ts
new file mode 100644
index 000000000000..14db3ee47213
--- /dev/null
+++ b/packages/angular_devkit/build_angular/src/browser/tests/options/output-hashing_spec.ts
@@ -0,0 +1,176 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+
+import { buildWebpackBrowser } from '../../index';
+import { OutputHashing } from '../../schema';
+import { BASE_OPTIONS, BROWSER_BUILDER_INFO, describeBuilder } from '../setup';
+
+describeBuilder(buildWebpackBrowser, BROWSER_BUILDER_INFO, (harness) => {
+ describe('Option: "outputHashing"', () => {
+ beforeEach(async () => {
+ // Application code is not needed for asset tests
+ await harness.writeFile('src/main.ts', '');
+ });
+
+ it('hashes all filenames when set to "all"', async () => {
+ await harness.writeFile(
+ 'src/styles.css',
+ `h1 { background: url('./spectrum.png')}`,
+ );
+
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ styles: ['src/styles.css'],
+ outputHashing: OutputHashing.All,
+ });
+
+ const { result } = await harness.executeOnce();
+ expect(result?.success).toBe(true);
+
+ expect(harness.hasFileMatch('dist', /runtime\.[0-9a-f]{20}\.js$/)).toBeTrue();
+ expect(harness.hasFileMatch('dist', /main\.[0-9a-f]{20}\.js$/)).toBeTrue();
+ expect(harness.hasFileMatch('dist', /polyfills\.[0-9a-f]{20}\.js$/)).toBeTrue();
+ expect(harness.hasFileMatch('dist', /styles\.[0-9a-f]{20}\.css$/)).toBeTrue();
+ expect(harness.hasFileMatch('dist', /spectrum\.[0-9a-f]{20}\.png$/)).toBeTrue();
+ });
+
+ it(`doesn't hash any filenames when not set`, async () => {
+ await harness.writeFile(
+ 'src/styles.css',
+ `h1 { background: url('./spectrum.png')}`,
+ );
+
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ styles: ['src/styles.css'],
+ });
+
+ const { result } = await harness.executeOnce();
+ expect(result?.success).toBe(true);
+
+ expect(harness.hasFileMatch('dist', /runtime\.[0-9a-f]{20}\.js$/)).toBeFalse();
+ expect(harness.hasFileMatch('dist', /main\.[0-9a-f]{20}\.js$/)).toBeFalse();
+ expect(harness.hasFileMatch('dist', /polyfills\.[0-9a-f]{20}\.js$/)).toBeFalse();
+ expect(harness.hasFileMatch('dist', /styles\.[0-9a-f]{20}\.css$/)).toBeFalse();
+ expect(harness.hasFileMatch('dist', /spectrum\.[0-9a-f]{20}\.png$/)).toBeFalse();
+ });
+
+ it(`doesn't hash any filenames when set to "none"`, async () => {
+ await harness.writeFile(
+ 'src/styles.css',
+ `h1 { background: url('./spectrum.png')}`,
+ );
+
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ styles: ['src/styles.css'],
+ outputHashing: OutputHashing.None,
+ });
+
+ const { result } = await harness.executeOnce();
+ expect(result?.success).toBe(true);
+
+ expect(harness.hasFileMatch('dist', /runtime\.[0-9a-f]{20}\.js$/)).toBeFalse();
+ expect(harness.hasFileMatch('dist', /main\.[0-9a-f]{20}\.js$/)).toBeFalse();
+ expect(harness.hasFileMatch('dist', /polyfills\.[0-9a-f]{20}\.js$/)).toBeFalse();
+ expect(harness.hasFileMatch('dist', /styles\.[0-9a-f]{20}\.css$/)).toBeFalse();
+ expect(harness.hasFileMatch('dist', /spectrum\.[0-9a-f]{20}\.png$/)).toBeFalse();
+ });
+
+ it(`hashes CSS resources filenames only when set to "media"`, async () => {
+ await harness.writeFile(
+ 'src/styles.css',
+ `h1 { background: url('./spectrum.png')}`,
+ );
+
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ styles: ['src/styles.css'],
+ outputHashing: OutputHashing.Media,
+ });
+
+ const { result } = await harness.executeOnce();
+ expect(result?.success).toBe(true);
+
+ expect(harness.hasFileMatch('dist', /runtime\.[0-9a-f]{20}\.js$/)).toBeFalse();
+ expect(harness.hasFileMatch('dist', /main\.[0-9a-f]{20}\.js$/)).toBeFalse();
+ expect(harness.hasFileMatch('dist', /polyfills\.[0-9a-f]{20}\.js$/)).toBeFalse();
+ expect(harness.hasFileMatch('dist', /styles\.[0-9a-f]{20}\.css$/)).toBeFalse();
+ expect(harness.hasFileMatch('dist', /spectrum\.[0-9a-f]{20}\.png$/)).toBeTrue();
+ });
+
+ it(`hashes bundles filenames only when set to "bundles"`, async () => {
+ await harness.writeFile(
+ 'src/styles.css',
+ `h1 { background: url('./spectrum.png')}`,
+ );
+
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ styles: ['src/styles.css'],
+ outputHashing: OutputHashing.Bundles,
+ });
+
+ const { result } = await harness.executeOnce();
+ expect(result?.success).toBe(true);
+
+ expect(harness.hasFileMatch('dist', /runtime\.[0-9a-f]{20}\.js$/)).toBeTrue();
+ expect(harness.hasFileMatch('dist', /main\.[0-9a-f]{20}\.js$/)).toBeTrue();
+ expect(harness.hasFileMatch('dist', /polyfills\.[0-9a-f]{20}\.js$/)).toBeTrue();
+ expect(harness.hasFileMatch('dist', /styles\.[0-9a-f]{20}\.css$/)).toBeTrue();
+ expect(harness.hasFileMatch('dist', /spectrum\.[0-9a-f]{20}\.png$/)).toBeFalse();
+ });
+
+ it('does not hash non injected styles', async () => {
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ outputHashing: OutputHashing.All,
+ styles: [{
+ input: 'src/styles.css',
+ inject: false,
+ }],
+ });
+
+ const { result } = await harness.executeOnce();
+ expect(result?.success).toBe(true);
+
+ expect(harness.hasFileMatch('dist', /styles\.[0-9a-f]{20}\.js$/)).toBeFalse();
+ expect(harness.hasFileMatch('dist', /styles\.[0-9a-f]{20}\.js.map$/)).toBeFalse();
+ harness.expectFile('dist/styles.css').toExist();
+ harness.expectFile('dist/styles.css.map').toExist();
+ });
+
+ it('does not override different files which has the same filenames when hashing is "none"', async () => {
+ await harness.writeFiles({
+ 'src/styles.css': `
+ h1 { background: url('./test.svg')}
+ h2 { background: url('./small/test.svg')}
+ `,
+ './src/test.svg': `
+ Hello World
+ `,
+ './src/small/test.svg': `
+ Hello World
+ `,
+ });
+
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ styles: ['src/styles.css'],
+ outputHashing: OutputHashing.None,
+ });
+
+ const { result } = await harness.executeOnce();
+ expect(result?.success).toBe(true);
+
+ harness.expectFile('dist/test.svg').toExist();
+ harness.expectFile('dist/small-test.svg').toExist();
+ });
+ });
+});
diff --git a/packages/angular_devkit/build_angular/src/browser/tests/options/polyfills_spec.ts b/packages/angular_devkit/build_angular/src/browser/tests/options/polyfills_spec.ts
new file mode 100644
index 000000000000..587fe85be84c
--- /dev/null
+++ b/packages/angular_devkit/build_angular/src/browser/tests/options/polyfills_spec.ts
@@ -0,0 +1,57 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+import { buildWebpackBrowser } from '../../index';
+import { BASE_OPTIONS, BROWSER_BUILDER_INFO, describeBuilder } from '../setup';
+
+describeBuilder(buildWebpackBrowser, BROWSER_BUILDER_INFO, (harness) => {
+ describe('Option: "polyfills"', () => {
+ it('uses a provided TypeScript file', async () => {
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ polyfills: 'src/polyfills.ts',
+ });
+
+ const { result } = await harness.executeOnce();
+
+ expect(result?.success).toBe(true);
+
+ harness.expectFile('dist/polyfills.js').toExist();
+ });
+
+ it('uses a provided JavaScript file', async () => {
+ await harness.writeFile('src/polyfills.js', `console.log('main');`);
+
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ polyfills: 'src/polyfills.js',
+ });
+
+ const { result } = await harness.executeOnce();
+
+ expect(result?.success).toBe(true);
+
+ harness.expectFile('dist/polyfills.js').content.toContain(`console.log('main')`);
+ });
+
+ it('fails and shows an error when file does not exist', async () => {
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ polyfills: 'src/missing.ts',
+ });
+
+ const { result, logs } = await harness.executeOnce({ outputLogsOnFailure: false });
+
+ expect(result?.success).toBe(false);
+ expect(logs).toContain(
+ jasmine.objectContaining({ message: jasmine.stringMatching('Module not found:') }),
+ );
+
+ harness.expectFile('dist/polyfills.js').toNotExist();
+ });
+ });
+});
diff --git a/packages/angular_devkit/build_angular/src/browser/tests/options/scripts_spec.ts b/packages/angular_devkit/build_angular/src/browser/tests/options/scripts_spec.ts
new file mode 100644
index 000000000000..abe0b4cc1b47
--- /dev/null
+++ b/packages/angular_devkit/build_angular/src/browser/tests/options/scripts_spec.ts
@@ -0,0 +1,416 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+// tslint:disable:no-big-function
+import { buildWebpackBrowser } from '../../index';
+import { BASE_OPTIONS, BROWSER_BUILDER_INFO, describeBuilder } from '../setup';
+
+describeBuilder(buildWebpackBrowser, BROWSER_BUILDER_INFO, (harness) => {
+ describe('Option: "scripts"', () => {
+ beforeEach(async () => {
+ // Application code is not needed for scripts tests
+ await harness.writeFile('src/main.ts', '');
+ });
+
+ it('supports an empty array value', async () => {
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ scripts: [],
+ });
+
+ const { result } = await harness.executeOnce();
+
+ expect(result?.success).toBe(true);
+ });
+
+ describe('shorthand syntax', () => {
+ it('processes a single script into a single output', async () => {
+ await harness.writeFile('src/test-script-a.js', 'console.log("a");');
+
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ scripts: ['src/test-script-a.js'],
+ });
+
+ const { result } = await harness.executeOnce();
+
+ expect(result?.success).toBe(true);
+
+ harness.expectFile('dist/scripts.js').content.toContain('console.log("a")');
+ harness
+ .expectFile('dist/index.html')
+ .content.toContain('');
+ });
+
+ it('processes multiple scripts into a single output', async () => {
+ await harness.writeFile('src/test-script-a.js', 'console.log("a");');
+ await harness.writeFile('src/test-script-b.js', 'console.log("b");');
+
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ scripts: ['src/test-script-a.js', 'src/test-script-b.js'],
+ });
+
+ const { result } = await harness.executeOnce();
+
+ expect(result?.success).toBe(true);
+
+ harness.expectFile('dist/scripts.js').content.toContain('console.log("a")');
+ harness.expectFile('dist/scripts.js').content.toContain('console.log("b")');
+ harness
+ .expectFile('dist/index.html')
+ .content.toContain('');
+ });
+
+ it('preserves order of multiple scripts in single output', async () => {
+ await harness.writeFile('src/test-script-a.js', 'console.log("a");');
+ await harness.writeFile('src/test-script-b.js', 'console.log("b");');
+ await harness.writeFile('src/test-script-c.js', 'console.log("c");');
+ await harness.writeFile('src/test-script-d.js', 'console.log("d");');
+
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ scripts: [
+ 'src/test-script-c.js',
+ 'src/test-script-d.js',
+ 'src/test-script-b.js',
+ 'src/test-script-a.js',
+ ],
+ });
+
+ const { result } = await harness.executeOnce();
+
+ expect(result?.success).toBe(true);
+
+ harness
+ .expectFile('dist/scripts.js')
+ .content.toMatch(
+ /console\.log\("c"\)[;\s]+console\.log\("d"\)[;\s]+console\.log\("b"\)[;\s]+console\.log\("a"\)/,
+ );
+ });
+
+ it('throws an exception if script does not exist', async () => {
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ scripts: ['src/test-script-a.js'],
+ });
+
+ const { result, error } = await harness.executeOnce({ outputLogsOnException: false });
+
+ expect(result).toBeUndefined();
+ expect(error).toEqual(
+ jasmine.objectContaining({
+ message: jasmine.stringMatching(`Script file src/test-script-a.js does not exist.`),
+ }),
+ );
+
+ harness.expectFile('dist/scripts.js').toNotExist();
+ });
+
+ it('shows the output script as a chunk entry in the logging output', async () => {
+ await harness.writeFile('src/test-script-a.js', 'console.log("a");');
+
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ scripts: ['src/test-script-a.js'],
+ });
+
+ const { result, logs } = await harness.executeOnce();
+
+ expect(result?.success).toBe(true);
+
+ expect(logs).toContain(
+ jasmine.objectContaining({ message: jasmine.stringMatching(/scripts\.js.+\d+ bytes/) }),
+ );
+ });
+ });
+
+ describe('longhand syntax', () => {
+ it('processes a single script into a single output', async () => {
+ await harness.writeFile('src/test-script-a.js', 'console.log("a");');
+
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ scripts: [{ input: 'src/test-script-a.js' }],
+ });
+
+ const { result } = await harness.executeOnce();
+
+ expect(result?.success).toBe(true);
+
+ harness.expectFile('dist/scripts.js').content.toContain('console.log("a")');
+ harness
+ .expectFile('dist/index.html')
+ .content.toContain('');
+ });
+
+ it('processes a single script into a single output named with bundleName', async () => {
+ await harness.writeFile('src/test-script-a.js', 'console.log("a");');
+
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ scripts: [{ input: 'src/test-script-a.js', bundleName: 'extra' }],
+ });
+
+ const { result } = await harness.executeOnce();
+
+ expect(result?.success).toBe(true);
+
+ harness.expectFile('dist/extra.js').content.toContain('console.log("a")');
+ harness
+ .expectFile('dist/index.html')
+ .content.toContain('');
+ });
+
+ it('uses default bundleName when bundleName is empty string', async () => {
+ await harness.writeFile('src/test-script-a.js', 'console.log("a");');
+
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ scripts: [{ input: 'src/test-script-a.js', bundleName: '' }],
+ });
+
+ const { result } = await harness.executeOnce();
+
+ expect(result?.success).toBe(true);
+
+ harness.expectFile('dist/scripts.js').content.toContain('console.log("a")');
+ harness
+ .expectFile('dist/index.html')
+ .content.toContain('');
+ });
+
+ it('processes multiple scripts with no bundleName into a single output', async () => {
+ await harness.writeFiles({
+ 'src/test-script-a.js': 'console.log("a");',
+ 'src/test-script-b.js': 'console.log("b");',
+ });
+
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ scripts: [{ input: 'src/test-script-a.js' }, { input: 'src/test-script-b.js' }],
+ });
+
+ const { result } = await harness.executeOnce();
+
+ expect(result?.success).toBe(true);
+
+ harness.expectFile('dist/scripts.js').content.toContain('console.log("a")');
+ harness.expectFile('dist/scripts.js').content.toContain('console.log("b")');
+ harness
+ .expectFile('dist/index.html')
+ .content.toContain('');
+ });
+
+ it('processes multiple scripts with same bundleName into a single output', async () => {
+ await harness.writeFiles({
+ 'src/test-script-a.js': 'console.log("a");',
+ 'src/test-script-b.js': 'console.log("b");',
+ });
+
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ scripts: [
+ { input: 'src/test-script-a.js', bundleName: 'extra' },
+ { input: 'src/test-script-b.js', bundleName: 'extra' },
+ ],
+ });
+
+ const { result } = await harness.executeOnce();
+
+ expect(result?.success).toBe(true);
+
+ harness.expectFile('dist/extra.js').content.toContain('console.log("a")');
+ harness.expectFile('dist/extra.js').content.toContain('console.log("b")');
+ harness
+ .expectFile('dist/index.html')
+ .content.toContain('');
+ });
+
+ it('processes multiple scripts with different bundleNames into separate outputs', async () => {
+ await harness.writeFiles({
+ 'src/test-script-a.js': 'console.log("a");',
+ 'src/test-script-b.js': 'console.log("b");',
+ });
+
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ scripts: [
+ { input: 'src/test-script-a.js', bundleName: 'extra' },
+ { input: 'src/test-script-b.js', bundleName: 'other' },
+ ],
+ });
+
+ const { result } = await harness.executeOnce();
+
+ expect(result?.success).toBe(true);
+
+ harness.expectFile('dist/extra.js').content.toContain('console.log("a")');
+ harness.expectFile('dist/other.js').content.toContain('console.log("b")');
+ harness
+ .expectFile('dist/index.html')
+ .content.toContain('');
+ harness
+ .expectFile('dist/index.html')
+ .content.toContain('');
+ });
+
+ it('preserves order of multiple scripts in single output', async () => {
+ await harness.writeFiles({
+ 'src/test-script-a.js': 'console.log("a");',
+ 'src/test-script-b.js': 'console.log("b");',
+ 'src/test-script-c.js': 'console.log("c");',
+ 'src/test-script-d.js': 'console.log("d");',
+ });
+
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ scripts: [
+ { input: 'src/test-script-c.js' },
+ { input: 'src/test-script-d.js' },
+ { input: 'src/test-script-b.js' },
+ { input: 'src/test-script-a.js' },
+ ],
+ });
+
+ const { result } = await harness.executeOnce();
+
+ expect(result?.success).toBe(true);
+
+ harness
+ .expectFile('dist/scripts.js')
+ .content.toMatch(
+ /console\.log\("c"\)[;\s]+console\.log\("d"\)[;\s]+console\.log\("b"\)[;\s]+console\.log\("a"\)/,
+ );
+ });
+
+ it('preserves order of multiple scripts with different bundleNames', async () => {
+ await harness.writeFiles({
+ 'src/test-script-a.js': 'console.log("a");',
+ 'src/test-script-b.js': 'console.log("b");',
+ 'src/test-script-c.js': 'console.log("c");',
+ 'src/test-script-d.js': 'console.log("d");',
+ });
+
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ scripts: [
+ { input: 'src/test-script-c.js', bundleName: 'other' },
+ { input: 'src/test-script-d.js', bundleName: 'extra' },
+ { input: 'src/test-script-b.js', bundleName: 'extra' },
+ { input: 'src/test-script-a.js', bundleName: 'other' },
+ ],
+ });
+
+ const { result } = await harness.executeOnce();
+
+ expect(result?.success).toBe(true);
+
+ harness
+ .expectFile('dist/other.js')
+ .content.toMatch(/console\.log\("c"\)[;\s]+console\.log\("a"\)/);
+ harness
+ .expectFile('dist/extra.js')
+ .content.toMatch(/console\.log\("d"\)[;\s]+console\.log\("b"\)/);
+ harness
+ .expectFile('dist/index.html')
+ .content.toMatch(
+ /');
+ });
+
+ it('does not add script element to index when inject is false', async () => {
+ await harness.writeFile('src/test-script-a.js', 'console.log("a");');
+
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ scripts: [{ input: 'src/test-script-a.js', inject: false }],
+ });
+
+ const { result } = await harness.executeOnce();
+
+ expect(result?.success).toBe(true);
+
+ // `inject: false` causes the bundleName to be the input file name
+ harness.expectFile('dist/test-script-a.js').content.toContain('console.log("a")');
+ harness
+ .expectFile('dist/index.html')
+ .content.not.toContain('');
+ });
+
+ it('does not add script element to index with bundleName when inject is false', async () => {
+ await harness.writeFile('src/test-script-a.js', 'console.log("a");');
+
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ scripts: [{ input: 'src/test-script-a.js', bundleName: 'extra', inject: false }],
+ });
+
+ const { result } = await harness.executeOnce();
+
+ expect(result?.success).toBe(true);
+
+ harness.expectFile('dist/extra.js').content.toContain('console.log("a")');
+ harness
+ .expectFile('dist/index.html')
+ .content.not.toContain('');
+ });
+
+ it('shows the output script as a chunk entry in the logging output', async () => {
+ await harness.writeFile('src/test-script-a.js', 'console.log("a");');
+
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ scripts: [{ input: 'src/test-script-a.js' }],
+ });
+
+ const { result, logs } = await harness.executeOnce();
+
+ expect(result?.success).toBe(true);
+
+ expect(logs).toContain(
+ jasmine.objectContaining({ message: jasmine.stringMatching(/scripts\.js.+\d+ bytes/) }),
+ );
+ });
+
+ it('shows the output script as a chunk entry with bundleName in the logging output', async () => {
+ await harness.writeFile('src/test-script-a.js', 'console.log("a");');
+
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ scripts: [{ input: 'src/test-script-a.js', bundleName: 'extra' }],
+ });
+
+ const { result, logs } = await harness.executeOnce();
+
+ expect(result?.success).toBe(true);
+
+ expect(logs).toContain(
+ jasmine.objectContaining({ message: jasmine.stringMatching(/extra\.js.+\d+ bytes/) }),
+ );
+ });
+ });
+ });
+});
diff --git a/packages/angular_devkit/build_angular/src/browser/tests/options/stats-json_spec.ts b/packages/angular_devkit/build_angular/src/browser/tests/options/stats-json_spec.ts
new file mode 100644
index 000000000000..177799a51171
--- /dev/null
+++ b/packages/angular_devkit/build_angular/src/browser/tests/options/stats-json_spec.ts
@@ -0,0 +1,77 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+import { buildWebpackBrowser } from '../../index';
+import { BASE_OPTIONS, BROWSER_BUILDER_INFO, describeBuilder } from '../setup';
+
+describeBuilder(buildWebpackBrowser, BROWSER_BUILDER_INFO, (harness) => {
+ describe('Option: "statsJson"', () => {
+ beforeEach(async () => {
+ // Application code is not needed for stat JSON tests
+ await harness.writeFile('src/main.ts', '');
+ });
+
+ it('generates a Webpack Stats file in output when true', async () => {
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ statsJson: true,
+ });
+
+ const { result } = await harness.executeOnce();
+
+ expect(result?.success).toBe(true);
+
+ if (harness.expectFile('dist/stats.json').toExist()) {
+ const content = harness.readFile('dist/stats.json');
+ expect(() => JSON.parse(content))
+ .withContext('Expected Webpack Stats file to be valid JSON.')
+ .not.toThrow();
+ }
+ });
+
+ it('includes Webpack profiling information', async () => {
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ statsJson: true,
+ });
+
+ const { result } = await harness.executeOnce();
+
+ expect(result?.success).toBe(true);
+
+ if (harness.expectFile('dist/stats.json').toExist()) {
+ const stats = JSON.parse(harness.readFile('dist/stats.json'));
+ expect(stats?.chunks?.[0]?.modules?.[0]?.profile?.building).toBeDefined();
+ }
+ });
+
+ it('does not generate a Webpack Stats file in output when false', async () => {
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ statsJson: false,
+ });
+
+ const { result } = await harness.executeOnce();
+
+ expect(result?.success).toBe(true);
+
+ harness.expectFile('dist/stats.json').toNotExist();
+ });
+
+ it('does not generate a Webpack Stats file in output when not present', async () => {
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ });
+
+ const { result } = await harness.executeOnce();
+
+ expect(result?.success).toBe(true);
+
+ harness.expectFile('dist/stats.json').toNotExist();
+ });
+ });
+});
diff --git a/packages/angular_devkit/build_angular/src/browser/tests/options/styles_spec.ts b/packages/angular_devkit/build_angular/src/browser/tests/options/styles_spec.ts
new file mode 100644
index 000000000000..e726fbaf565a
--- /dev/null
+++ b/packages/angular_devkit/build_angular/src/browser/tests/options/styles_spec.ts
@@ -0,0 +1,432 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+// tslint:disable:no-big-function
+import { buildWebpackBrowser } from '../../index';
+import { BASE_OPTIONS, BROWSER_BUILDER_INFO, describeBuilder } from '../setup';
+
+describeBuilder(buildWebpackBrowser, BROWSER_BUILDER_INFO, (harness) => {
+ describe('Option: "styles"', () => {
+ beforeEach(async () => {
+ // Application code is not needed for styles tests
+ await harness.writeFile('src/main.ts', '');
+ });
+
+ it('supports an empty array value', async () => {
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ styles: [],
+ });
+
+ const { result } = await harness.executeOnce();
+
+ expect(result?.success).toBe(true);
+
+ harness.expectFile('dist/styles.css').toNotExist();
+ });
+
+ it('does not create an output styles file when option is not present', async () => {
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ });
+
+ const { result } = await harness.executeOnce();
+
+ expect(result?.success).toBe(true);
+
+ harness.expectFile('dist/styles.css').toNotExist();
+ });
+
+ describe('shorthand syntax', () => {
+ it('processes a single style into a single output', async () => {
+ await harness.writeFile('src/test-style-a.css', '.test-a {color: red}');
+
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ styles: ['src/test-style-a.css'],
+ });
+
+ const { result } = await harness.executeOnce();
+
+ expect(result?.success).toBe(true);
+
+ harness.expectFile('dist/styles.css').content.toContain('.test-a {color: red}');
+ harness
+ .expectFile('dist/index.html')
+ .content.toContain(' ');
+ });
+
+ it('processes multiple styles into a single output', async () => {
+ await harness.writeFiles({
+ 'src/test-style-a.css': '.test-a {color: red}',
+ 'src/test-style-b.css': '.test-b {color: green}',
+ });
+
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ styles: ['src/test-style-a.css', 'src/test-style-b.css'],
+ });
+
+ const { result } = await harness.executeOnce();
+
+ expect(result?.success).toBe(true);
+
+ harness.expectFile('dist/styles.css').content.toContain('.test-a {color: red}');
+ harness.expectFile('dist/styles.css').content.toContain('.test-b {color: green}');
+ harness
+ .expectFile('dist/index.html')
+ .content.toContain(' ');
+ });
+
+ it('preserves order of multiple styles in single output', async () => {
+ await harness.writeFiles({
+ 'src/test-style-a.css': '.test-a {color: red}',
+ 'src/test-style-b.css': '.test-b {color: green}',
+ 'src/test-style-c.css': '.test-c {color: blue}',
+ 'src/test-style-d.css': '.test-d {color: yellow}',
+ });
+
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ styles: [
+ 'src/test-style-c.css',
+ 'src/test-style-d.css',
+ 'src/test-style-b.css',
+ 'src/test-style-a.css',
+ ],
+ });
+
+ const { result } = await harness.executeOnce();
+
+ expect(result?.success).toBe(true);
+
+ harness
+ .expectFile('dist/styles.css')
+ .content.toMatch(
+ /\.test-c {color: blue}\s+\.test-d {color: yellow}\s+\.test-b {color: green}\s+\.test-a {color: red}/,
+ );
+ });
+
+ it('fails and shows an error if style does not exist', async () => {
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ styles: ['src/test-style-a.css'],
+ });
+
+ const { result, logs } = await harness.executeOnce({ outputLogsOnFailure: false });
+
+ expect(result?.success).toBeFalse();
+ expect(logs).toContain(
+ jasmine.objectContaining({ message: jasmine.stringMatching('Module not found:') }),
+ );
+
+ harness.expectFile('dist/styles.css').toNotExist();
+ });
+
+ it('shows the output style as a chunk entry in the logging output', async () => {
+ await harness.writeFile('src/test-style-a.css', '.test-a {color: red}');
+
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ styles: ['src/test-style-a.css'],
+ });
+
+ const { result, logs } = await harness.executeOnce();
+
+ expect(result?.success).toBe(true);
+
+ expect(logs).toContain(
+ jasmine.objectContaining({ message: jasmine.stringMatching(/styles\.css.+\d+ bytes/) }),
+ );
+ });
+ });
+
+ describe('longhand syntax', () => {
+ it('processes a single style into a single output', async () => {
+ await harness.writeFile('src/test-style-a.css', '.test-a {color: red}');
+
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ styles: [{ input: 'src/test-style-a.css' }],
+ });
+
+ const { result } = await harness.executeOnce();
+
+ expect(result?.success).toBe(true);
+
+ harness.expectFile('dist/styles.css').content.toContain('.test-a {color: red}');
+ harness
+ .expectFile('dist/index.html')
+ .content.toContain(' ');
+ });
+
+ it('processes a single style into a single output named with bundleName', async () => {
+ await harness.writeFile('src/test-style-a.css', '.test-a {color: red}');
+
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ styles: [{ input: 'src/test-style-a.css', bundleName: 'extra' }],
+ });
+
+ const { result } = await harness.executeOnce();
+
+ expect(result?.success).toBe(true);
+
+ harness.expectFile('dist/extra.css').content.toContain('.test-a {color: red}');
+ harness
+ .expectFile('dist/index.html')
+ .content.toContain(' ');
+ });
+
+ it('uses default bundleName when bundleName is empty string', async () => {
+ await harness.writeFile('src/test-style-a.css', '.test-a {color: red}');
+
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ styles: [{ input: 'src/test-style-a.css', bundleName: '' }],
+ });
+
+ const { result } = await harness.executeOnce();
+
+ expect(result?.success).toBe(true);
+
+ harness.expectFile('dist/styles.css').content.toContain('.test-a {color: red}');
+ harness
+ .expectFile('dist/index.html')
+ .content.toContain(' ');
+ });
+
+ it('processes multiple styles with no bundleName into a single output', async () => {
+ await harness.writeFiles({
+ 'src/test-style-a.css': '.test-a {color: red}',
+ 'src/test-style-b.css': '.test-b {color: green}',
+ });
+
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ styles: [{ input: 'src/test-style-a.css' }, { input: 'src/test-style-b.css' }],
+ });
+
+ const { result } = await harness.executeOnce();
+
+ expect(result?.success).toBe(true);
+
+ harness.expectFile('dist/styles.css').content.toContain('.test-a {color: red}');
+ harness.expectFile('dist/styles.css').content.toContain('.test-b {color: green}');
+ harness
+ .expectFile('dist/index.html')
+ .content.toContain(' ');
+ });
+
+ it('processes multiple styles with same bundleName into a single output', async () => {
+ await harness.writeFiles({
+ 'src/test-style-a.css': '.test-a {color: red}',
+ 'src/test-style-b.css': '.test-b {color: green}',
+ });
+
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ styles: [
+ { input: 'src/test-style-a.css', bundleName: 'extra' },
+ { input: 'src/test-style-b.css', bundleName: 'extra' },
+ ],
+ });
+
+ const { result } = await harness.executeOnce();
+
+ expect(result?.success).toBe(true);
+
+ harness.expectFile('dist/extra.css').content.toContain('.test-a {color: red}');
+ harness.expectFile('dist/extra.css').content.toContain('.test-b {color: green}');
+ harness
+ .expectFile('dist/index.html')
+ .content.toContain(' ');
+ });
+
+ it('processes multiple styles with different bundleNames into separate outputs', async () => {
+ await harness.writeFiles({
+ 'src/test-style-a.css': '.test-a {color: red}',
+ 'src/test-style-b.css': '.test-b {color: green}',
+ });
+
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ styles: [
+ { input: 'src/test-style-a.css', bundleName: 'extra' },
+ { input: 'src/test-style-b.css', bundleName: 'other' },
+ ],
+ });
+
+ const { result } = await harness.executeOnce();
+
+ expect(result?.success).toBe(true);
+
+ harness.expectFile('dist/extra.css').content.toContain('.test-a {color: red}');
+ harness.expectFile('dist/other.css').content.toContain('.test-b {color: green}');
+ harness
+ .expectFile('dist/index.html')
+ .content.toContain(' ');
+ harness
+ .expectFile('dist/index.html')
+ .content.toContain(' ');
+ });
+
+ it('preserves order of multiple styles in single output', async () => {
+ await harness.writeFiles({
+ 'src/test-style-a.css': '.test-a {color: red}',
+ 'src/test-style-b.css': '.test-b {color: green}',
+ 'src/test-style-c.css': '.test-c {color: blue}',
+ 'src/test-style-d.css': '.test-d {color: yellow}',
+ });
+
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ styles: [
+ { input: 'src/test-style-c.css' },
+ { input: 'src/test-style-d.css' },
+ { input: 'src/test-style-b.css' },
+ { input: 'src/test-style-a.css' },
+ ],
+ });
+
+ const { result } = await harness.executeOnce();
+
+ expect(result?.success).toBe(true);
+
+ harness
+ .expectFile('dist/styles.css')
+ .content.toMatch(
+ /\.test-c {color: blue}\s+\.test-d {color: yellow}\s+\.test-b {color: green}\s+\.test-a {color: red}/,
+ );
+ });
+
+ it('preserves order of multiple styles with different bundleNames', async () => {
+ await harness.writeFiles({
+ 'src/test-style-a.css': '.test-a {color: red}',
+ 'src/test-style-b.css': '.test-b {color: green}',
+ 'src/test-style-c.css': '.test-c {color: blue}',
+ 'src/test-style-d.css': '.test-d {color: yellow}',
+ });
+
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ styles: [
+ { input: 'src/test-style-c.css', bundleName: 'other' },
+ { input: 'src/test-style-d.css', bundleName: 'extra' },
+ { input: 'src/test-style-b.css', bundleName: 'extra' },
+ { input: 'src/test-style-a.css', bundleName: 'other' },
+ ],
+ });
+
+ const { result } = await harness.executeOnce();
+
+ expect(result?.success).toBe(true);
+
+ harness
+ .expectFile('dist/other.css')
+ .content.toMatch(/\.test-c {color: blue}\s+\.test-a {color: red}/);
+ harness
+ .expectFile('dist/extra.css')
+ .content.toMatch(/\.test-d {color: yellow}\s+\.test-b {color: green}/);
+ harness
+ .expectFile('dist/index.html')
+ .content.toMatch(
+ / \s* /,
+ );
+ });
+
+ it('adds link element to index when inject is true', async () => {
+ await harness.writeFile('src/test-style-a.css', '.test-a {color: red}');
+
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ styles: [{ input: 'src/test-style-a.css', inject: true }],
+ });
+
+ const { result } = await harness.executeOnce();
+
+ expect(result?.success).toBe(true);
+
+ harness.expectFile('dist/styles.css').content.toContain('.test-a {color: red}');
+ harness
+ .expectFile('dist/index.html')
+ .content.toContain(' ');
+ });
+
+ it('does not add link element to index when inject is false', async () => {
+ await harness.writeFile('src/test-style-a.css', '.test-a {color: red}');
+
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ styles: [{ input: 'src/test-style-a.css', inject: false }],
+ });
+
+ const { result } = await harness.executeOnce();
+
+ expect(result?.success).toBe(true);
+
+ // `inject: false` causes the bundleName to be the input file name
+ harness.expectFile('dist/test-style-a.css').content.toContain('.test-a {color: red}');
+ harness
+ .expectFile('dist/index.html')
+ .content.not.toContain(' ');
+ });
+
+ it('does not add link element to index with bundleName when inject is false', async () => {
+ await harness.writeFile('src/test-style-a.css', '.test-a {color: red}');
+
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ styles: [{ input: 'src/test-style-a.css', bundleName: 'extra', inject: false }],
+ });
+
+ const { result } = await harness.executeOnce();
+
+ expect(result?.success).toBe(true);
+
+ harness.expectFile('dist/extra.css').content.toContain('.test-a {color: red}');
+ harness
+ .expectFile('dist/index.html')
+ .content.not.toContain(' ');
+ });
+
+ it('shows the output style as a chunk entry in the logging output', async () => {
+ await harness.writeFile('src/test-style-a.css', '.test-a {color: red}');
+
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ styles: [{ input: 'src/test-style-a.css' }],
+ });
+
+ const { result, logs } = await harness.executeOnce();
+
+ expect(result?.success).toBe(true);
+
+ expect(logs).toContain(
+ jasmine.objectContaining({ message: jasmine.stringMatching(/styles\.css.+\d+ bytes/) }),
+ );
+ });
+
+ it('shows the output style as a chunk entry with bundleName in the logging output', async () => {
+ await harness.writeFile('src/test-style-a.css', '.test-a {color: red}');
+
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ styles: [{ input: 'src/test-style-a.css', bundleName: 'extra' }],
+ });
+
+ const { result, logs } = await harness.executeOnce();
+
+ expect(result?.success).toBe(true);
+
+ expect(logs).toContain(
+ jasmine.objectContaining({ message: jasmine.stringMatching(/extra\.css.+\d+ bytes/) }),
+ );
+ });
+ });
+ });
+});
diff --git a/packages/angular_devkit/build_angular/src/browser/tests/options/subresource-integrity_spec.ts b/packages/angular_devkit/build_angular/src/browser/tests/options/subresource-integrity_spec.ts
new file mode 100644
index 000000000000..3561aa2c332a
--- /dev/null
+++ b/packages/angular_devkit/build_angular/src/browser/tests/options/subresource-integrity_spec.ts
@@ -0,0 +1,70 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+// tslint:disable:no-big-function
+import { logging } from '@angular-devkit/core';
+import { buildWebpackBrowser } from '../../index';
+import { BASE_OPTIONS, BROWSER_BUILDER_INFO, describeBuilder } from '../setup';
+
+describeBuilder(buildWebpackBrowser, BROWSER_BUILDER_INFO, (harness) => {
+ describe('Option: "subresourceIntegrity"', () => {
+ it(`does not add integrity attribute when not present`, async () => {
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ });
+
+ const { result } = await harness.executeOnce();
+
+ expect(result?.success).toBe(true);
+ harness.expectFile('dist/index.html').content.not.toContain('integrity=');
+ });
+
+ it(`does not add integrity attribute when 'false'`, async () => {
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ subresourceIntegrity: false,
+ });
+
+ const { result } = await harness.executeOnce();
+
+ expect(result?.success).toBe(true);
+ harness.expectFile('dist/index.html').content.not.toContain('integrity=');
+ });
+
+ it(`does add integrity attribute when 'true'`, async () => {
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ subresourceIntegrity: true,
+ });
+
+ const { result } = await harness.executeOnce();
+
+ expect(result?.success).toBe(true);
+ harness.expectFile('dist/index.html').content.toMatch(/integrity="\w+-[A-Za-z0-9\/\+=]+"/);
+ });
+
+ it(`does not issue a warning when 'true' and 'scripts' is set.`, async () => {
+ await harness.writeFile('src/script.js', '');
+
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ subresourceIntegrity: true,
+ scripts: ['src/script.js'],
+ });
+
+ const { result, logs } = await harness.executeOnce();
+
+ expect(result?.success).toBe(true);
+ harness.expectFile('dist/index.html').content.toMatch(/integrity="\w+-[A-Za-z0-9\/\+=]+"/);
+ expect(logs).not.toContain(
+ jasmine.objectContaining({
+ message: jasmine.stringMatching(/subresource-integrity/),
+ }),
+ );
+ });
+ });
+});
diff --git a/packages/angular_devkit/build_angular/src/browser/tests/options/tsconfig_spec.ts b/packages/angular_devkit/build_angular/src/browser/tests/options/tsconfig_spec.ts
new file mode 100644
index 000000000000..289c5f780a01
--- /dev/null
+++ b/packages/angular_devkit/build_angular/src/browser/tests/options/tsconfig_spec.ts
@@ -0,0 +1,56 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+import { buildWebpackBrowser } from '../../index';
+import { BASE_OPTIONS, BROWSER_BUILDER_INFO, describeBuilder } from '../setup';
+
+describeBuilder(buildWebpackBrowser, BROWSER_BUILDER_INFO, (harness) => {
+ describe('Option: "tsConfig"', () => {
+ it('uses a provided TypeScript configuration file', async () => {
+ // Setup a TS file that uses ES2015+ const and then target ES5.
+ // The const usage should be downleveled in the output if the TS config is used.
+ await harness.writeFile('src/main.ts', 'const a = 5; console.log(a);');
+ await harness.writeFile(
+ 'src/tsconfig.option.json',
+ JSON.stringify({
+ compilerOptions: {
+ target: 'es5',
+ types: [],
+ },
+ files: ['main.ts'],
+ }),
+ );
+
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ tsConfig: 'src/tsconfig.option.json',
+ });
+
+ const { result } = await harness.executeOnce();
+
+ expect(result?.success).toBe(true);
+
+ harness.expectFile('dist/main.js').content.not.toContain('const');
+ });
+
+ it('throws an exception when TypeScript Configuration file does not exist', async () => {
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ tsConfig: 'src/missing.json',
+ });
+
+ const { result, error } = await harness.executeOnce({ outputLogsOnException: false });
+
+ expect(result).toBeUndefined();
+ expect(error).toEqual(
+ jasmine.objectContaining({
+ message: jasmine.stringMatching('no such file or directory'),
+ }),
+ );
+ });
+ });
+});
diff --git a/packages/angular_devkit/build_angular/src/browser/tests/options/watch_spec.ts b/packages/angular_devkit/build_angular/src/browser/tests/options/watch_spec.ts
new file mode 100644
index 000000000000..6b4bc73fb92d
--- /dev/null
+++ b/packages/angular_devkit/build_angular/src/browser/tests/options/watch_spec.ts
@@ -0,0 +1,103 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+import { concatMap, count, take, timeout } from 'rxjs/operators';
+import { buildWebpackBrowser } from '../../index';
+import { BASE_OPTIONS, BROWSER_BUILDER_INFO, describeBuilder } from '../setup';
+
+describeBuilder(buildWebpackBrowser, BROWSER_BUILDER_INFO, (harness) => {
+ describe('Option: "watch"', () => {
+ it('does not wait for file changes when false', (done) => {
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ watch: false,
+ });
+
+ // If the build waits then it will timeout with the custom timeout.
+ // A single build should not take more than 15 seconds.
+ let count = 0;
+ harness
+ .execute()
+ .pipe(timeout(15000))
+ .subscribe({
+ complete() {
+ expect(count).toBe(1);
+ done();
+ },
+ next({ result }) {
+ count++;
+ expect(result?.success).toBe(true);
+ },
+ error(error) {
+ done.fail(error);
+ },
+ });
+ });
+
+ it('does not wait for file changes when not present', (done) => {
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ });
+
+ // If the build waits then it will timeout with the custom timeout.
+ // A single build should not take more than 15 seconds.
+ let count = 0;
+ harness
+ .execute()
+ .pipe(timeout(15000))
+ .subscribe({
+ complete() {
+ expect(count).toBe(1);
+ done();
+ },
+ next({ result }) {
+ count++;
+ expect(result?.success).toBe(true);
+ },
+ error(error) {
+ done.fail(error);
+ },
+ });
+ });
+
+ it('watches for file changes when true', async () => {
+ harness.useTarget('build', {
+ ...BASE_OPTIONS,
+ main: 'src/main.ts',
+ watch: true,
+ });
+
+ const buildCount = await harness
+ .execute()
+ .pipe(
+ timeout(30000),
+ concatMap(async ({ result }, index) => {
+ expect(result?.success).toBe(true);
+
+ switch (index) {
+ case 0:
+ harness.expectFile('dist/main.js').content.not.toContain('abcd1234');
+
+ await harness.modifyFile(
+ 'src/main.ts',
+ (content) => content + 'console.log("abcd1234");',
+ );
+ break;
+ case 1:
+ harness.expectFile('dist/main.js').content.toContain('abcd1234');
+ break;
+ }
+ }),
+ take(2),
+ count(),
+ )
+ .toPromise();
+
+ expect(buildCount).toBe(2);
+ });
+ });
+});
diff --git a/packages/angular_devkit/build_angular/src/browser/tests/setup.ts b/packages/angular_devkit/build_angular/src/browser/tests/setup.ts
new file mode 100644
index 000000000000..d511bd2ac8ad
--- /dev/null
+++ b/packages/angular_devkit/build_angular/src/browser/tests/setup.ts
@@ -0,0 +1,27 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+import { Schema } from '../schema';
+
+export { describeBuilder } from '../../testing';
+
+export const BROWSER_BUILDER_INFO = Object.freeze({
+ name: '@angular-devkit/build-angular:browser',
+ schemaPath: __dirname + '/../schema.json',
+});
+
+/**
+ * Contains all required browser builder fields.
+ * Also disables progress reporting to minimize logging output.
+ */
+export const BASE_OPTIONS = Object.freeze({
+ index: 'src/index.html',
+ main: 'src/main.ts',
+ outputPath: 'dist',
+ tsConfig: 'src/tsconfig.app.json',
+ progress: false,
+});
diff --git a/packages/angular_devkit/build_angular/src/dev-server/allowed-hosts_spec.ts b/packages/angular_devkit/build_angular/src/dev-server/allowed-hosts_spec.ts
index c36d18182906..27b659128094 100644
--- a/packages/angular_devkit/build_angular/src/dev-server/allowed-hosts_spec.ts
+++ b/packages/angular_devkit/build_angular/src/dev-server/allowed-hosts_spec.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
diff --git a/packages/angular_devkit/build_angular/src/dev-server/budgets_spec.ts b/packages/angular_devkit/build_angular/src/dev-server/budgets_spec.ts
index 735db97a68d6..f40df3e3ab26 100644
--- a/packages/angular_devkit/build_angular/src/dev-server/budgets_spec.ts
+++ b/packages/angular_devkit/build_angular/src/dev-server/budgets_spec.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
diff --git a/packages/angular_devkit/build_angular/src/dev-server/common-js-warning_spec.ts b/packages/angular_devkit/build_angular/src/dev-server/common-js-warning_spec.ts
new file mode 100644
index 000000000000..56cf7e7d8471
--- /dev/null
+++ b/packages/angular_devkit/build_angular/src/dev-server/common-js-warning_spec.ts
@@ -0,0 +1,46 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+import { Architect } from '@angular-devkit/architect';
+import { logging } from '@angular-devkit/core';
+import { createArchitect, host } from '../test-utils';
+
+describe('Dev Server Builder commonjs warning', () => {
+ const targetSpec = { project: 'app', target: 'serve' };
+
+ let architect: Architect;
+ let logger: logging.Logger;
+ let logs: string[];
+
+ beforeEach(async () => {
+ await host.initialize().toPromise();
+ architect = (await createArchitect(host.root())).architect;
+
+ // Create logger
+ logger = new logging.Logger('');
+ logs = [];
+ logger.subscribe(e => logs.push(e.message));
+ });
+
+ afterEach(async () => host.restore().toPromise());
+
+ it('should not show warning when using HMR', async () => {
+ const run = await architect.scheduleTarget(targetSpec, { hmr: true }, { logger });
+ const output = await run.result;
+ expect(output.success).toBe(true);
+ expect(logs.join()).not.toContain('Warning');
+ await run.stop();
+ });
+
+ it('should not show warning when using live-reload', async () => {
+ const run = await architect.scheduleTarget(targetSpec, { liveReload: true}, { logger });
+ const output = await run.result;
+ expect(output.success).toBe(true);
+ expect(logs.join()).not.toContain('Warning');
+ await run.stop();
+ });
+});
diff --git a/packages/angular_devkit/build_angular/src/dev-server/deploy-url_spec.ts b/packages/angular_devkit/build_angular/src/dev-server/deploy-url_spec.ts
index 125b8cd05ae3..757d0ed1527a 100644
--- a/packages/angular_devkit/build_angular/src/dev-server/deploy-url_spec.ts
+++ b/packages/angular_devkit/build_angular/src/dev-server/deploy-url_spec.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
diff --git a/packages/angular_devkit/build_angular/src/dev-server/hmr_spec.ts b/packages/angular_devkit/build_angular/src/dev-server/hmr_spec.ts
index a4215a6b180c..a617ccd5a45b 100644
--- a/packages/angular_devkit/build_angular/src/dev-server/hmr_spec.ts
+++ b/packages/angular_devkit/build_angular/src/dev-server/hmr_spec.ts
@@ -1,15 +1,15 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import { Architect, BuilderRun } from '@angular-devkit/architect';
// tslint:disable: no-implicit-dependencies
-import puppeteer from 'puppeteer/lib/cjs/puppeteer';
import { Browser } from 'puppeteer/lib/cjs/puppeteer/common/Browser';
import { Page } from 'puppeteer/lib/cjs/puppeteer/common/Page';
+import puppeteer from 'puppeteer/lib/cjs/puppeteer/node';
// tslint:enable: no-implicit-dependencies
import { debounceTime, switchMap, take } from 'rxjs/operators';
import { createArchitect, host } from '../test-utils';
@@ -56,8 +56,8 @@ describe('Dev Server Builder HMR', () => {
'src/app/app.component.html': `
{{title}}
-
-
+
+
one
two
@@ -165,7 +165,7 @@ describe('Dev Server Builder HMR', () => {
await page.goto(url);
expect(logs).toContain('[HMR] Waiting for update signal from WDS...');
await page.evaluate(() => {
- document.querySelector('input').value = 'input value';
+ document.querySelector('input.visible').value = 'input value';
document.querySelector('select').value = 'two';
});
@@ -177,7 +177,7 @@ describe('Dev Server Builder HMR', () => {
expect(logs).toContain('[NG HMR] Restoring input/textarea values.');
expect(logs).toContain('[NG HMR] Restoring selected options.');
- const inputValue = await page.evaluate(() => document.querySelector('input').value);
+ const inputValue = await page.evaluate(() => document.querySelector('input.visible').value);
expect(inputValue).toBe('input value');
const selectValue = await page.evaluate(() => document.querySelector('select').value);
diff --git a/packages/angular_devkit/build_angular/src/dev-server/index.ts b/packages/angular_devkit/build_angular/src/dev-server/index.ts
index 1c13473af3d6..04ca568f61aa 100644
--- a/packages/angular_devkit/build_angular/src/dev-server/index.ts
+++ b/packages/angular_devkit/build_angular/src/dev-server/index.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
@@ -12,31 +12,36 @@ import {
WebpackLoggingCallback,
runWebpackDevServer,
} from '@angular-devkit/build-webpack';
-import { json, logging, tags } from '@angular-devkit/core';
-import { NodeJsSyncHost } from '@angular-devkit/core/node';
-import { existsSync, readFileSync } from 'fs';
+import { json, tags } from '@angular-devkit/core';
import * as path from 'path';
import { Observable, from } from 'rxjs';
-import { map, switchMap } from 'rxjs/operators';
+import { concatMap, switchMap } from 'rxjs/operators';
import * as ts from 'typescript';
import * as url from 'url';
import * as webpack from 'webpack';
-import * as WebpackDevServer from 'webpack-dev-server';
-import { buildBrowserWebpackConfigFromContext } from '../browser';
-import { Schema as BrowserBuilderSchema } from '../browser/schema';
+import * as webpackDevServer from 'webpack-dev-server';
+import { getAnalyticsConfig, getCompilerConfig } from '../browser';
+import { OutputHashing, Schema as BrowserBuilderSchema } from '../browser/schema';
import { ExecutionTransformer } from '../transforms';
import { BuildBrowserFeatures, normalizeOptimization } from '../utils';
import { findCachePath } from '../utils/cache-path';
import { checkPort } from '../utils/check-port';
+import { colors } from '../utils/color';
import { I18nOptions } from '../utils/i18n-options';
-import { getHtmlTransforms } from '../utils/index-file/transforms';
-import { IndexHtmlTransform } from '../utils/index-file/write-index-html';
+import { IndexHtmlTransform } from '../utils/index-file/index-html-generator';
import { generateEntryPoints } from '../utils/package-chunk-sort';
-import { createI18nPlugins } from '../utils/process-bundle';
import { readTsconfig } from '../utils/read-tsconfig';
import { assertCompatibleAngularVersion } from '../utils/version';
-import { getIndexInputFile, getIndexOutputFile } from '../utils/webpack-browser-config';
+import { generateI18nBrowserWebpackConfigFromContext, getIndexInputFile, getIndexOutputFile } from '../utils/webpack-browser-config';
import { addError, addWarning } from '../utils/webpack-diagnostics';
+import {
+ getBrowserConfig,
+ getCommonConfig,
+ getDevServerConfig,
+ getStatsConfig,
+ getStylesConfig,
+ getWorkerConfig,
+} from '../webpack/configs';
import { IndexHtmlWebpackPlugin } from '../webpack/plugins/index-html-webpack-plugin';
import { createWebpackLoggingCallback } from '../webpack/utils/stats';
import { Schema } from './schema';
@@ -79,22 +84,20 @@ export function serveWebpackBrowser(
} = {},
): Observable {
// Check Angular version.
- assertCompatibleAngularVersion(context.workspaceRoot, context.logger);
+ const { logger, workspaceRoot } = context;
+ assertCompatibleAngularVersion(workspaceRoot, logger);
const browserTarget = targetFromTargetString(options.browserTarget);
- const root = context.workspaceRoot;
- let first = true;
- const host = new NodeJsSyncHost();
async function setup(): Promise<{
browserOptions: json.JsonObject & BrowserBuilderSchema;
webpackConfig: webpack.Configuration;
- webpackDevServerConfig: WebpackDevServer.Configuration;
projectRoot: string;
locale: string | undefined;
}> {
// Get the browser configuration from the target name.
- const rawBrowserOptions = await context.getTargetOptions(browserTarget);
+ const rawBrowserOptions = (await context.getTargetOptions(browserTarget)) as json.JsonObject & BrowserBuilderSchema;
+ options.port = await checkPort(options.port ?? 4200, options.host || 'localhost');
// Override options we need to override, if defined.
const overrides = (Object.keys(options) as (keyof DevServerBuilderOptions)[])
@@ -107,41 +110,141 @@ export function serveWebpackBrowser(
{},
);
+ // Get dev-server only options.
+ type DevServerOptions = Partial>;
+ const devServerOptions: DevServerOptions = (Object.keys(options) as (keyof Schema)[])
+ .filter(key => !devServerBuildOverriddenKeys.includes(key) && key !== 'browserTarget')
+ .reduce(
+ (previous, key) => ({
+ ...previous,
+ [key]: options[key],
+ }),
+ {},
+ );
+
// In dev server we should not have budgets because of extra libs such as socks-js
overrides.budgets = undefined;
+ if (rawBrowserOptions.outputHashing && rawBrowserOptions.outputHashing !== OutputHashing.None) {
+ // Disable output hashing for dev build as this can cause memory leaks
+ // See: https://github.com/webpack/webpack-dev-server/issues/377#issuecomment-241258405
+ overrides.outputHashing = OutputHashing.None;
+ logger.warn(`Warning: 'outputHashing' option is disabled when using the dev-server.`);
+ }
+
const browserName = await context.getBuilderNameForTarget(browserTarget);
- const browserOptions = await context.validateOptions(
+ const browserOptions = await context.validateOptions(
{ ...rawBrowserOptions, ...overrides },
browserName,
- );
+ ) as json.JsonObject & BrowserBuilderSchema;
- const { config, projectRoot, i18n } = await buildBrowserWebpackConfigFromContext(
+ const { config, projectRoot, i18n } = await generateI18nBrowserWebpackConfigFromContext(
browserOptions,
context,
- host,
- { hmr: options.hmr },
+ wco => [
+ getDevServerConfig(wco),
+ getCommonConfig(wco),
+ getBrowserConfig(wco),
+ getStylesConfig(wco),
+ getStatsConfig(wco),
+ getAnalyticsConfig(wco, context),
+ getCompilerConfig(wco),
+ browserOptions.webWorkerTsConfig ? getWorkerConfig(wco) : {},
+ ],
+ devServerOptions,
);
- let webpackConfig = config;
- const tsConfig = readTsconfig(browserOptions.tsConfig, context.workspaceRoot);
- if (i18n.shouldInline && tsConfig.options.enableIvy !== false) {
- if (i18n.inlineLocales.size > 1) {
- throw new Error(
- 'The development server only supports localizing a single locale per build',
- );
+ if (!config.devServer) {
+ throw new Error(
+ 'Webpack Dev Server configuration was not set.',
+ );
+ }
+
+ if (options.liveReload && !options.hmr) {
+ // This is needed because we cannot use the inline option directly in the config
+ // because of the SuppressExtractedTextChunksWebpackPlugin
+ // Consider not using SuppressExtractedTextChunksWebpackPlugin when liveReload is enable.
+ webpackDevServer.addDevServerEntrypoints(config, {
+ ...config.devServer,
+ inline: true,
+ });
+
+ // Remove live-reload code from all entrypoints but not main.
+ // Otherwise this will break SuppressExtractedTextChunksWebpackPlugin because
+ // 'addDevServerEntrypoints' adds addional entry-points to all entries.
+ if (config.entry && typeof config.entry === 'object' && !Array.isArray(config.entry) && config.entry.main) {
+ for (const [key, value] of Object.entries(config.entry)) {
+ if (key === 'main' || typeof value === 'string') {
+ continue;
+ }
+
+ const webpackClientScriptIndex = value.findIndex(x => x.includes('webpack-dev-server/client/index.js'));
+ if (webpackClientScriptIndex >= 0) {
+ // Remove the webpack-dev-server/client script from array.
+ value.splice(webpackClientScriptIndex, 1);
+ }
+ }
}
+ }
- await setupLocalize(i18n, browserOptions, webpackConfig);
+ if (options.hmr) {
+ logger.warn(tags.stripIndents`NOTICE: Hot Module Replacement (HMR) is enabled for the dev server.
+ See https://webpack.js.org/guides/hot-module-replacement for information on working with HMR for Webpack.`);
}
- options.port = await checkPort(options.port ?? 4200, options.host || 'localhost');
- const webpackDevServerConfig = (webpackConfig.devServer = buildServerConfig(
- root,
- options,
- browserOptions,
- context.logger,
- ));
+ if (
+ options.host
+ && !/^127\.\d+\.\d+\.\d+/g.test(options.host)
+ && options.host !== 'localhost'
+ ) {
+ logger.warn(tags.stripIndent`
+ Warning: This is a simple server for use in testing or debugging Angular applications
+ locally. It hasn't been reviewed for security issues.
+
+ Binding this server to an open connection can result in compromising your application or
+ computer. Using a different host than the one passed to the "--host" flag might result in
+ websocket connection issues. You might need to use "--disableHostCheck" if that's the
+ case.
+ `);
+ }
+
+ if (options.disableHostCheck) {
+ logger.warn(tags.oneLine`
+ Warning: Running a server with --disable-host-check is a security risk.
+ See https://medium.com/webpack/webpack-dev-server-middleware-security-issues-1489d950874a
+ for more information.
+ `);
+ }
+
+ let locale: string | undefined;
+ if (browserOptions.i18nLocale) {
+ // Deprecated VE option
+ locale = browserOptions.i18nLocale;
+ } else if (i18n.shouldInline) {
+ // Dev-server only supports one locale
+ locale = [...i18n.inlineLocales][0];
+ } else if (i18n.hasDefinedSourceLocale) {
+ // use source locale if not localizing
+ locale = i18n.sourceLocale;
+ }
+
+ let webpackConfig = config;
+
+ // If a locale is defined, setup localization
+ if (locale) {
+ // Only supported with Ivy
+ const tsConfig = readTsconfig(browserOptions.tsConfig, workspaceRoot);
+ if (tsConfig.options.enableIvy !== false) {
+ if (i18n.inlineLocales.size > 1) {
+ throw new Error(
+ 'The development server only supports localizing a single locale per build.',
+ );
+ }
+
+ await setupLocalize(locale, i18n, browserOptions, webpackConfig);
+ }
+ }
if (transforms.webpackConfiguration) {
webpackConfig = await transforms.webpackConfiguration(webpackConfig);
@@ -150,53 +253,18 @@ export function serveWebpackBrowser(
return {
browserOptions,
webpackConfig,
- webpackDevServerConfig,
projectRoot,
- locale:
- browserOptions.i18nLocale || (i18n.shouldInline ? [...i18n.inlineLocales][0] : undefined),
+ locale,
};
}
return from(setup()).pipe(
- switchMap(({ browserOptions, webpackConfig, webpackDevServerConfig, projectRoot, locale }) => {
- // Resolve public host and client address.
- let clientAddress = url.parse(`${options.ssl ? 'https' : 'http'}://0.0.0.0:0`);
- if (options.publicHost) {
- let publicHost = options.publicHost;
- if (!/^\w+:\/\//.test(publicHost)) {
- publicHost = `${options.ssl ? 'https' : 'http'}://${publicHost}`;
- }
- clientAddress = url.parse(publicHost);
- options.publicHost = clientAddress.host;
- }
-
- // Add live reload config.
- if (options.liveReload) {
- _addLiveReload(root, options, browserOptions, webpackConfig, clientAddress, context.logger);
- } else if (options.hmr) {
- context.logger.warn('Live reload is disabled. HMR option ignored.');
- }
-
- webpackConfig.plugins = [...(webpackConfig.plugins || [])];
-
- if (!options.watch) {
- // There's no option to turn off file watching in webpack-dev-server, but
- // we can override the file watcher instead.
- webpackConfig.plugins.push({
- // tslint:disable-next-line:no-any
- apply: (compiler: any) => {
- compiler.hooks.afterEnvironment.tap('angular-cli', () => {
- compiler.watchFileSystem = { watch: () => {} };
- });
- },
- });
- }
-
+ switchMap(({ browserOptions, webpackConfig, projectRoot, locale }) => {
const normalizedOptimization = normalizeOptimization(browserOptions.optimization);
if (browserOptions.index) {
const { scripts = [], styles = [], baseHref, tsConfig } = browserOptions;
- const { options: compilerOptions } = readTsconfig(tsConfig, context.workspaceRoot);
+ const { options: compilerOptions } = readTsconfig(tsConfig, workspaceRoot);
const target = compilerOptions.target || ts.ScriptTarget.ES5;
const buildBrowserFeatures = new BuildBrowserFeatures(projectRoot);
@@ -205,29 +273,28 @@ export function serveWebpackBrowser(
? generateEntryPoints({ scripts: [], styles })
: [];
+ webpackConfig.plugins = [...(webpackConfig.plugins || [])];
webpackConfig.plugins.push(
new IndexHtmlWebpackPlugin({
- input: path.resolve(root, getIndexInputFile(browserOptions)),
- output: getIndexOutputFile(browserOptions),
+ indexPath: path.resolve(workspaceRoot, getIndexInputFile(browserOptions.index)),
+ outputPath: getIndexOutputFile(browserOptions.index),
baseHref,
- moduleEntrypoints,
entrypoints,
+ moduleEntrypoints,
+ noModuleEntrypoints: ['polyfills-es5'],
deployUrl: browserOptions.deployUrl,
sri: browserOptions.subresourceIntegrity,
- noModuleEntrypoints: ['polyfills-es5'],
- postTransforms: getHtmlTransforms(
- normalizedOptimization,
- buildBrowserFeatures,
- transforms.indexHtml,
- ),
+ postTransform: transforms.indexHtml,
+ optimization: normalizedOptimization,
+ WOFFSupportNeeded: !buildBrowserFeatures.isFeatureSupported('woff2'),
crossOrigin: browserOptions.crossOrigin,
lang: locale,
}),
);
}
- if (normalizedOptimization.scripts || normalizedOptimization.styles) {
- context.logger.error(tags.stripIndents`
+ if (normalizedOptimization.scripts || normalizedOptimization.styles.minify) {
+ logger.error(tags.stripIndents`
****************************************************************************************
This is a simple server for use in testing or debugging Angular applications locally.
It hasn't been reviewed for security issues.
@@ -241,37 +308,36 @@ export function serveWebpackBrowser(
webpackConfig,
context,
{
- logging: transforms.logging || createWebpackLoggingCallback(!!options.verbose, context.logger),
+ logging: transforms.logging || createWebpackLoggingCallback(!!options.verbose, logger),
webpackFactory: require('webpack') as typeof webpack,
- webpackDevServerFactory: require('webpack-dev-server') as typeof WebpackDevServer,
+ webpackDevServerFactory: require('webpack-dev-server') as typeof webpackDevServer,
},
).pipe(
- map(buildEvent => {
+ concatMap(async (buildEvent, index) => {
// Resolve serve address.
const serverAddress = url.format({
protocol: options.ssl ? 'https' : 'http',
hostname: options.host === '0.0.0.0' ? 'localhost' : options.host,
- pathname: webpackDevServerConfig.publicPath,
+ pathname: webpackConfig.devServer?.publicPath,
port: buildEvent.port,
});
- if (first) {
- first = false;
- context.logger.info(tags.oneLine`
+ if (index === 0) {
+ logger.info('\n' + tags.oneLine`
**
Angular Live Development Server is listening on ${options.host}:${buildEvent.port},
open your browser on ${serverAddress}
**
- `);
+ ` + '\n');
if (options.open) {
- const open = require('open');
- open(serverAddress);
+ const open = await import('open');
+ await open(serverAddress);
}
}
if (buildEvent.success) {
- context.logger.info(': Compiled successfully.');
+ logger.info(`\n${colors.greenBright(colors.symbols.check)} Compiled successfully.`);
}
return { ...buildEvent, baseUrl: serverAddress } as DevServerBuilderOutput;
@@ -282,17 +348,12 @@ export function serveWebpackBrowser(
}
async function setupLocalize(
+ locale: string,
i18n: I18nOptions,
browserOptions: BrowserBuilderSchema,
webpackConfig: webpack.Configuration,
) {
- const locale = [...i18n.inlineLocales][0];
const localeDescription = i18n.locales[locale];
- const { plugins, diagnostics } = await createI18nPlugins(
- locale,
- localeDescription?.translation,
- browserOptions.i18nMissingTranslation || 'ignore',
- );
// Modify main entrypoint to include locale data
if (
@@ -308,24 +369,33 @@ async function setupLocalize(
}
}
+ let missingTranslationBehavior = browserOptions.i18nMissingTranslation || 'ignore';
+ let translation = localeDescription?.translation || {};
+
+ if (locale === i18n.sourceLocale) {
+ missingTranslationBehavior = 'ignore';
+ translation = {};
+ }
+
+ const i18nLoaderOptions = {
+ locale,
+ missingTranslationBehavior,
+ translation: i18n.shouldInline ? translation : undefined,
+ };
+
const i18nRule: webpack.RuleSetRule = {
- test: /\.(?:m?js|ts)$/,
+ test: /\.(?:[cm]?js|ts)$/,
enforce: 'post',
use: [
{
- loader: require.resolve('babel-loader'),
+ loader: require.resolve('../babel/webpack-loader'),
options: {
- babelrc: false,
- configFile: false,
- compact: false,
- cacheCompression: false,
- cacheDirectory: findCachePath('babel-loader'),
+ cacheDirectory: findCachePath('babel-dev-server-i18n'),
cacheIdentifier: JSON.stringify({
- buildAngular: require('../../package.json').version,
locale,
translationIntegrity: localeDescription?.files.map((file) => file.integrity),
}),
- plugins,
+ i18n: i18nLoaderOptions,
},
},
],
@@ -340,340 +410,6 @@ async function setupLocalize(
}
rules.push(i18nRule);
-
- // Add a plugin to inject the i18n diagnostics
- // tslint:disable-next-line: no-non-null-assertion
- webpackConfig.plugins!.push({
- apply: (compiler: webpack.Compiler) => {
- compiler.hooks.thisCompilation.tap('build-angular', compilation => {
- compilation.hooks.finishModules.tap('build-angular', () => {
- if (!diagnostics) {
- return;
- }
- for (const diagnostic of diagnostics.messages) {
- if (diagnostic.type === 'error') {
- addError(compilation, diagnostic.message);
- } else {
- addWarning(compilation, diagnostic.message);
- }
- }
- diagnostics.messages.length = 0;
- });
- });
- },
- });
-}
-
-/**
- * Create a webpack configuration for the dev server.
- * @param workspaceRoot The root of the workspace. This comes from the context.
- * @param serverOptions DevServer options, based on the dev server input schema.
- * @param browserOptions Browser builder options. See the browser builder from this package.
- * @param logger A generic logger to use for showing warnings.
- * @returns A webpack dev-server configuration.
- */
-export function buildServerConfig(
- workspaceRoot: string,
- serverOptions: DevServerBuilderOptions,
- browserOptions: BrowserBuilderSchema,
- logger: logging.LoggerApi,
-): WebpackDevServer.Configuration {
- // Check that the host is either localhost or prints out a message.
- if (
- serverOptions.host
- && !/^127\.\d+\.\d+\.\d+/g.test(serverOptions.host)
- && serverOptions.host !== 'localhost'
- ) {
- logger.warn(tags.stripIndent`
- Warning: This is a simple server for use in testing or debugging Angular applications
- locally. It hasn't been reviewed for security issues.
-
- Binding this server to an open connection can result in compromising your application or
- computer. Using a different host than the one passed to the "--host" flag might result in
- websocket connection issues. You might need to use "--disableHostCheck" if that's the
- case.
- `);
- }
-
- if (serverOptions.disableHostCheck) {
- logger.warn(tags.oneLine`
- Warning: Running a server with --disable-host-check is a security risk.
- See https://medium.com/webpack/webpack-dev-server-middleware-security-issues-1489d950874a
- for more information.
- `);
- }
-
- const servePath = buildServePath(serverOptions, browserOptions, logger);
- const { styles, scripts } = normalizeOptimization(browserOptions.optimization);
-
- const config: WebpackDevServer.Configuration&{logLevel: string} = {
- host: serverOptions.host,
- port: serverOptions.port,
- headers: {
- 'Access-Control-Allow-Origin': '*',
- ...serverOptions.headers,
- },
- historyApiFallback: !!browserOptions.index && {
- index: `${servePath}/${getIndexOutputFile(browserOptions)}`,
- disableDotRule: true,
- htmlAcceptHeaders: ['text/html', 'application/xhtml+xml'],
- rewrites: [
- {
- from: new RegExp(`^(?!${servePath})/.*`),
- to: context => url.format(context.parsedUrl),
- },
- ],
- },
- stats: false,
- compress: styles || scripts,
- watchOptions: {
- // Using just `--poll` will result in a value of 0 which is very likely not the intention
- // A value of 0 is falsy and will disable polling rather then enable
- // 500 ms is a sensible default in this case
- poll: serverOptions.poll === 0 ? 500 : serverOptions.poll,
- ignored: serverOptions.poll === undefined ? undefined : /[\\\/]node_modules[\\\/]/,
- },
- https: serverOptions.ssl,
- overlay: {
- errors: !(styles || scripts),
- warnings: false,
- },
- // inline is always false, because we add live reloading scripts in _addLiveReload when needed
- inline: false,
- public: serverOptions.publicHost,
- allowedHosts: serverOptions.allowedHosts,
- disableHostCheck: serverOptions.disableHostCheck,
- publicPath: servePath,
- hot: serverOptions.hmr,
- contentBase: false,
- logLevel: 'silent',
- };
-
- if (serverOptions.ssl) {
- _addSslConfig(workspaceRoot, serverOptions, config);
- }
-
- if (serverOptions.proxyConfig) {
- _addProxyConfig(workspaceRoot, serverOptions, config);
- }
-
- return config;
-}
-
-/**
- * Resolve and build a URL _path_ that will be the root of the server. This resolved base href and
- * deploy URL from the browser options and returns a path from the root.
- * @param serverOptions The server options that were passed to the server builder.
- * @param browserOptions The browser options that were passed to the browser builder.
- * @param logger A generic logger to use for showing warnings.
- */
-export function buildServePath(
- serverOptions: DevServerBuilderOptions,
- browserOptions: BrowserBuilderSchema,
- logger: logging.LoggerApi,
-): string {
- let servePath = serverOptions.servePath;
- if (!servePath && servePath !== '') {
- const defaultPath = _findDefaultServePath(browserOptions.baseHref, browserOptions.deployUrl);
- if (defaultPath == null) {
- logger.warn(tags.oneLine`
- Warning: --deploy-url and/or --base-href contain unsupported values for ng serve. Default
- serve path of '/' used. Use --serve-path to override.
- `);
- }
- servePath = defaultPath || '';
- }
- if (servePath.endsWith('/')) {
- servePath = servePath.substr(0, servePath.length - 1);
- }
- if (!servePath.startsWith('/')) {
- servePath = `/${servePath}`;
- }
-
- return servePath;
-}
-
-/**
- * Private method to enhance a webpack config with live reload configuration.
- * @private
- */
-function _addLiveReload(
- root: string,
- options: DevServerBuilderOptions,
- browserOptions: BrowserBuilderSchema,
- webpackConfig: webpack.Configuration,
- clientAddress: url.UrlWithStringQuery,
- logger: logging.LoggerApi,
-) {
- if (webpackConfig.plugins === undefined) {
- webpackConfig.plugins = [];
- }
-
- // Workaround node shim hoisting issues with live reload client
- // Only needed in dev server mode to support live reload capabilities in all package managers
- // Not needed in Webpack 5 - node-libs-browser will not be present in webpack 5
- let nodeLibsBrowserPath;
- try {
- const webpackPath = path.dirname(require.resolve('webpack/package.json'));
- nodeLibsBrowserPath = require.resolve('node-libs-browser', { paths: [webpackPath] });
- } catch {}
- if (nodeLibsBrowserPath) {
- const nodeLibsBrowser = require(nodeLibsBrowserPath);
- webpackConfig.plugins.push(
- new webpack.NormalModuleReplacementPlugin(
- /^events|url|querystring$/,
- (resource: { issuer?: string; request: string }) => {
- if (!resource.issuer) {
- return;
- }
- if (/[\/\\]hot[\/\\]emitter\.js$/.test(resource.issuer)) {
- if (resource.request === 'events') {
- resource.request = nodeLibsBrowser.events;
- }
- } else if (
- /[\/\\]webpack-dev-server[\/\\]client[\/\\]utils[\/\\]createSocketUrl\.js$/.test(
- resource.issuer,
- )
- ) {
- switch (resource.request) {
- case 'url':
- resource.request = nodeLibsBrowser.url;
- break;
- case 'querystring':
- resource.request = nodeLibsBrowser.querystring;
- break;
- }
- }
- },
- ),
- );
- }
-
- // This allows for live reload of page when changes are made to repo.
- // https://webpack.js.org/configuration/dev-server/#devserver-inline
- let webpackDevServerPath;
- try {
- webpackDevServerPath = require.resolve('webpack-dev-server/client');
- } catch {
- throw new Error('The "webpack-dev-server" package could not be found.');
- }
-
- // If a custom path is provided the webpack dev server client drops the sockjs-node segment.
- // This adds it back so that behavior is consistent when using a custom URL path
- let sockjsPath = '';
- if (clientAddress.pathname) {
- clientAddress.pathname = path.posix.join(clientAddress.pathname, 'sockjs-node');
- sockjsPath = '&sockPath=' + clientAddress.pathname;
- }
-
- const entryPoints = [`${webpackDevServerPath}?${url.format(clientAddress)}${sockjsPath}`];
- if (options.hmr) {
- logger.warn(tags.stripIndents`NOTICE: Hot Module Replacement (HMR) is enabled for the dev server.
- See https://webpack.js.org/guides/hot-module-replacement for information on working with HMR for Webpack.`);
-
- entryPoints.push(
- 'webpack/hot/dev-server',
- );
- }
-
- if (typeof webpackConfig.entry !== 'object' || Array.isArray(webpackConfig.entry)) {
- webpackConfig.entry = {};
- }
- if (!Array.isArray(webpackConfig.entry.main)) {
- webpackConfig.entry.main = [];
- }
- webpackConfig.entry.main.unshift(...entryPoints);
-}
-
-/**
- * Private method to enhance a webpack config with SSL configuration.
- * @private
- */
-function _addSslConfig(
- root: string,
- options: DevServerBuilderOptions,
- config: WebpackDevServer.Configuration,
-) {
- let sslKey: string | undefined = undefined;
- let sslCert: string | undefined = undefined;
- if (options.sslKey) {
- const keyPath = path.resolve(root, options.sslKey);
- if (existsSync(keyPath)) {
- sslKey = readFileSync(keyPath, 'utf-8');
- }
- }
- if (options.sslCert) {
- const certPath = path.resolve(root, options.sslCert);
- if (existsSync(certPath)) {
- sslCert = readFileSync(certPath, 'utf-8');
- }
- }
-
- config.https = true;
- if (sslKey != null && sslCert != null) {
- config.https = {
- key: sslKey,
- cert: sslCert,
- };
- }
-}
-
-/**
- * Private method to enhance a webpack config with Proxy configuration.
- * @private
- */
-function _addProxyConfig(
- root: string,
- options: DevServerBuilderOptions,
- config: WebpackDevServer.Configuration,
-) {
- let proxyConfig = {};
- const proxyPath = path.resolve(root, options.proxyConfig as string);
- if (existsSync(proxyPath)) {
- proxyConfig = require(proxyPath);
- } else {
- const message = 'Proxy config file ' + proxyPath + ' does not exist.';
- throw new Error(message);
- }
- config.proxy = proxyConfig;
-}
-
-/**
- * Find the default server path. We don't want to expose baseHref and deployUrl as arguments, only
- * the browser options where needed. This method should stay private (people who want to resolve
- * baseHref and deployUrl should use the buildServePath exported function.
- * @private
- */
-function _findDefaultServePath(baseHref?: string, deployUrl?: string): string | null {
- if (!baseHref && !deployUrl) {
- return '';
- }
-
- if (/^(\w+:)?\/\//.test(baseHref || '') || /^(\w+:)?\/\//.test(deployUrl || '')) {
- // If baseHref or deployUrl is absolute, unsupported by ng serve
- return null;
- }
-
- // normalize baseHref
- // for ng serve the starting base is always `/` so a relative
- // and root relative value are identical
- const baseHrefParts = (baseHref || '').split('/').filter(part => part !== '');
- if (baseHref && !baseHref.endsWith('/')) {
- baseHrefParts.pop();
- }
- const normalizedBaseHref = baseHrefParts.length === 0 ? '/' : `/${baseHrefParts.join('/')}/`;
-
- if (deployUrl && deployUrl[0] === '/') {
- if (baseHref && baseHref[0] === '/' && normalizedBaseHref !== deployUrl) {
- // If baseHref and deployUrl are root relative and not equivalent, unsupported by ng serve
- return null;
- }
-
- return deployUrl;
- }
-
- // Join together baseHref and deployUrl
- return `${normalizedBaseHref}${deployUrl || ''}`;
}
export default createBuilder(serveWebpackBrowser);
diff --git a/packages/angular_devkit/build_angular/src/dev-server/index_spec.ts b/packages/angular_devkit/build_angular/src/dev-server/index_spec.ts
index ecaa8d98da31..32de590781fb 100644
--- a/packages/angular_devkit/build_angular/src/dev-server/index_spec.ts
+++ b/packages/angular_devkit/build_angular/src/dev-server/index_spec.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
diff --git a/packages/angular_devkit/build_angular/src/dev-server/inline-critical-css-optimization_spec.ts b/packages/angular_devkit/build_angular/src/dev-server/inline-critical-css-optimization_spec.ts
new file mode 100644
index 000000000000..d16ef5084b5d
--- /dev/null
+++ b/packages/angular_devkit/build_angular/src/dev-server/inline-critical-css-optimization_spec.ts
@@ -0,0 +1,51 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+import { Architect, BuilderRun } from '@angular-devkit/architect';
+import fetch from 'node-fetch'; // tslint:disable-line:no-implicit-dependencies
+import { mergeMap, take, timeout } from 'rxjs/operators';
+import { createArchitect, host } from '../test-utils';
+
+describe('Dev Server Builder inline critical CSS optimization', () => {
+ const target = { project: 'app', target: 'serve' };
+ let architect: Architect;
+ let runs: BuilderRun[] = [];
+
+ beforeEach(async () => {
+ await host.initialize().toPromise();
+ architect = (await createArchitect(host.root())).architect;
+ runs = [];
+
+ host.writeMultipleFiles({
+ 'src/styles.css': `
+ body { color: #000 }
+ `,
+ });
+ });
+
+ afterEach(async () => {
+ await host.restore().toPromise();
+ await Promise.all(runs.map(r => r.stop()));
+ });
+
+ it('works', async () => {
+ const run = await architect.scheduleTarget(target, { browserTarget: 'app:build:production,inline-critical-css', port: 0 });
+ runs.push(run);
+
+ await run.output.pipe(
+ take(1),
+ timeout(39000),
+ mergeMap(async output => {
+ expect(output.success).toBe(true);
+ const response = await fetch(`${output.baseUrl}/index.html`);
+ expect(await response.text()).toContain(`body{color:#000}`);
+ }),
+ ).toPromise();
+
+ }, 40000);
+});
diff --git a/packages/angular_devkit/build_angular/src/dev-server/live-reload_spec.ts b/packages/angular_devkit/build_angular/src/dev-server/live-reload_spec.ts
new file mode 100644
index 000000000000..570d82f23fbc
--- /dev/null
+++ b/packages/angular_devkit/build_angular/src/dev-server/live-reload_spec.ts
@@ -0,0 +1,283 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+// tslint:disable: no-implicit-dependencies
+import { Architect, BuilderRun } from '@angular-devkit/architect';
+import { tags } from '@angular-devkit/core';
+import { createProxyServer } from 'http-proxy';
+import { HTTPResponse } from 'puppeteer/lib/cjs/puppeteer/api-docs-entry';
+import { Browser } from 'puppeteer/lib/cjs/puppeteer/common/Browser';
+import { Page } from 'puppeteer/lib/cjs/puppeteer/common/Page';
+import puppeteer from 'puppeteer/lib/cjs/puppeteer/node';
+import { debounceTime, switchMap, take } from 'rxjs/operators';
+import { createArchitect, host } from '../test-utils';
+
+// tslint:disable-next-line: no-any
+declare const document: any;
+
+interface ProxyInstance {
+ server: typeof createProxyServer extends () => infer R ? R : never;
+ url: string;
+}
+
+let proxyPort = 9100;
+function createProxy(target: string, secure: boolean, ws = true): ProxyInstance {
+ proxyPort++;
+
+ const server = createProxyServer({
+ ws,
+ target,
+ secure,
+ ssl: secure && {
+ key: tags.stripIndents`
+ -----BEGIN RSA PRIVATE KEY-----
+ MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDEBRUsUz4rdcMt
+ CQGLvG3SzUinsmgdgOyTNQNA0eOMyRSrmS8L+F/kSLUnqqu4mzdeqDzo2Xj553jK
+ dRqMCRFGJuGnQ/VIbW2A+ywgrqILuDyF5i4PL1aQW4yJ7TnXfONKfpswQArlN6DF
+ gBYJtoJlf8XD1sOeJpsv/O46/ix/wngQ+GwQQ2cfqxQT0fE9SBCY23VNt3SPUJ3k
+ 9etJMvJ9U9GHSb1CFdNQe7Gyx7xdKf1TazB27ElNZEg2aF99if47uRskYjvvFivy
+ 7nxGx/ccIwjwNMpk29AsKG++0sn1yTK7tD5Px6aCSVK0BKbdXZS2euJor8hASGBJ
+ 3GpVGJvdAgMBAAECggEAapYo8TVCdPdP7ckb4hPP0/R0MVu9aW2VNmZ5ImH+zar5
+ ZmWhQ20HF2bBupP/VB5yeTIaDLNUKO9Iqy4KBWNY1UCHKyC023FFPgFV+V98FctU
+ faqwGOmwtEZToRwxe48ZOISndhEc247oCPyg/x8SwIY9z0OUkwaDFBEAqWtUXxM3
+ /SPpCT5ilLgxnRgVB8Fj5Z0q7ThnxNVOmVC1OSIakEj46PzmMXn1pCKLOCUmAAOQ
+ BnrOZuty2b8b2M/GHsktLZwojQQJmArnIBymTXQTVhaGgKSyOv1qvHLp9L1OJf0/
+ Xm+/TqT6ztzhzlftcObdfQZZ5JuoEwlvyrsGFlA3MQKBgQDiQC3KYMG8ViJkWrv6
+ XNAFEoAjVEKrtirGWJ66YfQ9KSJ7Zttrd1Y1V1OLtq3z4YMH39wdQ8rOD+yR8mWV
+ 6Tnsxma6yJXAH8uan8iVbxjIZKF1hnvNCxUoxYmWOmTLcEQMzmxvTzAiR+s6R6Uj
+ 9LgGqppt30nM4wnOhOJU6UxqbwKBgQDdy03KidbPZuycJSy1C9AIt0jlrxDsYm+U
+ fZrB6mHEZcgoZS5GbLKinQCdGcgERa05BXvJmNbfZtT5a37YEnbjsTImIhDiBP5P
+ nW36/9a3Vg1svd1KP2206/Bh3gfZbgTsQg4YogXgjf0Uzuvw18btgTtLVpVyeuqz
+ TU3eeF30cwKBgQCN6lvOmapsDEs+T3uhqx4AUH53qp63PmjOSUAnANJGmsq6ROZV
+ HmHAy6nn9Qpf85BRHCXhZWiMoIhvc3As/EINNtWxS6hC/q6jqp4SvcD50cVFBroY
+ /16iWGXZCX+37A+DSOfTWgSDPEFcKRx41UOpStHbITgVgEPieo/NWxlHmQKBgQDX
+ JOLs2RB6V0ilnpnjdPXzvncD9fHgmwvJap24BPeZX3HtXViqD76oZsu1mNCg9EW3
+ zk3pnEyyoDlvSIreZerVq4kN3HWsCVP3Pqr0kz9g0CRtmy8RWr28hjHDfXD3xPUZ
+ iGnMEz7IOHOKv722/liFAprV1cNaLUmFbDNg3jmlaQKBgQDG5WwngPhOHmjTnSml
+ amfEz9a4yEhQqpqgVNW5wwoXOf6DbjL2m/maJh01giThj7inMcbpkZlIclxD0Eu6
+ Lof+ctCeqSAJvaVPmd+nv8Yp26zsF1yM8ax9xXjrIvv9fSbycNveGTDCsNNTiYoW
+ QyvMqmN1kGy20SZbQDD/fLfqBQ==
+ -----END RSA PRIVATE KEY-----
+ `,
+ cert: tags.stripIndents`
+ -----BEGIN CERTIFICATE-----
+ MIIDXTCCAkWgAwIBAgIJALz8gD/gAt0OMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV
+ BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX
+ aWRnaXRzIFB0eSBMdGQwHhcNMTgxMDIzMTgyMTQ5WhcNMTkxMDIzMTgyMTQ5WjBF
+ MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50
+ ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
+ CgKCAQEAxAUVLFM+K3XDLQkBi7xt0s1Ip7JoHYDskzUDQNHjjMkUq5kvC/hf5Ei1
+ J6qruJs3Xqg86Nl4+ed4ynUajAkRRibhp0P1SG1tgPssIK6iC7g8heYuDy9WkFuM
+ ie0513zjSn6bMEAK5TegxYAWCbaCZX/Fw9bDniabL/zuOv4sf8J4EPhsEENnH6sU
+ E9HxPUgQmNt1Tbd0j1Cd5PXrSTLyfVPRh0m9QhXTUHuxsse8XSn9U2swduxJTWRI
+ NmhffYn+O7kbJGI77xYr8u58Rsf3HCMI8DTKZNvQLChvvtLJ9ckyu7Q+T8emgklS
+ tASm3V2UtnriaK/IQEhgSdxqVRib3QIDAQABo1AwTjAdBgNVHQ4EFgQUDZBhVKdb
+ 3BRhLIhuuE522Vsul0IwHwYDVR0jBBgwFoAUDZBhVKdb3BRhLIhuuE522Vsul0Iw
+ DAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEABh9WWZwWLgb9/DcTxL72
+ 6pI96t4jiF79Q+pPefkaIIi0mE6yodWrTAsBQu9I6bNRaEcCSoiXkP2bqskD/UGg
+ LwUFgSrDOAA3UjdHw3QU5g2NocduG7mcFwA40TB98sOsxsUyYlzSyWzoiQWwPYwb
+ hek1djuWkqPXsTjlj54PTPN/SjTFmo4p5Ip6nbRf2nOREl7v0rJpGbJvXiCMYyd+
+ Zv+j4mRjCGo8ysMR2HjCUGkYReLAgKyyz3M7i8vevJhKslyOmy6Txn4F0nPVumaU
+ DDIy4xXPW1STWfsmSYJfYW3wa0wk+pJQ3j2cTzkPQQ8gwpvM3U9DJl43uwb37v6I
+ 7Q==
+ -----END CERTIFICATE-----
+ `,
+ },
+ })
+ .listen(proxyPort);
+
+ return {
+ server,
+ url: `${secure ? 'https' : 'http'}://localhost:${proxyPort}`,
+ };
+}
+
+async function goToPageAndWaitForSockJs(page: Page, url: string): Promise {
+ const socksRequest = `${url.endsWith('/') ? url : url + '/'}sockjs-node/info?t=`;
+
+ await Promise.all([
+ page.waitForResponse((r: HTTPResponse) => r.url().startsWith(socksRequest) && r.status() === 200),
+ page.goto(url),
+ ]);
+}
+
+describe('Dev Server Builder live-reload', () => {
+ const target = { project: 'app', target: 'serve' };
+ // Avoid using port `0` as these tests will behave differrently and tests will pass when they shouldn't.
+ // Port 0 and host 0.0.0.0 have special meaning in dev-server.
+ const overrides = { hmr: false, watch: true, port: 4202, liveReload: true };
+ let architect: Architect;
+ let browser: Browser;
+ let page: Page;
+ let runs: BuilderRun[];
+ let proxy: ProxyInstance | undefined;
+
+ beforeAll(async () => {
+ browser = await puppeteer.launch({
+ // MacOSX users need to set the local binary manually because Chrome has lib files with
+ // spaces in them which Bazel does not support in runfiles
+ // See: https://github.com/angular/angular-cli/pull/17624
+ // tslint:disable-next-line: max-line-length
+ // executablePath: '/Users//git/angular-cli/node_modules/puppeteer/.local-chromium/mac-818858/chrome-mac/Chromium.app/Contents/MacOS/Chromium',
+ ignoreHTTPSErrors: true,
+ args: [
+ '--no-sandbox',
+ '--disable-gpu',
+ ],
+ });
+ });
+
+ afterAll(async () => {
+ await browser.close();
+ });
+
+ beforeEach(async () => {
+ await host.initialize().toPromise();
+ architect = (await createArchitect(host.root())).architect;
+
+ host.writeMultipleFiles({
+ 'src/app/app.component.html': `
+ {{title}}
+ `,
+ });
+
+ runs = [];
+ page = await browser.newPage();
+ });
+
+ afterEach(async () => {
+ proxy?.server.close();
+ proxy = undefined;
+ await host.restore().toPromise();
+ await page.close();
+ await Promise.all(runs.map(r => r.stop()));
+ });
+
+ it('works without proxy', async () => {
+ const run = await architect.scheduleTarget(target, overrides);
+ runs.push(run);
+
+ let buildCount = 0;
+ await run.output
+ .pipe(
+ debounceTime(1000),
+ switchMap(async buildEvent => {
+ expect(buildEvent.success).toBe(true);
+ const url = buildEvent.baseUrl as string;
+ switch (buildCount) {
+ case 0:
+ await goToPageAndWaitForSockJs(page, url);
+ host.replaceInFile('src/app/app.component.ts', `'app'`, `'app-live-reload'`);
+ break;
+ case 1:
+ const innerText = await page.evaluate(() => document.querySelector('p').innerText);
+ expect(innerText).toBe('app-live-reload');
+ break;
+ }
+
+ buildCount++;
+ }),
+ take(2),
+ )
+ .toPromise();
+ }, 30000);
+
+ it('works without http -> http proxy', async () => {
+ const run = await architect.scheduleTarget(target, overrides);
+ runs.push(run);
+
+ let proxy: ProxyInstance | undefined;
+ let buildCount = 0;
+ await run.output
+ .pipe(
+ debounceTime(1000),
+ switchMap(async buildEvent => {
+ expect(buildEvent.success).toBe(true);
+ const url = buildEvent.baseUrl as string;
+ switch (buildCount) {
+ case 0:
+ proxy = createProxy(url, false);
+ await goToPageAndWaitForSockJs(page, proxy.url);
+ host.replaceInFile('src/app/app.component.ts', `'app'`, `'app-live-reload'`);
+ break;
+ case 1:
+ const innerText = await page.evaluate(() => document.querySelector('p').innerText);
+ expect(innerText).toBe('app-live-reload');
+ break;
+ }
+
+ buildCount++;
+ }),
+ take(2),
+ )
+ .toPromise();
+ }, 30000);
+
+ it('works without https -> http proxy', async () => {
+ const run = await architect.scheduleTarget(target, overrides);
+ runs.push(run);
+
+ let proxy: ProxyInstance | undefined;
+ let buildCount = 0;
+ await run.output
+ .pipe(
+ debounceTime(1000),
+ switchMap(async buildEvent => {
+ expect(buildEvent.success).toBe(true);
+ const url = buildEvent.baseUrl as string;
+ switch (buildCount) {
+ case 0:
+ proxy = createProxy(url, true);
+ await goToPageAndWaitForSockJs(page, proxy.url);
+ host.replaceInFile('src/app/app.component.ts', `'app'`, `'app-live-reload'`);
+ break;
+ case 1:
+ const innerText = await page.evaluate(() => document.querySelector('p').innerText);
+ expect(innerText).toBe('app-live-reload');
+ break;
+ }
+
+ buildCount++;
+ }),
+ take(2),
+ )
+ .toPromise();
+ }, 30000);
+
+ it('works without https -> http proxy without websockets (dotnet emulation)', async () => {
+ const run = await architect.scheduleTarget(target, overrides);
+ runs.push(run);
+
+ let proxy: ProxyInstance | undefined;
+ let buildCount = 0;
+
+ await run.output
+ .pipe(
+ debounceTime(1000),
+ switchMap(async buildEvent => {
+ expect(buildEvent.success).toBe(true);
+ const url = buildEvent.baseUrl as string;
+ switch (buildCount) {
+ case 0:
+ proxy = createProxy(url, true, false);
+ await goToPageAndWaitForSockJs(page, proxy.url);
+ await page.waitForResponse((response: HTTPResponse) => response.url().includes('xhr_streaming') && response.status() === 200);
+ host.replaceInFile('src/app/app.component.ts', `'app'`, `'app-live-reload'`);
+ break;
+ case 1:
+ const innerText = await page.evaluate(() => document.querySelector('p').innerText);
+ expect(innerText).toBe('app-live-reload');
+ break;
+ }
+
+ buildCount++;
+ }),
+ take(2),
+ )
+ .toPromise();
+ }, 30000);
+});
diff --git a/packages/angular_devkit/build_angular/src/dev-server/proxy_spec.ts b/packages/angular_devkit/build_angular/src/dev-server/proxy_spec.ts
index 684e639cbe33..e83fb0628431 100644
--- a/packages/angular_devkit/build_angular/src/dev-server/proxy_spec.ts
+++ b/packages/angular_devkit/build_angular/src/dev-server/proxy_spec.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
diff --git a/packages/angular_devkit/build_angular/src/dev-server/public-host_spec.ts b/packages/angular_devkit/build_angular/src/dev-server/public-host_spec.ts
index b0389eb2a80f..9a3be224ac54 100644
--- a/packages/angular_devkit/build_angular/src/dev-server/public-host_spec.ts
+++ b/packages/angular_devkit/build_angular/src/dev-server/public-host_spec.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
diff --git a/packages/angular_devkit/build_angular/src/dev-server/schema.json b/packages/angular_devkit/build_angular/src/dev-server/schema.json
index 2c2c0bf15279..077f762c9576 100644
--- a/packages/angular_devkit/build_angular/src/dev-server/schema.json
+++ b/packages/angular_devkit/build_angular/src/dev-server/schema.json
@@ -6,7 +6,7 @@
"properties": {
"browserTarget": {
"type": "string",
- "description": "Target to serve.",
+ "description": "A browser builder target to serve in the format of `project:target[:configuration]`. You can also pass in more than one configuration name as a comma-separated list. Example: `project:target:production,staging`.",
"pattern": "^[^:\\s]+:[^:\\s]+(:[^\\s]+)?$"
},
"port": {
@@ -105,7 +105,7 @@
"x-deprecated": "No longer has an effect."
},
"optimization": {
- "description": "Enables optimization of the build output.",
+ "description": "Enables optimization of the build output. Including minification of scripts and styles, tree-shaking, dead-code elimination, tree-shaking and fonts inlining. For more information, see https://angular.io/guide/workspace-config#optimization-configuration.",
"x-user-analytics": 16,
"oneOf": [
{
@@ -137,29 +137,29 @@
"x-deprecated": "Use the \"aot\" option in the browser builder instead."
},
"sourceMap": {
- "description": "Output sourcemaps.",
+ "description": "Output source maps for scripts and styles. For more information, see https://angular.io/guide/workspace-config#source-map-configuration.",
"oneOf": [
{
"type": "object",
"properties": {
"scripts": {
"type": "boolean",
- "description": "Output sourcemaps for all scripts.",
+ "description": "Output source maps for all scripts.",
"default": true
},
"styles": {
"type": "boolean",
- "description": "Output sourcemaps for all styles.",
+ "description": "Output source maps for all styles.",
"default": true
},
"hidden": {
"type": "boolean",
- "description": "Output sourcemaps used for error reporting tools.",
+ "description": "Output source maps used for error reporting tools.",
"default": false
},
"vendor": {
"type": "boolean",
- "description": "Resolve vendor packages sourcemaps.",
+ "description": "Resolve vendor packages source maps.",
"default": false
}
},
@@ -173,12 +173,12 @@
},
"vendorChunk": {
"type": "boolean",
- "description": "Use a separate bundle containing only vendor libraries.",
+ "description": "Generate a seperate bundle containing only vendor libraries. This option should only used for development.",
"x-deprecated": "Use the \"vendorChunk\" option in the browser builder instead."
},
"commonChunk": {
"type": "boolean",
- "description": "Use a separate bundle containing code used across multiple bundles.",
+ "description": "Generate a seperate bundle containing code used across multiple bundles.",
"x-deprecated": "Use the \"commonChunk\" option in the browser builder instead."
},
"baseHref": {
diff --git a/packages/angular_devkit/build_angular/src/dev-server/serve-path_spec.ts b/packages/angular_devkit/build_angular/src/dev-server/serve-path_spec.ts
index 6edc51abeb51..acf779ff97fe 100644
--- a/packages/angular_devkit/build_angular/src/dev-server/serve-path_spec.ts
+++ b/packages/angular_devkit/build_angular/src/dev-server/serve-path_spec.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
diff --git a/packages/angular_devkit/build_angular/src/dev-server/ssl_spec.ts b/packages/angular_devkit/build_angular/src/dev-server/ssl_spec.ts
index ca3ec505d318..c14375efb0d8 100644
--- a/packages/angular_devkit/build_angular/src/dev-server/ssl_spec.ts
+++ b/packages/angular_devkit/build_angular/src/dev-server/ssl_spec.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
@@ -102,8 +102,8 @@ describe('Dev Server Builder ssl', () => {
const overrides = {
ssl: true,
- sslKey: '../ssl/server.key',
- sslCert: '../ssl/server.crt',
+ sslKey: 'ssl/server.key',
+ sslCert: 'ssl/server.crt',
};
const run = await architect.scheduleTarget(target, overrides);
diff --git a/packages/angular_devkit/build_angular/src/dev-server/works_spec.ts b/packages/angular_devkit/build_angular/src/dev-server/works_spec.ts
index c8ec85604300..e50e5412d5dd 100644
--- a/packages/angular_devkit/build_angular/src/dev-server/works_spec.ts
+++ b/packages/angular_devkit/build_angular/src/dev-server/works_spec.ts
@@ -1,13 +1,13 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import { Architect, BuilderRun } from '@angular-devkit/architect';
import { DevServerBuilderOutput } from '@angular-devkit/build-angular';
-import { logging } from '@angular-devkit/core';
+import { logging, normalize, virtualFs } from '@angular-devkit/core';
import fetch from 'node-fetch'; // tslint:disable-line:no-implicit-dependencies
import { createArchitect, host } from '../test-utils';
@@ -112,4 +112,30 @@ describe('Dev Server Builder', () => {
expect(response.headers.get('X-Header')).toBe('Hello World');
}, 30000);
+ it('uses source locale when not localizing', async () => {
+ const config = host.scopedSync().read(normalize('angular.json'));
+ const jsonConfig = JSON.parse(virtualFs.fileBufferToString(config));
+ const applicationProject = jsonConfig.projects.app;
+
+ applicationProject.i18n = { sourceLocale: 'fr' };
+
+ host.writeMultipleFiles({
+ 'angular.json': JSON.stringify(jsonConfig),
+ });
+
+ const architect = (await createArchitect(host.root())).architect;
+ const run = await architect.scheduleTarget(target);
+ const output = await run.result;
+ expect(output.success).toBe(true);
+
+ const indexResponse = await fetch('http://localhost:4200/index.html');
+ expect(await indexResponse.text()).toContain('lang="fr"');
+ const vendorResponse = await fetch('http://localhost:4200/vendor.js');
+ const vendorText = await vendorResponse.text();
+ expect(vendorText).toContain('fr');
+ expect(vendorText).toContain('octobre');
+
+ await run.stop();
+ });
+
});
diff --git a/packages/angular_devkit/build_angular/src/extract-i18n/index.ts b/packages/angular_devkit/build_angular/src/extract-i18n/index.ts
index 71a59633ba32..6b3556f50350 100644
--- a/packages/angular_devkit/build_angular/src/extract-i18n/index.ts
+++ b/packages/angular_devkit/build_angular/src/extract-i18n/index.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
@@ -21,7 +21,7 @@ import { ExecutionTransformer } from '../transforms';
import { createI18nOptions } from '../utils/i18n-options';
import { assertCompatibleAngularVersion } from '../utils/version';
import { generateBrowserWebpackConfigFromContext } from '../utils/webpack-browser-config';
-import { getAotConfig, getCommonConfig, getStatsConfig } from '../webpack/configs';
+import { getAotConfig, getBrowserConfig, getCommonConfig, getStatsConfig } from '../webpack/configs';
import { createWebpackLoggingCallback } from '../webpack/utils/stats';
import { Format, Schema } from './schema';
@@ -37,12 +37,16 @@ function getI18nOutfile(format: string | undefined) {
case 'xlf2':
case 'xliff2':
return 'messages.xlf';
+ case 'json':
+ return 'messages.json';
+ case 'arb':
+ return 'messages.arb';
default:
throw new Error(`Unsupported format "${format}"`);
}
}
-async function getSerializer(format: Format, sourceLocale: string, basePath: string, useLegacyIds = true) {
+async function getSerializer(format: Format, sourceLocale: string, basePath: string, useLegacyIds: boolean) {
switch (format) {
case Format.Xmb:
const { XmbTranslationSerializer } =
@@ -65,13 +69,62 @@ async function getSerializer(format: Format, sourceLocale: string, basePath: str
// tslint:disable-next-line: no-any
return new Xliff2TranslationSerializer(sourceLocale, basePath as any, useLegacyIds, {});
+ case Format.Json:
+ const { SimpleJsonTranslationSerializer } =
+ await import('@angular/localize/src/tools/src/extract/translation_files/json_translation_serializer');
+
+ // tslint:disable-next-line: no-any
+ return new SimpleJsonTranslationSerializer(sourceLocale);
+ case Format.Arb:
+ const { ArbTranslationSerializer } =
+ await import('@angular/localize/src/tools/src/extract/translation_files/arb_translation_serializer');
+
+ const fileSystem = {
+ relative(from: string, to: string): string {
+ return path.relative(from, to);
+ },
+ };
+
+ // tslint:disable-next-line: no-any
+ return new ArbTranslationSerializer(sourceLocale, basePath as any, fileSystem as any);
+ }
+}
+
+function normalizeFormatOption(options: ExtractI18nBuilderOptions) {
+ let format;
+ if (options.i18nFormat !== Format.Xlf) {
+ format = options.i18nFormat;
+ } else {
+ format = options.format;
+ }
+
+ switch (format) {
+ case Format.Xlf:
+ case Format.Xlif:
+ case Format.Xliff:
+ format = Format.Xlf;
+ break;
+ case Format.Xlf2:
+ case Format.Xliff2:
+ format = Format.Xlf2;
+ break;
+ case Format.Json:
+ format = Format.Json;
+ break;
+ case Format.Arb:
+ format = Format.Arb;
+ break;
+ case undefined:
+ format = Format.Xlf;
+ break;
}
+
+ return format;
}
-class InMemoryOutputPlugin {
+class NoEmitPlugin {
apply(compiler: webpack.Compiler): void {
- // tslint:disable-next-line:no-any
- compiler.outputFileSystem = new (webpack as any).MemoryOutputFileSystem();
+ compiler.hooks.shouldEmit.tap('angular-no-emit', () => false);
}
}
@@ -91,27 +144,10 @@ export async function execute(
await context.getBuilderNameForTarget(browserTarget),
);
- if (options.i18nFormat !== Format.Xlf) {
- options.format = options.i18nFormat;
- }
-
- switch (options.format) {
- case Format.Xlf:
- case Format.Xlif:
- case Format.Xliff:
- options.format = Format.Xlf;
- break;
- case Format.Xlf2:
- case Format.Xliff2:
- options.format = Format.Xlf2;
- break;
- case undefined:
- options.format = Format.Xlf;
- break;
- }
+ const format = normalizeFormatOption(options);
// We need to determine the outFile name so that AngularCompiler can retrieve it.
- let outFile = options.outFile || getI18nOutfile(options.format);
+ let outFile = options.outFile || getI18nOutfile(format);
if (options.outputPath) {
// AngularCompilerPlugin doesn't support genDir so we have to adjust outFile instead.
outFile = path.join(options.outputPath, outFile);
@@ -126,15 +162,13 @@ export async function execute(
const i18n = createI18nOptions(metadata);
let usingIvy = false;
+ let useLegacyIds = true;
const ivyMessages: LocalizeMessage[] = [];
const { config, projectRoot } = await generateBrowserWebpackConfigFromContext(
{
...browserOptions,
- optimization: {
- scripts: false,
- styles: false,
- },
+ optimization: false,
sourceMap: {
scripts: true,
styles: false,
@@ -142,7 +176,7 @@ export async function execute(
},
buildOptimizer: false,
i18nLocale: options.i18nLocale || i18n.sourceLocale,
- i18nFormat: options.format,
+ i18nFormat: format,
i18nFile: outFile,
aot: true,
progress: options.progress,
@@ -150,11 +184,16 @@ export async function execute(
scripts: [],
styles: [],
deleteOutputPath: false,
+ extractLicenses: false,
+ subresourceIntegrity: false,
},
context,
(wco) => {
const isIvyApplication = wco.tsConfig.options.enableIvy !== false;
+ // Default value for legacy message ids is currently true
+ useLegacyIds = wco.tsConfig.options.enableI18nLegacyMessageIdFormat ?? true;
+
// Ivy extraction is the default for Ivy applications.
usingIvy = (isIvyApplication && options.ivy === undefined) || !!options.ivy;
@@ -172,8 +211,9 @@ export async function execute(
}
const partials = [
- { plugins: [new InMemoryOutputPlugin()] },
+ { plugins: [new NoEmitPlugin()] },
getCommonConfig(wco),
+ getBrowserConfig(wco),
// Only use VE extraction if not using Ivy
getAotConfig(wco, !usingIvy),
getStatsConfig(wco),
@@ -240,11 +280,38 @@ export async function execute(
return webpackResult;
}
+ const basePath = config.context || projectRoot;
+
+ const { checkDuplicateMessages } = await import(
+ // tslint:disable-next-line: trailing-comma
+ '@angular/localize/src/tools/src/extract/duplicates'
+ );
+
+ // The filesystem is used to create a relative path for each file
+ // from the basePath. This relative path is then used in the error message.
+ const checkFileSystem = {
+ relative(from: string, to: string): string {
+ return path.relative(from, to);
+ },
+ };
+ const diagnostics = checkDuplicateMessages(
+ // tslint:disable-next-line: no-any
+ checkFileSystem as any,
+ ivyMessages,
+ 'warning',
+ // tslint:disable-next-line: no-any
+ basePath as any,
+ );
+ if (diagnostics.messages.length > 0) {
+ context.logger.warn(diagnostics.formatDiagnostics(''));
+ }
+
// Serialize all extracted messages
const serializer = await getSerializer(
- options.format,
+ format,
i18n.sourceLocale,
- config.context || projectRoot,
+ basePath,
+ useLegacyIds,
);
const content = serializer.serialize(ivyMessages);
diff --git a/packages/angular_devkit/build_angular/src/extract-i18n/ivy-extract-loader.ts b/packages/angular_devkit/build_angular/src/extract-i18n/ivy-extract-loader.ts
index 8cf600f93f72..07042e18d808 100644
--- a/packages/angular_devkit/build_angular/src/extract-i18n/ivy-extract-loader.ts
+++ b/packages/angular_devkit/build_angular/src/extract-i18n/ivy-extract-loader.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
diff --git a/packages/angular_devkit/build_angular/src/extract-i18n/schema.json b/packages/angular_devkit/build_angular/src/extract-i18n/schema.json
index 5beb6e262b0b..79739483be7c 100644
--- a/packages/angular_devkit/build_angular/src/extract-i18n/schema.json
+++ b/packages/angular_devkit/build_angular/src/extract-i18n/schema.json
@@ -6,7 +6,7 @@
"properties": {
"browserTarget": {
"type": "string",
- "description": "Target to extract from.",
+ "description": "A browser builder target to extract i18n messages in the format of `project:target[:configuration]`. You can also pass in more than one configuration name as a comma-separated list. Example: `project:target:production,staging`.",
"pattern": "^[^:\\s]+:[^:\\s]+(:[^\\s]+)?$"
},
"format": {
@@ -19,7 +19,9 @@
"xlif",
"xliff",
"xlf2",
- "xliff2"
+ "xliff2",
+ "json",
+ "arb"
]
},
"i18nFormat": {
@@ -33,7 +35,9 @@
"xlif",
"xliff",
"xlf2",
- "xliff2"
+ "xliff2",
+ "json",
+ "arb"
]
},
"i18nLocale": {
diff --git a/packages/angular_devkit/build_angular/src/extract-i18n/works_spec.ts b/packages/angular_devkit/build_angular/src/extract-i18n/works_spec.ts
index 9746133a07ef..67a5daa20c8b 100644
--- a/packages/angular_devkit/build_angular/src/extract-i18n/works_spec.ts
+++ b/packages/angular_devkit/build_angular/src/extract-i18n/works_spec.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
@@ -39,6 +39,18 @@ describe('Extract i18n Target', () => {
}
}, 30000);
+ it('does not emit the application files', async () => {
+ host.appendToFile('src/app/app.component.html', 'i18n test
');
+
+ const run = await architect.scheduleTarget(extractI18nTargetSpec);
+
+ await expectAsync(run.result).toBeResolvedTo(jasmine.objectContaining({ success: true }));
+
+ await run.stop();
+
+ expect(host.scopedSync().exists(normalize('dist/app/main.js'))).toBeFalse();
+ }, 30000);
+
it('shows errors', async () => {
const logger = new logging.Logger('');
const logs: string[] = [];
@@ -126,4 +138,29 @@ describe('Extract i18n Target', () => {
expect(virtualFs.fileBufferToString(host.scopedSync().read(extractionFile)))
.toMatch(/i18n test/);
}, 30000);
+
+ // DISABLED_FOR_VE
+ (veEnabled ? xit : it)('issues warnings for duplicate message identifiers', async () => {
+ host.appendToFile(
+ 'src/app/app.component.ts',
+ 'const c = $localize`:@@message-2:message contents`; const d = $localize`:@@message-2:different message contents`;',
+ );
+
+ const logger = new logging.Logger('');
+ const logs: string[] = [];
+ logger.subscribe((e) => logs.push(e.message));
+
+ const run = await architect.scheduleTarget(extractI18nTargetSpec, undefined, { logger });
+ await expectAsync(run.result).toBeResolvedTo(jasmine.objectContaining({ success: true }));
+
+ await run.stop();
+
+ expect(host.scopedSync().exists(extractionFile)).toBe(true);
+
+ const fullLog = logs.join();
+ expect(fullLog).toContain(
+ 'Duplicate messages with id',
+ );
+
+ }, 30000);
});
diff --git a/packages/angular_devkit/build_angular/src/index.ts b/packages/angular_devkit/build_angular/src/index.ts
index 83613150fc33..dc462532a654 100644
--- a/packages/angular_devkit/build_angular/src/index.ts
+++ b/packages/angular_devkit/build_angular/src/index.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
diff --git a/packages/angular_devkit/build_angular/src/karma/code-coverage_spec.ts b/packages/angular_devkit/build_angular/src/karma/code-coverage_spec.ts
index ab92ac8fd261..934598f55fdb 100644
--- a/packages/angular_devkit/build_angular/src/karma/code-coverage_spec.ts
+++ b/packages/angular_devkit/build_angular/src/karma/code-coverage_spec.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
@@ -169,4 +169,31 @@ describe('Karma Builder code coverage', () => {
await run.stop();
}, 120000);
+
+ it('is able to process coverage plugin provided as string', async () => {
+ host.replaceInFile('karma.conf.js', /plugins: \[.+?\]/s, `plugins: [
+ require('karma-jasmine'),
+ require('karma-chrome-launcher'),
+ require('karma-jasmine-html-reporter'),
+ require('@angular-devkit/build-angular/plugins/karma'),
+ 'karma-coverage', // instead of require('karma-coverage')
+ ]`);
+ const run = await architect.scheduleTarget(karmaTargetSpec, { codeCoverage: true });
+
+ const {success} = await run.result;
+ expect(success).toBe(true);
+ await run.stop();
+ }, 120000);
+
+ it('is able to process coverage plugins provided as string karma-*', async () => {
+ host.replaceInFile('karma.conf.js', /plugins: \[.+?\]/s, `plugins: [
+ 'karma-*',
+ require('@angular-devkit/build-angular/plugins/karma'),
+ ]`);
+ const run = await architect.scheduleTarget(karmaTargetSpec, { codeCoverage: true });
+
+ const {success} = await run.result;
+ expect(success).toBe(true);
+ await run.stop();
+ }, 120000);
});
diff --git a/packages/angular_devkit/build_angular/src/karma/find-tests.ts b/packages/angular_devkit/build_angular/src/karma/find-tests.ts
index 17f26197b9da..b02d22aaf3d4 100644
--- a/packages/angular_devkit/build_angular/src/karma/find-tests.ts
+++ b/packages/angular_devkit/build_angular/src/karma/find-tests.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
diff --git a/packages/angular_devkit/build_angular/src/karma/index.ts b/packages/angular_devkit/build_angular/src/karma/index.ts
index 9a4cc9587ffd..de12f6d8e66b 100644
--- a/packages/angular_devkit/build_angular/src/karma/index.ts
+++ b/packages/angular_devkit/build_angular/src/karma/index.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
diff --git a/packages/angular_devkit/build_angular/src/karma/rebuilds_spec.ts b/packages/angular_devkit/build_angular/src/karma/rebuilds_spec.ts
index fd73eb706184..c05b56bae119 100644
--- a/packages/angular_devkit/build_angular/src/karma/rebuilds_spec.ts
+++ b/packages/angular_devkit/build_angular/src/karma/rebuilds_spec.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
diff --git a/packages/angular_devkit/build_angular/src/karma/schema.json b/packages/angular_devkit/build_angular/src/karma/schema.json
index f3402b9df894..7b2583982c31 100644
--- a/packages/angular_devkit/build_angular/src/karma/schema.json
+++ b/packages/angular_devkit/build_angular/src/karma/schema.json
@@ -67,7 +67,7 @@
"description": "Globs of files to include, relative to workspace or project root. \nThere are 2 special cases:\n - when a path to directory is provided, all spec files ending \".spec.@(ts|tsx)\" will be included\n - when a path to a file is provided, and a matching spec file exists it will be included instead"
},
"sourceMap": {
- "description": "Output sourcemaps.",
+ "description": "Output source maps for scripts and styles. For more information, see https://angular.io/guide/workspace-config#source-map-configuration.",
"default": true,
"oneOf": [
{
@@ -75,17 +75,17 @@
"properties": {
"scripts": {
"type": "boolean",
- "description": "Output sourcemaps for all scripts.",
+ "description": "Output source maps for all scripts.",
"default": true
},
"styles": {
"type": "boolean",
- "description": "Output sourcemaps for all styles.",
+ "description": "Output source maps for all styles.",
"default": true
},
"vendor": {
"type": "boolean",
- "description": "Resolve vendor packages sourcemaps.",
+ "description": "Resolve vendor packages source maps.",
"default": false
}
},
@@ -98,7 +98,8 @@
},
"progress": {
"type": "boolean",
- "description": "Log progress to the console while building."
+ "description": "Log progress to the console while building.",
+ "default": true
},
"watch": {
"type": "boolean",
@@ -237,6 +238,7 @@
},
"bundleName": {
"type": "string",
+ "pattern": "^[\\w\\-.]*$",
"description": "The bundle name for this extra entry point."
},
"inject": {
diff --git a/packages/angular_devkit/build_angular/src/karma/selected_spec.ts b/packages/angular_devkit/build_angular/src/karma/selected_spec.ts
index 0b4de133a0fa..1ea72fa40d50 100644
--- a/packages/angular_devkit/build_angular/src/karma/selected_spec.ts
+++ b/packages/angular_devkit/build_angular/src/karma/selected_spec.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
diff --git a/packages/angular_devkit/build_angular/src/karma/works_spec.ts b/packages/angular_devkit/build_angular/src/karma/works_spec.ts
index ad749f97fb1e..24feab4d6007 100644
--- a/packages/angular_devkit/build_angular/src/karma/works_spec.ts
+++ b/packages/angular_devkit/build_angular/src/karma/works_spec.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
diff --git a/packages/angular_devkit/build_angular/src/ng-packagr/index.ts b/packages/angular_devkit/build_angular/src/ng-packagr/index.ts
index d8e2fa24262c..8258eebda601 100644
--- a/packages/angular_devkit/build_angular/src/ng-packagr/index.ts
+++ b/packages/angular_devkit/build_angular/src/ng-packagr/index.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
diff --git a/packages/angular_devkit/build_angular/src/ng-packagr/works_spec.ts b/packages/angular_devkit/build_angular/src/ng-packagr/works_spec.ts
index 550a92f9ef9e..a331e50af15f 100644
--- a/packages/angular_devkit/build_angular/src/ng-packagr/works_spec.ts
+++ b/packages/angular_devkit/build_angular/src/ng-packagr/works_spec.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
diff --git a/packages/angular_devkit/build_angular/src/protractor/index.ts b/packages/angular_devkit/build_angular/src/protractor/index.ts
index 755324d83c78..541e586b30d7 100644
--- a/packages/angular_devkit/build_angular/src/protractor/index.ts
+++ b/packages/angular_devkit/build_angular/src/protractor/index.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
@@ -14,9 +14,11 @@ import {
import { JsonObject, tags } from '@angular-devkit/core';
import { resolve } from 'path';
import * as url from 'url';
+import { DevServerBuilderOptions } from '../dev-server/index';
import { runModuleAsObservableFork } from '../utils';
import { Schema as ProtractorBuilderOptions } from './schema';
+
interface JasmineNodeOpts {
jasmineNodeOpts: {
grep?: string;
@@ -105,7 +107,11 @@ export async function execute(
const target = targetFromTargetString(options.devServerTarget);
const serverOptions = await context.getTargetOptions(target);
- const overrides: Record = { watch: false };
+ const overrides = {
+ watch: false,
+ liveReload: false,
+ } as DevServerBuilderOptions;
+
if (options.host !== undefined) {
overrides.host = options.host;
} else if (typeof serverOptions.host === 'string') {
diff --git a/packages/angular_devkit/build_angular/src/protractor/schema.json b/packages/angular_devkit/build_angular/src/protractor/schema.json
index c4fd14eff06a..ff431254bdfe 100644
--- a/packages/angular_devkit/build_angular/src/protractor/schema.json
+++ b/packages/angular_devkit/build_angular/src/protractor/schema.json
@@ -10,7 +10,7 @@
},
"devServerTarget": {
"type": "string",
- "description": "Dev server target to run tests against.",
+ "description": "A dev-server builder target to run tests against in the format of `project:target[:configuration]`. You can also pass in more than one configuration name as a comma-separated list. Example: `project:target:production,staging`.",
"pattern": "^([^:\\s]+:[^:\\s]+(:[^\\s]+)?)?$"
},
"grep": {
diff --git a/packages/angular_devkit/build_angular/src/protractor/works_spec.ts b/packages/angular_devkit/build_angular/src/protractor/works_spec.ts
index 011b936fbfd5..212a485538a9 100644
--- a/packages/angular_devkit/build_angular/src/protractor/works_spec.ts
+++ b/packages/angular_devkit/build_angular/src/protractor/works_spec.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
diff --git a/packages/angular_devkit/build_angular/src/server/base_spec.ts b/packages/angular_devkit/build_angular/src/server/base_spec.ts
index 9923d1ffc36b..4d880741cca1 100644
--- a/packages/angular_devkit/build_angular/src/server/base_spec.ts
+++ b/packages/angular_devkit/build_angular/src/server/base_spec.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
diff --git a/packages/angular_devkit/build_angular/src/server/external_dependencies_spec.ts b/packages/angular_devkit/build_angular/src/server/external_dependencies_spec.ts
index e8ddba737bfa..30ecdc812b88 100644
--- a/packages/angular_devkit/build_angular/src/server/external_dependencies_spec.ts
+++ b/packages/angular_devkit/build_angular/src/server/external_dependencies_spec.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
diff --git a/packages/angular_devkit/build_angular/src/server/index.ts b/packages/angular_devkit/build_angular/src/server/index.ts
index f19ea4d01e27..74cf6d41aeda 100644
--- a/packages/angular_devkit/build_angular/src/server/index.ts
+++ b/packages/angular_devkit/build_angular/src/server/index.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
@@ -28,7 +28,7 @@ import {
getStatsConfig,
getStylesConfig,
} from '../webpack/configs';
-import { createWebpackLoggingCallback } from '../webpack/utils/stats';
+import { webpackStatsLogger } from '../webpack/utils/stats';
import { Schema as ServerBuilderOptions } from './schema';
// If success is true, outputPath should be set.
@@ -76,7 +76,7 @@ export function execute(
context.logger.warn(tags.stripIndent`
Warning: Turning off 'bundleDependencies' with Ivy may result in undefined behaviour
unless 'node_modules' are transformed using the standalone Angular compatibility compiler (NGCC).
- See: http://v9.angular.io/guide/ivy#ivy-and-universal-app-shell
+ See: https://angular.io/guide/ivy#ivy-and-universal-app-shell
`);
}
}
@@ -85,34 +85,39 @@ export function execute(
concatMap(({ config, i18n }) => {
return runWebpack(config, context, {
webpackFactory: require('webpack') as typeof webpack,
- logging: createWebpackLoggingCallback(!!options.verbose, context.logger),
+ logging: (stats, config) => {
+ if (options.verbose) {
+ context.logger.info(stats.toString(config.stats));
+ }
+ },
}).pipe(
concatMap(async output => {
const { emittedFiles = [], webpackStats } = output;
- if (!output.success || !i18n.shouldInline) {
- return output;
- }
-
if (!webpackStats) {
throw new Error('Webpack stats build result is required.');
}
- outputPaths = ensureOutputPaths(baseOutputPath, i18n);
-
- const success = await i18nInlineEmittedFiles(
- context,
- emittedFiles,
- i18n,
- baseOutputPath,
- Array.from(outputPaths.values()),
- [],
- // tslint:disable-next-line: no-non-null-assertion
- webpackStats.outputPath!,
- target <= ScriptTarget.ES5,
- options.i18nMissingTranslation,
- );
-
- return { output, success };
+ let success = output.success;
+ if (success && i18n.shouldInline) {
+ outputPaths = ensureOutputPaths(baseOutputPath, i18n);
+
+ success = await i18nInlineEmittedFiles(
+ context,
+ emittedFiles,
+ i18n,
+ baseOutputPath,
+ Array.from(outputPaths.values()),
+ [],
+ // tslint:disable-next-line: no-non-null-assertion
+ webpackStats.outputPath!,
+ target <= ScriptTarget.ES5,
+ options.i18nMissingTranslation,
+ );
+ }
+
+ webpackStatsLogger(context.logger, webpackStats, config);
+
+ return { ...output, success };
}),
);
}),
diff --git a/packages/angular_devkit/build_angular/src/server/resources-output-path_spec.ts b/packages/angular_devkit/build_angular/src/server/resources-output-path_spec.ts
index 643f1b272cc9..31c5df9e1048 100644
--- a/packages/angular_devkit/build_angular/src/server/resources-output-path_spec.ts
+++ b/packages/angular_devkit/build_angular/src/server/resources-output-path_spec.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
diff --git a/packages/angular_devkit/build_angular/src/server/schema.json b/packages/angular_devkit/build_angular/src/server/schema.json
index b62571479787..6eac23815460 100644
--- a/packages/angular_devkit/build_angular/src/server/schema.json
+++ b/packages/angular_devkit/build_angular/src/server/schema.json
@@ -29,7 +29,7 @@
"additionalProperties": false
},
"optimization": {
- "description": "Enables optimization of the build output.",
+ "description": "Enables optimization of the build output. Including minification of scripts and styles, tree-shaking and dead-code elimination. For more information, see https://angular.io/guide/workspace-config#optimization-configuration.",
"x-user-analytics": 16,
"default": false,
"oneOf": [
@@ -72,7 +72,7 @@
"default": ""
},
"sourceMap": {
- "description": "Output sourcemaps.",
+ "description": "Output source maps for scripts and styles. For more information, see https://angular.io/guide/workspace-config#source-map-configuration.",
"default": true,
"oneOf": [
{
@@ -80,22 +80,22 @@
"properties": {
"scripts": {
"type": "boolean",
- "description": "Output sourcemaps for all scripts.",
+ "description": "Output source maps for all scripts.",
"default": true
},
"styles": {
"type": "boolean",
- "description": "Output sourcemaps for all styles.",
+ "description": "Output source maps for all styles.",
"default": true
},
"hidden": {
"type": "boolean",
- "description": "Output sourcemaps used for error reporting tools.",
+ "description": "Output source maps used for error reporting tools.",
"default": false
},
"vendor": {
"type": "boolean",
- "description": "Resolve vendor packages sourcemaps.",
+ "description": "Resolve vendor packages source maps.",
"default": false
}
},
@@ -117,7 +117,8 @@
},
"progress": {
"type": "boolean",
- "description": "Log progress to the console while building."
+ "description": "Log progress to the console while building.",
+ "default": true
},
"i18nFile": {
"type": "string",
@@ -141,6 +142,7 @@
"default": "warning"
},
"localize": {
+ "description": "Translate the bundles in one or more locales.",
"oneOf": [
{
"type": "boolean",
@@ -258,10 +260,12 @@
"type": "object",
"properties": {
"src": {
- "type": "string"
+ "type": "string",
+ "pattern": "\\.(([cm]?j|t)sx?|json)$"
},
"replaceWith": {
- "type": "string"
+ "type": "string",
+ "pattern": "\\.(([cm]?j|t)sx?|json)$"
}
},
"additionalProperties": false,
@@ -274,10 +278,12 @@
"type": "object",
"properties": {
"replace": {
- "type": "string"
+ "type": "string",
+ "pattern": "\\.(([cm]?j|t)sx?|json)$"
},
"with": {
- "type": "string"
+ "type": "string",
+ "pattern": "\\.(([cm]?j|t)sx?|json)$"
}
},
"additionalProperties": false,
diff --git a/packages/angular_devkit/build_angular/src/test-utils.ts b/packages/angular_devkit/build_angular/src/test-utils.ts
index 9a1ae6f738f9..65fc9426d21a 100644
--- a/packages/angular_devkit/build_angular/src/test-utils.ts
+++ b/packages/angular_devkit/build_angular/src/test-utils.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
@@ -81,6 +81,15 @@ export async function browserBuild(
const output = (await run.result) as BrowserBuilderOutput;
expect(output.success).toBe(true);
+ if (!output.success) {
+ await run.stop();
+
+ return {
+ output,
+ files: {},
+ };
+ }
+
expect(output.outputPaths[0]).not.toBeUndefined();
const outputPath = normalize(output.outputPaths[0]);
diff --git a/packages/angular_devkit/build_angular/src/testing/builder-harness.ts b/packages/angular_devkit/build_angular/src/testing/builder-harness.ts
new file mode 100644
index 000000000000..9234d0cb4799
--- /dev/null
+++ b/packages/angular_devkit/build_angular/src/testing/builder-harness.ts
@@ -0,0 +1,465 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+import {
+ BuilderContext,
+ BuilderHandlerFn,
+ BuilderInfo,
+ BuilderOutput,
+ BuilderOutputLike,
+ BuilderProgressReport,
+ BuilderRun,
+ ScheduleOptions,
+ Target,
+ fromAsyncIterable,
+ isBuilderOutput,
+} from '@angular-devkit/architect';
+import { WorkspaceHost } from '@angular-devkit/architect/node';
+import { TestProjectHost } from '@angular-devkit/architect/testing';
+import { analytics, getSystemPath, join, json, logging, normalize } from '@angular-devkit/core';
+import { Observable, Subject, from as observableFrom, of as observableOf } from 'rxjs';
+import { catchError, finalize, first, map, mergeMap, shareReplay } from 'rxjs/operators';
+import { BuilderWatcherFactory, WatcherNotifier } from './file-watching';
+
+export interface BuilderHarnessExecutionResult {
+ result?: T;
+ error?: Error;
+ logs: readonly logging.LogEntry[];
+}
+
+export interface BuilderHarnessExecutionOptions {
+ configuration: string;
+ outputLogsOnFailure: boolean;
+ outputLogsOnException: boolean;
+ useNativeFileWatching: boolean;
+}
+
+export class BuilderHarness {
+ private readonly builderInfo: BuilderInfo;
+ private schemaRegistry = new json.schema.CoreSchemaRegistry();
+ private projectName = 'test';
+ private projectMetadata: Record = { root: '.', sourceRoot: 'src' };
+ private targetName?: string;
+ private options = new Map();
+ private builderTargets = new Map<
+ string,
+ // tslint:disable-next-line: no-any
+ { handler: BuilderHandlerFn; info: BuilderInfo; options: json.JsonObject }
+ >();
+ private watcherNotifier?: WatcherNotifier;
+
+ constructor(
+ private readonly builderHandler: BuilderHandlerFn,
+ private readonly host: TestProjectHost,
+ builderInfo?: Partial,
+ ) {
+ // Generate default pseudo builder info for test purposes
+ this.builderInfo = {
+ builderName: builderHandler.name,
+ description: '',
+ optionSchema: true,
+ ...builderInfo,
+ };
+
+ this.schemaRegistry.addPostTransform(json.schema.transforms.addUndefinedDefaults);
+ }
+
+ useProject(name: string, metadata: Record = {}): this {
+ if (!name) {
+ throw new Error('Project name cannot be an empty string.');
+ }
+
+ this.projectName = name;
+ this.projectMetadata = metadata;
+
+ return this;
+ }
+
+ useTarget(name: string, baseOptions: T): this {
+ if (!name) {
+ throw new Error('Target name cannot be an empty string.');
+ }
+
+ this.targetName = name;
+ this.options.set(null, baseOptions);
+
+ return this;
+ }
+
+ withConfiguration(configuration: string, options: T): this {
+ this.options.set(configuration, options);
+
+ return this;
+ }
+
+ withBuilderTarget(
+ target: string,
+ handler: BuilderHandlerFn,
+ options?: O,
+ info?: Partial,
+ ): this {
+ this.builderTargets.set(target, {
+ handler,
+ options: options || {},
+ info: { builderName: handler.name, description: '', optionSchema: true, ...info },
+ });
+
+ return this;
+ }
+
+ execute(
+ options: Partial = {},
+ ): Observable {
+ const {
+ configuration,
+ outputLogsOnException = true,
+ outputLogsOnFailure = true,
+ useNativeFileWatching = false,
+ } = options;
+
+ const targetOptions = {
+ ...this.options.get(null),
+ ...((configuration && this.options.get(configuration)) ?? {}),
+ };
+
+ if (!useNativeFileWatching) {
+ if (this.watcherNotifier) {
+ throw new Error('Only one harness execution at a time is supported.');
+ }
+ this.watcherNotifier = new WatcherNotifier();
+ }
+
+ const contextHost: ContextHost = {
+ findBuilderByTarget: async (project, target) => {
+ this.validateProjectName(project);
+ if (target === this.targetName) {
+ return {
+ info: this.builderInfo,
+ handler: this.builderHandler as BuilderHandlerFn,
+ };
+ }
+
+ const builderTarget = this.builderTargets.get(target);
+ if (builderTarget) {
+ return { info: builderTarget.info, handler: builderTarget.handler };
+ }
+
+ throw new Error('Project target does not exist.');
+ },
+ async getBuilderName(project, target) {
+ return (await this.findBuilderByTarget(project, target)).info.builderName;
+ },
+ getMetadata: async (project) => {
+ this.validateProjectName(project);
+
+ return this.projectMetadata as json.JsonObject;
+ },
+ getOptions: async (project, target, configuration) => {
+ this.validateProjectName(project);
+ if (target === this.targetName) {
+ return this.options.get(configuration ?? null) ?? {};
+ } else if (configuration !== undefined) {
+ // Harness builder targets currently do not support configurations
+ return {};
+ } else {
+ return (this.builderTargets.get(target)?.options as json.JsonObject) || {};
+ }
+ },
+ hasTarget: async (project, target) => {
+ this.validateProjectName(project);
+
+ return this.targetName === target || this.builderTargets.has(target);
+ },
+ validate: async (options, builderName) => {
+ let schema;
+ if (builderName === this.builderInfo.builderName) {
+ schema = this.builderInfo.optionSchema;
+ } else {
+ for (const [, value] of this.builderTargets) {
+ if (value.info.builderName === builderName) {
+ schema = value.info.optionSchema;
+ break;
+ }
+ }
+ }
+
+ const validator = await this.schemaRegistry.compile(schema ?? true).toPromise();
+ const { data } = await validator(options).toPromise();
+
+ return data as json.JsonObject;
+ },
+ };
+ const context = new HarnessBuilderContext(
+ this.builderInfo,
+ getSystemPath(this.host.root()),
+ contextHost,
+ useNativeFileWatching ? undefined : this.watcherNotifier,
+ );
+ if (this.targetName !== undefined) {
+ context.target = {
+ project: this.projectName,
+ target: this.targetName,
+ configuration: configuration as string,
+ };
+ }
+
+ const logs: logging.LogEntry[] = [];
+ context.logger.subscribe((e) => logs.push(e));
+
+ return this.schemaRegistry.compile(this.builderInfo.optionSchema).pipe(
+ mergeMap((validator) => validator(targetOptions)),
+ map((validationResult) => validationResult.data),
+ mergeMap((data) =>
+ convertBuilderOutputToObservable(this.builderHandler(data as T & json.JsonObject, context)),
+ ),
+ map((buildResult) => ({ result: buildResult, error: undefined })),
+ catchError((error) => {
+ if (outputLogsOnException) {
+ // tslint:disable-next-line: no-console
+ console.error(logs.map((entry) => entry.message).join('\n'));
+ // tslint:disable-next-line: no-console
+ console.error(error);
+ }
+
+ return observableOf({ result: undefined, error });
+ }),
+ map(({ result, error }) => {
+ if (outputLogsOnFailure && result?.success === false && logs.length > 0) {
+ // tslint:disable-next-line: no-console
+ console.error(logs.map((entry) => entry.message).join('\n'));
+ }
+
+ // Capture current logs and clear for next
+ const currentLogs = logs.slice();
+ logs.length = 0;
+
+ return { result, error, logs: currentLogs };
+ }),
+ finalize(() => {
+ this.watcherNotifier = undefined;
+
+ for (const teardown of context.teardowns) {
+ teardown();
+ }
+ }),
+ );
+ }
+
+ async executeOnce(
+ options?: Partial,
+ ): Promise {
+ // Return the first result
+ return this.execute(options).pipe(first()).toPromise();
+ }
+
+ async appendToFile(path: string, content: string): Promise {
+ await this.writeFile(path, this.readFile(path).concat(content));
+ }
+
+ async writeFile(path: string, content: string | Buffer): Promise {
+ this.host
+ .scopedSync()
+ .write(normalize(path), typeof content === 'string' ? Buffer.from(content) : content);
+
+ this.watcherNotifier?.notify([
+ { path: getSystemPath(join(this.host.root(), path)), type: 'modified' },
+ ]);
+ }
+
+ async writeFiles(files: Record): Promise {
+ const watchEvents = this.watcherNotifier
+ ? ([] as { path: string; type: 'modified' | 'deleted' }[])
+ : undefined;
+
+ for (const [path, content] of Object.entries(files)) {
+ this.host
+ .scopedSync()
+ .write(normalize(path), typeof content === 'string' ? Buffer.from(content) : content);
+
+ watchEvents?.push({ path: getSystemPath(join(this.host.root(), path)), type: 'modified' });
+ }
+
+ if (watchEvents) {
+ this.watcherNotifier?.notify(watchEvents);
+ }
+ }
+
+ async removeFile(path: string): Promise {
+ this.host.scopedSync().delete(normalize(path));
+
+ this.watcherNotifier?.notify([
+ { path: getSystemPath(join(this.host.root(), path)), type: 'deleted' },
+ ]);
+ }
+
+ async modifyFile(
+ path: string,
+ modifier: (content: string) => string | Promise,
+ ): Promise {
+ const content = this.readFile(path);
+ await this.writeFile(path, await modifier(content));
+
+ this.watcherNotifier?.notify([
+ { path: getSystemPath(join(this.host.root(), path)), type: 'modified' },
+ ]);
+ }
+
+ hasFile(path: string): boolean {
+ return this.host.scopedSync().exists(normalize(path));
+ }
+
+ hasFileMatch(directory: string, pattern: RegExp): boolean {
+ return this.host.scopedSync()
+ .list(normalize(directory))
+ .some(name => pattern.test(name));
+ }
+
+ readFile(path: string): string {
+ const content = this.host.scopedSync().read(normalize(path));
+
+ return Buffer.from(content).toString('utf8');
+ }
+
+ private validateProjectName(name: string): void {
+ if (name !== this.projectName) {
+ throw new Error(`Project "${name}" does not exist.`);
+ }
+ }
+}
+
+interface ContextHost extends WorkspaceHost {
+ findBuilderByTarget(
+ project: string,
+ target: string,
+ ): Promise<{ info: BuilderInfo; handler: BuilderHandlerFn }>;
+ validate(options: json.JsonObject, builderName: string): Promise;
+}
+
+class HarnessBuilderContext implements BuilderContext {
+ id = Math.trunc(Math.random() * 1000000);
+ logger = new logging.Logger(`builder-harness-${this.id}`);
+ workspaceRoot: string;
+ currentDirectory: string;
+ target?: Target;
+
+ teardowns: (() => Promise | void)[] = [];
+
+ constructor(
+ public builder: BuilderInfo,
+ basePath: string,
+ private readonly contextHost: ContextHost,
+ public readonly watcherFactory: BuilderWatcherFactory | undefined,
+ ) {
+ this.workspaceRoot = this.currentDirectory = basePath;
+ }
+
+ get analytics(): analytics.Analytics {
+ // Can be undefined even though interface does not allow it
+ return (undefined as unknown) as analytics.Analytics;
+ }
+
+ addTeardown(teardown: () => Promise | void): void {
+ this.teardowns.push(teardown);
+ }
+
+ async getBuilderNameForTarget(target: Target): Promise {
+ return this.contextHost.getBuilderName(target.project, target.target);
+ }
+
+ async getProjectMetadata(targetOrName: Target | string): Promise {
+ const project = typeof targetOrName === 'string' ? targetOrName : targetOrName.project;
+
+ return this.contextHost.getMetadata(project);
+ }
+
+ async getTargetOptions(target: Target): Promise {
+ return this.contextHost.getOptions(target.project, target.target, target.configuration);
+ }
+
+ // Unused by builders in this package
+ async scheduleBuilder(
+ builderName: string,
+ options?: json.JsonObject,
+ scheduleOptions?: ScheduleOptions,
+ ): Promise {
+ throw new Error('Not Implemented.');
+ }
+
+ async scheduleTarget(
+ target: Target,
+ overrides?: json.JsonObject,
+ scheduleOptions?: ScheduleOptions,
+ ): Promise {
+ const { info, handler } = await this.contextHost.findBuilderByTarget(
+ target.project,
+ target.target,
+ );
+ const targetOptions = await this.validateOptions(
+ {
+ ...(await this.getTargetOptions(target)),
+ ...overrides,
+ },
+ info.builderName,
+ );
+
+ const context = new HarnessBuilderContext(
+ info,
+ this.workspaceRoot,
+ this.contextHost,
+ this.watcherFactory,
+ );
+ context.target = target;
+ context.logger = scheduleOptions?.logger || this.logger.createChild('');
+
+ const progressSubject = new Subject();
+ const output = convertBuilderOutputToObservable(handler(targetOptions, context));
+
+ const run: BuilderRun = {
+ id: context.id,
+ info,
+ progress: progressSubject.asObservable(),
+ async stop() {
+ for (const teardown of context.teardowns) {
+ await teardown();
+ }
+ progressSubject.complete();
+ },
+ output: output.pipe(shareReplay()),
+ get result() {
+ return this.output.pipe(first()).toPromise();
+ },
+ };
+
+ return run;
+ }
+
+ async validateOptions(
+ options: json.JsonObject,
+ builderName: string,
+ ): Promise {
+ return (this.contextHost.validate(options, builderName) as unknown) as T;
+ }
+
+ // Unused report methods
+ reportRunning(): void {}
+ reportStatus(): void {}
+ reportProgress(): void {}
+}
+
+function isAsyncIterable(obj: unknown): obj is AsyncIterable {
+ return !!obj && typeof (obj as AsyncIterable)[Symbol.asyncIterator] === 'function';
+}
+
+function convertBuilderOutputToObservable(output: BuilderOutputLike): Observable {
+ if (isBuilderOutput(output)) {
+ return observableOf(output);
+ } else if (isAsyncIterable(output)) {
+ return fromAsyncIterable(output);
+ } else {
+ return observableFrom(output);
+ }
+}
diff --git a/packages/angular_devkit/build_angular/src/testing/builder-harness_spec.ts b/packages/angular_devkit/build_angular/src/testing/builder-harness_spec.ts
new file mode 100644
index 000000000000..ddbc2e195eac
--- /dev/null
+++ b/packages/angular_devkit/build_angular/src/testing/builder-harness_spec.ts
@@ -0,0 +1,134 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+import { TestProjectHost } from '@angular-devkit/architect/testing';
+import { BuilderHarness } from './builder-harness';
+
+describe('BuilderHarness', () => {
+ let mockHost: TestProjectHost;
+
+ beforeEach(() => {
+ mockHost = jasmine.createSpyObj('TestProjectHost', ['root']);
+ (mockHost.root as jasmine.Spy).and.returnValue('.');
+ });
+
+ it('uses the provided builder handler', async () => {
+ const mockHandler = jasmine.createSpy().and.returnValue({ success: true });
+
+ const harness = new BuilderHarness(mockHandler, mockHost);
+
+ await harness.executeOnce();
+
+ expect(mockHandler).toHaveBeenCalled();
+ });
+
+ it('provides the builder output result when executing', async () => {
+ const mockHandler = jasmine.createSpy().and.returnValue({ success: false, property: 'value' });
+
+ const harness = new BuilderHarness(mockHandler, mockHost);
+ const { result } = await harness.executeOnce();
+
+ expect(result).toBeDefined();
+ expect(result?.success).toBeFalse();
+ expect(result?.property).toBe('value');
+ });
+
+ it('does not show builder logs on console when a builder succeeds', async () => {
+ const consoleErrorMock = spyOn(console, 'error');
+
+ const harness = new BuilderHarness(async (_, context) => {
+ context.logger.warn('TEST WARNING');
+
+ return { success: true };
+ }, mockHost);
+
+ const { result } = await harness.executeOnce();
+
+ expect(result).toBeDefined();
+ expect(result?.success).toBeTrue();
+
+ expect(consoleErrorMock).not.toHaveBeenCalledWith(jasmine.stringMatching('TEST WARNING'));
+ });
+
+ it('shows builder logs on console when a builder fails', async () => {
+ const consoleErrorMock = spyOn(console, 'error');
+
+ const harness = new BuilderHarness(async (_, context) => {
+ context.logger.warn('TEST WARNING');
+
+ return { success: false };
+ }, mockHost);
+
+ const { result } = await harness.executeOnce();
+
+ expect(result).toBeDefined();
+ expect(result?.success).toBeFalse();
+
+ expect(consoleErrorMock).toHaveBeenCalledWith(jasmine.stringMatching('TEST WARNING'));
+ });
+
+ it('does not show builder logs on console when a builder fails and outputLogsOnFailure: false', async () => {
+ const consoleErrorMock = spyOn(console, 'error');
+
+ const harness = new BuilderHarness(async (_, context) => {
+ context.logger.warn('TEST WARNING');
+
+ return { success: false };
+ }, mockHost);
+
+ const { result } = await harness.executeOnce({ outputLogsOnFailure: false });
+
+ expect(result).toBeDefined();
+ expect(result?.success).toBeFalse();
+
+ expect(consoleErrorMock).not.toHaveBeenCalledWith(jasmine.stringMatching('TEST WARNING'));
+ });
+
+ it('provides and logs the builder output exception when builder throws', async () => {
+ const mockHandler = jasmine.createSpy().and.throwError(new Error('Builder Error'));
+ const consoleErrorMock = spyOn(console, 'error');
+
+ const harness = new BuilderHarness(mockHandler, mockHost);
+ const { result, error } = await harness.executeOnce();
+
+ expect(result).toBeUndefined();
+ expect(error).toEqual(jasmine.objectContaining({ message: 'Builder Error' }));
+ expect(consoleErrorMock).toHaveBeenCalledWith(jasmine.stringMatching('Builder Error'));
+ });
+
+ it('does not log exception with outputLogsOnException false when builder throws', async () => {
+ const mockHandler = jasmine.createSpy().and.throwError(new Error('Builder Error'));
+ const consoleErrorMock = spyOn(console, 'error');
+
+ const harness = new BuilderHarness(mockHandler, mockHost);
+ const { result, error } = await harness.executeOnce({ outputLogsOnException: false });
+
+ expect(result).toBeUndefined();
+ expect(error).toEqual(jasmine.objectContaining({ message: 'Builder Error' }));
+ expect(consoleErrorMock).not.toHaveBeenCalledWith(jasmine.stringMatching('Builder Error'));
+ });
+
+ it('supports executing a target from within a builder', async () => {
+ const mockHandler = jasmine.createSpy().and.returnValue({ success: true });
+
+ const harness = new BuilderHarness(async (_, context) => {
+ const run = await context.scheduleTarget({project: 'test', target: 'another' });
+ expect(await run.result).toEqual(jasmine.objectContaining({ success: true }));
+ await run.stop();
+
+ return { success: true };
+ }, mockHost);
+ harness.withBuilderTarget('another', mockHandler);
+
+ const { result } = await harness.executeOnce();
+
+ expect(result).toBeDefined();
+ expect(result?.success).toBeTrue();
+
+ expect(mockHandler).toHaveBeenCalled();
+ });
+});
diff --git a/packages/angular_devkit/build_angular/src/testing/file-watching.ts b/packages/angular_devkit/build_angular/src/testing/file-watching.ts
new file mode 100644
index 000000000000..868ef4769f1c
--- /dev/null
+++ b/packages/angular_devkit/build_angular/src/testing/file-watching.ts
@@ -0,0 +1,51 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+import {
+ BuilderWatcherCallback,
+ BuilderWatcherFactory,
+} from '../webpack/plugins/builder-watch-plugin';
+
+class WatcherDescriptor {
+ constructor(
+ readonly files: ReadonlySet,
+ readonly directories: ReadonlySet,
+ readonly callback: BuilderWatcherCallback,
+ ) {}
+
+ shouldNotify(path: string): boolean {
+ return true;
+ }
+}
+
+export class WatcherNotifier implements BuilderWatcherFactory {
+ private readonly descriptors = new Set();
+
+ notify(events: Iterable<{ path: string; type: 'modified' | 'deleted' }>): void {
+ for (const descriptor of this.descriptors) {
+ for (const { path } of events) {
+ if (descriptor.shouldNotify(path)) {
+ descriptor.callback([...events]);
+ break;
+ }
+ }
+ }
+ }
+
+ watch(
+ files: Iterable,
+ directories: Iterable,
+ callback: BuilderWatcherCallback,
+ ): { close(): void } {
+ const descriptor = new WatcherDescriptor(new Set(files), new Set(directories), callback);
+ this.descriptors.add(descriptor);
+
+ return { close: () => this.descriptors.delete(descriptor) };
+ }
+}
+
+export { BuilderWatcherFactory };
diff --git a/packages/angular_devkit/build_angular/src/testing/index.ts b/packages/angular_devkit/build_angular/src/testing/index.ts
new file mode 100644
index 000000000000..7887f1feebc1
--- /dev/null
+++ b/packages/angular_devkit/build_angular/src/testing/index.ts
@@ -0,0 +1,9 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+export { BuilderHarnessExecutionOptions, BuilderHarnessExecutionResult } from './builder-harness';
+export { HarnessFileMatchers, describeBuilder } from './jasmine-helpers';
diff --git a/packages/angular_devkit/build_angular/src/testing/jasmine-helpers.ts b/packages/angular_devkit/build_angular/src/testing/jasmine-helpers.ts
new file mode 100644
index 000000000000..d13dfd11c3af
--- /dev/null
+++ b/packages/angular_devkit/build_angular/src/testing/jasmine-helpers.ts
@@ -0,0 +1,126 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+import { BuilderHandlerFn } from '@angular-devkit/architect';
+import { json } from '@angular-devkit/core';
+import { readFileSync } from 'fs';
+import { host } from '../test-utils';
+import { BuilderHarness } from './builder-harness';
+
+const optionSchemaCache = new Map();
+
+export function describeBuilder(
+ builderHandler: BuilderHandlerFn,
+ options: { name?: string; schemaPath: string },
+ specDefinitions: (harness: JasmineBuilderHarness) => void,
+): void {
+ let optionSchema = optionSchemaCache.get(options.schemaPath);
+ if (optionSchema === undefined) {
+ optionSchema = JSON.parse(readFileSync(options.schemaPath, 'utf8')) as json.schema.JsonSchema;
+ optionSchemaCache.set(options.schemaPath, optionSchema);
+ }
+ const harness = new JasmineBuilderHarness(builderHandler, host, {
+ builderName: options.name,
+ optionSchema,
+ });
+
+ describe(options.name || builderHandler.name, () => {
+ beforeEach(() => host.initialize().toPromise());
+
+ afterEach(() => host.restore().toPromise());
+
+ specDefinitions(harness);
+ });
+}
+
+class JasmineBuilderHarness extends BuilderHarness {
+ expectFile(path: string): HarnessFileMatchers {
+ return expectFile(path, this);
+ }
+}
+
+export interface HarnessFileMatchers {
+ toExist(): boolean;
+ toNotExist(): boolean;
+ readonly content: jasmine.ArrayLikeMatchers;
+ readonly size: jasmine.Matchers;
+}
+
+/**
+ * Add a Jasmine expectation filter to an expectation that always fails with a message.
+ * @param base The base expectation (`expect(...)`) to use.
+ * @param message The message to provide in the expectation failure.
+ */
+function createFailureExpectation(base: T, message: string): T {
+ // Needed typings are not included in the Jasmine types
+ const expectation = base as T & {
+ expector: {
+ addFilter(filter: {
+ selectComparisonFunc(): () => { pass: boolean; message: string };
+ }): typeof expectation.expector;
+ };
+ };
+ expectation.expector = expectation.expector.addFilter({
+ selectComparisonFunc() {
+ return () => ({
+ pass: false,
+ message,
+ });
+ },
+ });
+
+ return expectation;
+}
+
+export function expectFile(path: string, harness: BuilderHarness): HarnessFileMatchers {
+ return {
+ toExist() {
+ const exists = harness.hasFile(path);
+ expect(exists).toBe(true, 'Expected file to exist: ' + path);
+
+ return exists;
+ },
+ toNotExist() {
+ const exists = harness.hasFile(path);
+ expect(exists).toBe(false, 'Expected file to not exist: ' + path);
+
+ return !exists;
+ },
+ get content() {
+ try {
+ return expect(harness.readFile(path)).withContext(`With file content for '${path}'`);
+ } catch (e) {
+ if (e.code !== 'ENOENT') {
+ throw e;
+ }
+
+ // File does not exist so always fail the expectation
+ return createFailureExpectation(
+ expect(''),
+ `Expected file content but file does not exist: '${path}'`,
+ );
+ }
+ },
+ get size() {
+ try {
+ return expect(Buffer.byteLength(harness.readFile(path))).withContext(
+ `With file size for '${path}'`,
+ );
+ } catch (e) {
+ if (e.code !== 'ENOENT') {
+ throw e;
+ }
+
+ // File does not exist so always fail the expectation
+ return createFailureExpectation(
+ expect(0),
+ `Expected file size but file does not exist: '${path}'`,
+ );
+ }
+ },
+ };
+}
diff --git a/packages/angular_devkit/build_angular/src/transforms.ts b/packages/angular_devkit/build_angular/src/transforms.ts
index c724aedab646..2e6399ca9acc 100644
--- a/packages/angular_devkit/build_angular/src/transforms.ts
+++ b/packages/angular_devkit/build_angular/src/transforms.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
diff --git a/packages/angular_devkit/build_angular/src/tslint/index.ts b/packages/angular_devkit/build_angular/src/tslint/index.ts
index 36e2a5cd96c2..777b97344662 100644
--- a/packages/angular_devkit/build_angular/src/tslint/index.ts
+++ b/packages/angular_devkit/build_angular/src/tslint/index.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
@@ -28,7 +28,7 @@ async function _run(
): Promise {
context.logger.warn(
`TSLint's support is discontinued and we're deprecating its support in Angular CLI.\n` +
- 'To opt-in using the community driven ESLint builder, see: https://github.com/angular-eslint/angular-eslint#migrating-from-codelyzer-and-tslint.',
+ 'To opt-in using the community driven ESLint builder, see: https://github.com/angular-eslint/angular-eslint#migrating-an-angular-cli-project-from-codelyzer-and-tslint.',
);
const systemRoot = context.workspaceRoot;
diff --git a/packages/angular_devkit/build_angular/src/tslint/works_spec.ts b/packages/angular_devkit/build_angular/src/tslint/works_spec.ts
index 9b850eb32531..ad0360941bd0 100644
--- a/packages/angular_devkit/build_angular/src/tslint/works_spec.ts
+++ b/packages/angular_devkit/build_angular/src/tslint/works_spec.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
@@ -14,7 +14,7 @@ import {
schema,
workspaces,
} from '@angular-devkit/core';
-import { NodeJsAsyncHost } from '@angular-devkit/core/node';
+import { NodeJsSyncHost } from '@angular-devkit/core/node';
import { workspaceRoot } from '../test-utils';
@@ -32,7 +32,7 @@ describe('Tslint Target', () => {
const { workspace } = await workspaces.readWorkspace(
normalize(workspaceRoot),
- workspaces.createWorkspaceHost(new NodeJsAsyncHost()),
+ workspaces.createWorkspaceHost(new NodeJsSyncHost()),
);
testArchitectHost = new TestingArchitectHost(
diff --git a/packages/angular_devkit/build_angular/src/typings.d.ts b/packages/angular_devkit/build_angular/src/typings.d.ts
index 9a5d06cfa771..121268adf4fb 100644
--- a/packages/angular_devkit/build_angular/src/typings.d.ts
+++ b/packages/angular_devkit/build_angular/src/typings.d.ts
@@ -1,22 +1,10 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
-
-// Workaround for https://github.com/bazelbuild/rules_nodejs/issues/1033
-// Alternative approach instead of https://github.com/angular/angular/pull/33226
-declare module '@babel/core' {
- export * from '@types/babel__core';
-}
-declare module '@babel/generator' {
- export { default } from '@types/babel__generator';
-}
-declare module '@babel/traverse' {
- export { default } from '@types/babel__traverse';
-}
-declare module '@babel/template' {
- export { default } from '@types/babel__template';
+declare module '@discoveryjs/json-ext' {
+ export function stringifyStream(value: unknown): import('stream').Readable;
}
diff --git a/packages/angular_devkit/build_angular/src/utils/action-cache.ts b/packages/angular_devkit/build_angular/src/utils/action-cache.ts
index 9a2fd8f0a407..f61869b0f506 100644
--- a/packages/angular_devkit/build_angular/src/utils/action-cache.ts
+++ b/packages/angular_devkit/build_angular/src/utils/action-cache.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
@@ -33,15 +33,21 @@ export class BundleActionCache {
}
}
- generateBaseCacheKey(content: string): string {
- // Create base cache key with elements:
- // * package version - different build-angular versions cause different final outputs
- // * code length/hash - ensure cached version matches the same input code
+ generateIntegrityValue(content: string): string {
const algorithm = this.integrityAlgorithm || 'sha1';
const codeHash = createHash(algorithm)
.update(content)
.digest('base64');
- let baseCacheKey = `${packageVersion}|${content.length}|${algorithm}-${codeHash}`;
+
+ return `${algorithm}-${codeHash}`;
+ }
+
+ generateBaseCacheKey(content: string): string {
+ // Create base cache key with elements:
+ // * package version - different build-angular versions cause different final outputs
+ // * code length/hash - ensure cached version matches the same input code
+ const integrity = this.generateIntegrityValue(content);
+ let baseCacheKey = `${packageVersion}|${content.length}|${integrity}`;
if (!allowMangle) {
baseCacheKey += '|MD';
}
@@ -116,7 +122,10 @@ export class BundleActionCache {
return null;
}
- const result: ProcessBundleResult = { name: action.name };
+ const result: ProcessBundleResult = {
+ name: action.name,
+ integrity: this.generateIntegrityValue(action.code),
+ };
let cacheEntry = entries[CacheKey.OriginalCode];
if (cacheEntry) {
diff --git a/packages/angular_devkit/build_angular/src/utils/action-executor.ts b/packages/angular_devkit/build_angular/src/utils/action-executor.ts
index a57d59e28d20..921b3edd39a5 100644
--- a/packages/angular_devkit/build_angular/src/utils/action-executor.ts
+++ b/packages/angular_devkit/build_angular/src/utils/action-executor.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
diff --git a/packages/angular_devkit/build_angular/src/utils/build-browser-features.ts b/packages/angular_devkit/build_angular/src/utils/build-browser-features.ts
index 29abf699a050..f1b783b2b258 100644
--- a/packages/angular_devkit/build_angular/src/utils/build-browser-features.ts
+++ b/packages/angular_devkit/build_angular/src/utils/build-browser-features.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
diff --git a/packages/angular_devkit/build_angular/src/utils/build-browser-features_spec.ts b/packages/angular_devkit/build_angular/src/utils/build-browser-features_spec.ts
index 17ef63fde0e3..09c9603a46d7 100644
--- a/packages/angular_devkit/build_angular/src/utils/build-browser-features_spec.ts
+++ b/packages/angular_devkit/build_angular/src/utils/build-browser-features_spec.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
diff --git a/packages/angular_devkit/build_angular/src/utils/build-options.ts b/packages/angular_devkit/build_angular/src/utils/build-options.ts
index 70954cb64af2..540d1b1546e3 100644
--- a/packages/angular_devkit/build_angular/src/utils/build-options.ts
+++ b/packages/angular_devkit/build_angular/src/utils/build-options.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
@@ -14,14 +14,16 @@ import {
CrossOrigin,
ExtraEntryPoint,
I18NMissingTranslation,
+ IndexUnion,
Localize,
- OptimizationClass,
SourceMapClass,
} from '../browser/schema';
+import { Schema as DevServerSchema } from '../dev-server/schema';
import { NormalizedFileReplacement } from './normalize-file-replacements';
+import { NormalizedOptimizationOptions } from './normalize-optimization';
export interface BuildOptions {
- optimization: OptimizationClass;
+ optimization: NormalizedOptimizationOptions;
environment?: string;
outputPath: string;
resourcesOutputPath?: string;
@@ -48,6 +50,7 @@ export interface BuildOptions {
watch?: boolean;
outputHashing?: string;
poll?: number;
+ index?: IndexUnion;
deleteOutputPath?: boolean;
preserveSymlinks?: boolean;
extractLicenses?: boolean;
@@ -61,7 +64,6 @@ export interface BuildOptions {
statsJson: boolean;
forkTypeChecker: boolean;
hmr?: boolean;
-
main: string;
polyfills?: string;
budgets: Budget[];
@@ -73,13 +75,11 @@ export interface BuildOptions {
lazyModules: string[];
platform?: 'browser' | 'server';
fileReplacements: NormalizedFileReplacement[];
- /** @deprecated use only for compatibility in 8.x; will be removed in 9.0 */
- rebaseRootRelativeCssUrls?: boolean;
experimentalRollupPass?: boolean;
allowedCommonJsDependencies?: string[];
- differentialLoadingMode?: boolean;
+ differentialLoadingNeeded?: boolean;
}
export interface WebpackTestOptions extends BuildOptions {
@@ -87,6 +87,8 @@ export interface WebpackTestOptions extends BuildOptions {
codeCoverageExclude?: string[];
}
+export interface WebpackDevServerOptions extends BuildOptions, Omit { }
+
export interface WebpackConfigOptions {
root: string;
logger: logging.Logger;
@@ -95,5 +97,5 @@ export interface WebpackConfigOptions {
buildOptions: T;
tsConfig: ParsedConfiguration;
tsConfigPath: string;
- supportES2015: boolean;
+ scriptTarget: import('typescript').ScriptTarget;
}
diff --git a/packages/angular_devkit/build_angular/src/utils/bundle-calculator.ts b/packages/angular_devkit/build_angular/src/utils/bundle-calculator.ts
index 8f27e1986115..b1e9650e8901 100644
--- a/packages/angular_devkit/build_angular/src/utils/bundle-calculator.ts
+++ b/packages/angular_devkit/build_angular/src/utils/bundle-calculator.ts
@@ -1,10 +1,11 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
+import { basename } from 'path';
import * as webpack from 'webpack';
import { Budget, Type } from '../browser/schema';
import { ProcessBundleFile, ProcessBundleResult } from '../utils/process-bundle';
@@ -32,9 +33,8 @@ export enum ThresholdSeverity {
}
enum DifferentialBuildType {
- // FIXME: this should match the actual file suffix and not hardcoded.
- ORIGINAL = 'es2015',
- DOWNLEVEL = 'es5',
+ ORIGINAL = 'original',
+ DOWNLEVEL = 'downlevel',
}
export function* calculateThresholds(budget: Budget): IterableIterator {
@@ -181,6 +181,18 @@ abstract class Calculator {
.reduce((l, r) => l + r, 0);
}
}
+
+ protected getAssetSize(asset: Asset): number {
+ if (asset.name.endsWith('.js')) {
+ const processResult = this.processResults
+ .find((processResult) => processResult.original && basename(processResult.original.filename) === asset.name);
+ if (processResult?.original) {
+ return processResult.original.size;
+ }
+ }
+
+ return asset.size;
+ }
}
/**
@@ -193,15 +205,17 @@ class BundleCalculator extends Calculator {
return [];
}
+ const buildTypeLabels = getBuildTypeLabels(this.processResults);
+
// The chunk may or may not have differential builds. Compute the size for
// each then check afterwards if they are all the same.
const buildSizes = Object.values(DifferentialBuildType).map((buildType) => {
const size = this.chunks
- .filter(chunk => chunk.names.indexOf(budgetName) !== -1)
- .map(chunk => this.calculateChunkSize(chunk, buildType))
- .reduce((l, r) => l + r, 0);
+ .filter(chunk => chunk.names.includes(budgetName))
+ .map(chunk => this.calculateChunkSize(chunk, buildType))
+ .reduce((l, r) => l + r, 0);
- return {size, label: `bundle ${this.budget.name}-${buildType}`};
+ return { size, label: `bundle ${this.budget.name}-${buildTypeLabels[buildType]}` };
});
// If this bundle was not actually generated by a differential build, then
@@ -219,13 +233,14 @@ class BundleCalculator extends Calculator {
*/
class InitialCalculator extends Calculator {
calculate() {
+ const buildTypeLabels = getBuildTypeLabels(this.processResults);
const buildSizes = Object.values(DifferentialBuildType).map((buildType) => {
return {
- label: `bundle initial-${buildType}`,
+ label: `bundle initial-${buildTypeLabels[buildType]}`,
size: this.chunks
- .filter(chunk => chunk.initial)
- .map(chunk => this.calculateChunkSize(chunk, buildType))
- .reduce((l, r) => l + r, 0),
+ .filter(chunk => chunk.initial)
+ .map(chunk => this.calculateChunkSize(chunk, buildType))
+ .reduce((l, r) => l + r, 0),
};
});
@@ -246,7 +261,7 @@ class AllScriptCalculator extends Calculator {
calculate() {
const size = this.assets
.filter(asset => asset.name.endsWith('.js'))
- .map(asset => asset.size)
+ .map(asset => this.getAssetSize(asset))
.reduce((total: number, size: number) => total + size, 0);
return [{size, label: 'total scripts'}];
@@ -260,7 +275,7 @@ class AllCalculator extends Calculator {
calculate() {
const size = this.assets
.filter(asset => !asset.name.endsWith('.map'))
- .map(asset => asset.size)
+ .map(asset => this.getAssetSize(asset))
.reduce((total: number, size: number) => total + size, 0);
return [{size, label: 'total'}];
@@ -275,7 +290,7 @@ class AnyScriptCalculator extends Calculator {
return this.assets
.filter(asset => asset.name.endsWith('.js'))
.map(asset => ({
- size: asset.size,
+ size: this.getAssetSize(asset),
label: asset.name,
}));
}
@@ -289,7 +304,7 @@ class AnyCalculator extends Calculator {
return this.assets
.filter(asset => !asset.name.endsWith('.map'))
.map(asset => ({
- size: asset.size,
+ size: this.getAssetSize(asset),
label: asset.name,
}));
}
@@ -419,3 +434,19 @@ function mergeDifferentialBuildSizes(buildSizes: Size[], mergeLabel: string): Si
function allEquivalent(items: Iterable): boolean {
return new Set(items).size < 2;
}
+
+function getBuildTypeLabels(processResults: ProcessBundleResult[]): Record {
+ const fileNameSuffixRegExp = /\-(es20\d{2}|esnext)\./;
+ const originalFileName = processResults
+ .find(({ original }) => original?.filename && fileNameSuffixRegExp.test(original.filename))?.original?.filename;
+
+ let originalSuffix: string | undefined;
+ if (originalFileName) {
+ originalSuffix = fileNameSuffixRegExp.exec(originalFileName)?.[1];
+ }
+
+ return {
+ [DifferentialBuildType.DOWNLEVEL]: 'es5',
+ [DifferentialBuildType.ORIGINAL]: originalSuffix || 'es2015',
+ };
+}
diff --git a/packages/angular_devkit/build_angular/src/utils/bundle-calculator_spec.ts b/packages/angular_devkit/build_angular/src/utils/bundle-calculator_spec.ts
index da599f2d3a6a..d0cbe3daa5ba 100644
--- a/packages/angular_devkit/build_angular/src/utils/bundle-calculator_spec.ts
+++ b/packages/angular_devkit/build_angular/src/utils/bundle-calculator_spec.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
@@ -240,7 +240,7 @@ describe('bundle-calculator', () => {
name: '0',
// Individual builds are under budget, but combined they are over.
original: {
- filename: 'initial-es2015.js',
+ filename: 'initial-es2017.js',
size: 1.25 * KB,
},
downlevel: {
@@ -255,7 +255,7 @@ describe('bundle-calculator', () => {
expect(failures.length).toBe(2);
expect(failures).toContain({
severity: ThresholdSeverity.Error,
- message: jasmine.stringMatching('bundle initial-es2015 exceeded maximum budget.'),
+ message: jasmine.stringMatching('bundle initial-es2017 exceeded maximum budget.'),
});
expect(failures).toContain({
severity: ThresholdSeverity.Error,
@@ -464,5 +464,169 @@ describe('bundle-calculator', () => {
message: jasmine.stringMatching('foo.ext exceeded maximum budget.'),
});
});
+
+ it('does *not* yield a combined differential bundle budget for any script', () => {
+ const budgets: Budget[] = [{
+ type: Type.AnyScript,
+ maximumError: '1kb',
+ }];
+ const stats = {
+ chunks: [
+ {
+ initial: true,
+ files: [ 'foo.js' ],
+ },
+ ],
+ assets: [
+ {
+ name: 'main-es2015.js',
+ size: 1.25 * KB,
+ },
+ ],
+ } as unknown as webpack.Stats.ToJsonOutput;
+ const processResults: ProcessBundleResult[] = [
+ {
+ name: '0',
+ // Individual builds are under budget, but combined they are over.
+ original: {
+ filename: '/home/main-es2015.js',
+ size: 0.5 * KB,
+ },
+ downlevel: {
+ filename: '/home/main-es5.js',
+ size: 0.75 * KB,
+ },
+ },
+ ];
+
+ const failures = Array.from(checkBudgets(budgets, stats, processResults));
+
+ // Because individual builds are under budget, they are acceptable. Should
+ // **not** yield a combined build which is over budget.
+ expect(failures.length).toBe(0);
+ });
+
+ it('does *not* yield a combined differential bundle budget for all script', () => {
+ const budgets: Budget[] = [{
+ type: Type.AllScript,
+ maximumError: '1kb',
+ }];
+ const stats = {
+ chunks: [
+ {
+ initial: true,
+ files: [ 'foo.js' ],
+ },
+ ],
+ assets: [
+ {
+ name: 'main-es2015.js',
+ size: 1.25 * KB,
+ },
+ ],
+ } as unknown as webpack.Stats.ToJsonOutput;
+ const processResults: ProcessBundleResult[] = [
+ {
+ name: '0',
+ // Individual builds are under budget, but combined they are over.
+ original: {
+ filename: '/home/main-es2015.js',
+ size: 0.5 * KB,
+ },
+ downlevel: {
+ filename: '/home/main-es5.js',
+ size: 0.75 * KB,
+ },
+ },
+ ];
+
+ const failures = Array.from(checkBudgets(budgets, stats, processResults));
+
+ // Because individual builds are under budget, they are acceptable. Should
+ // **not** yield a combined build which is over budget.
+ expect(failures.length).toBe(0);
+ });
+
+ it('does *not* yield a combined differential bundle budget for total budget', () => {
+ const budgets: Budget[] = [{
+ type: Type.All,
+ maximumError: '1kb',
+ }];
+ const stats = {
+ chunks: [
+ {
+ initial: true,
+ files: [ 'foo.js' ],
+ },
+ ],
+ assets: [
+ {
+ name: 'main-es2015.js',
+ size: 1.25 * KB,
+ },
+ ],
+ } as unknown as webpack.Stats.ToJsonOutput;
+ const processResults: ProcessBundleResult[] = [
+ {
+ name: '0',
+ // Individual builds are under budget, but combined they are over.
+ original: {
+ filename: '/home/main-es2015.js',
+ size: 0.5 * KB,
+ },
+ downlevel: {
+ filename: '/home/main-es5.js',
+ size: 0.75 * KB,
+ },
+ },
+ ];
+
+ const failures = Array.from(checkBudgets(budgets, stats, processResults));
+
+ // Because individual builds are under budget, they are acceptable. Should
+ // **not** yield a combined build which is over budget.
+ expect(failures.length).toBe(0);
+ });
+
+ it('does *not* yield a combined differential bundle budget for individual file budget', () => {
+ const budgets: Budget[] = [{
+ type: Type.Any,
+ maximumError: '1kb',
+ }];
+ const stats = {
+ chunks: [
+ {
+ initial: true,
+ files: [ 'foo.js' ],
+ },
+ ],
+ assets: [
+ {
+ name: 'main-es2015.js',
+ size: 1.25 * KB,
+ },
+ ],
+ } as unknown as webpack.Stats.ToJsonOutput;
+ const processResults: ProcessBundleResult[] = [
+ {
+ name: '0',
+ // Individual builds are under budget, but combined they are over.
+ original: {
+ filename: '/home/main-es2015.js',
+ size: 0.5 * KB,
+ },
+ downlevel: {
+ filename: '/home/main-es5.js',
+ size: 0.75 * KB,
+ },
+ },
+ ];
+
+ const failures = Array.from(checkBudgets(budgets, stats, processResults));
+
+ // Because individual builds are under budget, they are acceptable. Should
+ // **not** yield a combined build which is over budget.
+ expect(failures.length).toBe(0);
+ });
});
});
diff --git a/packages/angular_devkit/build_angular/src/utils/cache-path.ts b/packages/angular_devkit/build_angular/src/utils/cache-path.ts
index a6eeffdd7495..a1a9180b7242 100644
--- a/packages/angular_devkit/build_angular/src/utils/cache-path.ts
+++ b/packages/angular_devkit/build_angular/src/utils/cache-path.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
diff --git a/packages/angular_devkit/build_angular/src/utils/check-port.ts b/packages/angular_devkit/build_angular/src/utils/check-port.ts
index 1a21cfa61fb0..4048cc5866d2 100644
--- a/packages/angular_devkit/build_angular/src/utils/check-port.ts
+++ b/packages/angular_devkit/build_angular/src/utils/check-port.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
diff --git a/packages/angular_devkit/build_angular/src/utils/color.ts b/packages/angular_devkit/build_angular/src/utils/color.ts
index 6e7d11cb7fe2..c729fa6a5811 100644
--- a/packages/angular_devkit/build_angular/src/utils/color.ts
+++ b/packages/angular_devkit/build_angular/src/utils/color.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
diff --git a/packages/angular_devkit/build_angular/src/utils/copy-assets.ts b/packages/angular_devkit/build_angular/src/utils/copy-assets.ts
index 17e2fdd64c20..d69a4c6e074a 100644
--- a/packages/angular_devkit/build_angular/src/utils/copy-assets.ts
+++ b/packages/angular_devkit/build_angular/src/utils/copy-assets.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
@@ -17,7 +17,14 @@ function globAsync(pattern: string, options: glob.IOptions) {
}
export async function copyAssets(
- entries: { glob: string; ignore?: string[]; input: string; output: string; flatten?: boolean }[],
+ entries: {
+ glob: string;
+ ignore?: string[];
+ input: string;
+ output: string;
+ flatten?: boolean;
+ followSymlinks?: boolean;
+ }[],
basePaths: Iterable,
root: string,
changed?: Set,
@@ -31,6 +38,7 @@ export async function copyAssets(
dot: true,
nodir: true,
ignore: entry.ignore ? defaultIgnore.concat(entry.ignore) : defaultIgnore,
+ follow: entry.followSymlinks,
});
const directoryExists = new Set();
diff --git a/packages/angular_devkit/build_angular/src/utils/copy-file.ts b/packages/angular_devkit/build_angular/src/utils/copy-file.ts
index f935599fe95f..252f9475990b 100644
--- a/packages/angular_devkit/build_angular/src/utils/copy-file.ts
+++ b/packages/angular_devkit/build_angular/src/utils/copy-file.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
diff --git a/packages/angular_devkit/build_angular/src/utils/default-progress.ts b/packages/angular_devkit/build_angular/src/utils/default-progress.ts
index 258412b460f1..ce78668341a0 100644
--- a/packages/angular_devkit/build_angular/src/utils/default-progress.ts
+++ b/packages/angular_devkit/build_angular/src/utils/default-progress.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
diff --git a/packages/angular_devkit/build_angular/src/utils/delete-output-dir.ts b/packages/angular_devkit/build_angular/src/utils/delete-output-dir.ts
index bb8c648a1220..534e1e9a3229 100644
--- a/packages/angular_devkit/build_angular/src/utils/delete-output-dir.ts
+++ b/packages/angular_devkit/build_angular/src/utils/delete-output-dir.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
diff --git a/packages/angular_devkit/build_angular/src/utils/environment-options.ts b/packages/angular_devkit/build_angular/src/utils/environment-options.ts
index 2d896edb9389..ff57506f3f9d 100644
--- a/packages/angular_devkit/build_angular/src/utils/environment-options.ts
+++ b/packages/angular_devkit/build_angular/src/utils/environment-options.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
@@ -82,3 +82,8 @@ export const cachingBasePath = (() => {
// Build profiling
const profilingVariable = process.env['NG_BUILD_PROFILING'];
export const profilingEnabled = isPresent(profilingVariable) && isEnabled(profilingVariable);
+
+// Legacy Webpack plugin with Ivy
+const legacyIvyVariable = process.env['NG_BUILD_IVY_LEGACY'];
+export const legacyIvyPluginEnabled =
+ isPresent(legacyIvyVariable) && !isDisabled(legacyIvyVariable);
diff --git a/packages/angular_devkit/build_angular/src/utils/find-up.ts b/packages/angular_devkit/build_angular/src/utils/find-up.ts
index e499326b8822..7f5266f277f5 100644
--- a/packages/angular_devkit/build_angular/src/utils/find-up.ts
+++ b/packages/angular_devkit/build_angular/src/utils/find-up.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
@@ -9,34 +9,6 @@ import { existsSync } from 'fs';
import * as path from 'path';
import { isDirectory } from './is-directory';
-export function findUp(names: string | string[], from: string, stopOnNodeModules = false): string | null {
- if (!Array.isArray(names)) {
- names = [names];
- }
- const root = path.parse(from).root;
-
- let currentDir = from;
- while (currentDir && currentDir !== root) {
- for (const name of names) {
- const p = path.join(currentDir, name);
- if (existsSync(p)) {
- return p;
- }
- }
-
- if (stopOnNodeModules) {
- const nodeModuleP = path.join(currentDir, 'node_modules');
- if (existsSync(nodeModuleP)) {
- return null;
- }
- }
-
- currentDir = path.dirname(currentDir);
- }
-
- return null;
-}
-
export function findAllNodeModules(from: string, root?: string): string[] {
const nodeModules: string[] = [];
diff --git a/packages/angular_devkit/build_angular/src/utils/fs.ts b/packages/angular_devkit/build_angular/src/utils/fs.ts
new file mode 100644
index 000000000000..66240a1a0e99
--- /dev/null
+++ b/packages/angular_devkit/build_angular/src/utils/fs.ts
@@ -0,0 +1,14 @@
+/**
+ * @license
+ * Copyright Google LLC All Rights Reserved.
+ *
+ * Use of this source code is governed by an MIT-style license that can be
+ * found in the LICENSE file at https://angular.io/license
+ */
+
+import * as fs from 'fs';
+import { promisify } from 'util';
+
+export const mkdir = promisify(fs.mkdir);
+export const readFile = promisify(fs.readFile);
+export const writeFile = promisify(fs.writeFile);
diff --git a/packages/angular_devkit/build_angular/src/utils/i18n-inlining.ts b/packages/angular_devkit/build_angular/src/utils/i18n-inlining.ts
index 66a905c1ef95..ae60ad21c663 100644
--- a/packages/angular_devkit/build_angular/src/utils/i18n-inlining.ts
+++ b/packages/angular_devkit/build_angular/src/utils/i18n-inlining.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
@@ -8,13 +8,12 @@
import { BuilderContext } from '@angular-devkit/architect';
import { EmittedFiles } from '@angular-devkit/build-webpack';
import * as fs from 'fs';
-import * as ora from 'ora';
import * as path from 'path';
import { BundleActionExecutor } from './action-executor';
-import { colors } from './color';
import { copyAssets } from './copy-assets';
import { I18nOptions } from './i18n-options';
import { InlineOptions } from './process-bundle';
+import { Spinner } from './spinner';
function emittedFilesToInlineOptions(
emittedFiles: EmittedFiles[],
@@ -75,7 +74,8 @@ export async function i18nInlineEmittedFiles(
): Promise {
const executor = new BundleActionExecutor({ i18n });
let hasErrors = false;
- const spinner = ora('Generating localized bundles...').start();
+ const spinner = new Spinner();
+ spinner.start('Generating localized bundles...');
try {
const { options, originalFiles: processedFiles } = emittedFilesToInlineOptions(
@@ -114,7 +114,7 @@ export async function i18nInlineEmittedFiles(
'',
);
} catch (err) {
- spinner.fail(colors.redBright('Localized bundle generation failed: ' + err.message));
+ spinner.fail('Localized bundle generation failed: ' + err.message);
return false;
} finally {
@@ -122,7 +122,7 @@ export async function i18nInlineEmittedFiles(
}
if (hasErrors) {
- spinner.fail(colors.redBright('Localized bundle generation failed.'));
+ spinner.fail('Localized bundle generation failed.');
} else {
spinner.succeed('Localized bundle generation complete.');
}
diff --git a/packages/angular_devkit/build_angular/src/utils/i18n-options.ts b/packages/angular_devkit/build_angular/src/utils/i18n-options.ts
index 870b01179973..e21bc2553788 100644
--- a/packages/angular_devkit/build_angular/src/utils/i18n-options.ts
+++ b/packages/angular_devkit/build_angular/src/utils/i18n-options.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
@@ -31,6 +31,7 @@ export interface I18nOptions {
flatOutput?: boolean;
readonly shouldInline: boolean;
veCompatLocale?: string;
+ hasDefinedSourceLocale?: boolean;
}
function normalizeTranslationFileOption(
@@ -93,6 +94,7 @@ export function createI18nOptions(
}
i18n.sourceLocale = rawSourceLocale;
+ i18n.hasDefinedSourceLocale = true;
}
i18n.locales[i18n.sourceLocale] = {
@@ -202,91 +204,98 @@ export async function configureI18nBuild 0) {
- const projectRoot = path.join(context.workspaceRoot, (metadata.root as string) || '');
- const localeDataBasePath = findLocaleDataBasePath(projectRoot);
- if (!localeDataBasePath) {
- throw new Error(
- `Unable to find locale data within '@angular/common'. Please ensure '@angular/common' is installed.`,
- );
+ // No additional processing needed if no inlining requested and no source locale defined.
+ if (!i18n.shouldInline && !i18n.hasDefinedSourceLocale) {
+ return { buildOptions, i18n };
+ }
+
+ const projectRoot = path.join(context.workspaceRoot, (metadata.root as string) || '');
+ const localeDataBasePath = findLocaleDataBasePath(projectRoot);
+ if (!localeDataBasePath) {
+ throw new Error(
+ `Unable to find locale data within '@angular/common'. Please ensure '@angular/common' is installed.`,
+ );
+ }
+
+ // Load locale data and translations (if present)
+ let loader;
+ const usedFormats = new Set();
+ for (const [locale, desc] of Object.entries(i18n.locales)) {
+ if (!i18n.inlineLocales.has(locale) && locale !== i18n.sourceLocale) {
+ continue;
}
- // Load locales
- const loader = await createTranslationLoader();
- const usedFormats = new Set();
- for (const [locale, desc] of Object.entries(i18n.locales)) {
- if (!i18n.inlineLocales.has(locale)) {
- continue;
+ let localeDataPath = findLocaleDataPath(locale, localeDataBasePath);
+ if (!localeDataPath) {
+ const [first] = locale.split('-');
+ if (first) {
+ localeDataPath = findLocaleDataPath(first.toLowerCase(), localeDataBasePath);
+ if (localeDataPath) {
+ context.logger.warn(
+ `Locale data for '${locale}' cannot be found. Using locale data for '${first}'.`,
+ );
+ }
}
+ }
+ if (!localeDataPath) {
+ context.logger.warn(
+ `Locale data for '${locale}' cannot be found. No locale data will be included for this locale.`,
+ );
+ } else {
+ desc.dataPath = localeDataPath;
+ }
- let localeDataPath = findLocaleDataPath(locale, localeDataBasePath);
- if (!localeDataPath) {
- const [first] = locale.split('-');
- if (first) {
- localeDataPath = findLocaleDataPath(first.toLowerCase(), localeDataBasePath);
- if (localeDataPath) {
- context.logger.warn(
- `Locale data for '${locale}' cannot be found. Using locale data for '${first}'.`,
- );
- }
+ if (!desc.files.length) {
+ continue;
+ }
+
+ if (!loader) {
+ loader = await createTranslationLoader();
+ }
+
+ for (const file of desc.files) {
+ const loadResult = loader(path.join(context.workspaceRoot, file.path));
+
+ for (const diagnostics of loadResult.diagnostics.messages) {
+ if (diagnostics.type === 'error') {
+ throw new Error(
+ `Error parsing translation file '${file.path}': ${diagnostics.message}`,
+ );
+ } else {
+ context.logger.warn(`WARNING [${file.path}]: ${diagnostics.message}`);
}
}
- if (!localeDataPath) {
+
+ if (loadResult.locale !== undefined && loadResult.locale !== locale) {
context.logger.warn(
- `Locale data for '${locale}' cannot be found. No locale data will be included for this locale.`,
+ `WARNING [${file.path}]: File target locale ('${loadResult.locale}') does not match configured locale ('${locale}')`,
);
- } else {
- desc.dataPath = localeDataPath;
}
- if (!desc.files.length) {
- continue;
+ usedFormats.add(loadResult.format);
+ if (usedFormats.size > 1 && tsConfig.options.enableI18nLegacyMessageIdFormat !== false) {
+ // This limitation is only for legacy message id support (defaults to true as of 9.0)
+ throw new Error(
+ 'Localization currently only supports using one type of translation file format for the entire application.',
+ );
}
- for (const file of desc.files) {
- const loadResult = loader(path.join(context.workspaceRoot, file.path));
+ file.format = loadResult.format;
+ file.integrity = loadResult.integrity;
- for (const diagnostics of loadResult.diagnostics.messages) {
- if (diagnostics.type === 'error') {
- throw new Error(
- `Error parsing translation file '${file.path}': ${diagnostics.message}`,
+ if (desc.translation) {
+ // Merge translations
+ for (const [id, message] of Object.entries(loadResult.translations)) {
+ if (desc.translation[id] !== undefined) {
+ context.logger.warn(
+ `WARNING [${file.path}]: Duplicate translations for message '${id}' when merging`,
);
- } else {
- context.logger.warn(`WARNING [${file.path}]: ${diagnostics.message}`);
}
+ desc.translation[id] = message;
}
-
- if (loadResult.locale !== undefined && loadResult.locale !== locale) {
- context.logger.warn(
- `WARNING [${file.path}]: File target locale ('${loadResult.locale}') does not match configured locale ('${locale}')`,
- );
- }
-
- usedFormats.add(loadResult.format);
- if (usedFormats.size > 1 && tsConfig.options.enableI18nLegacyMessageIdFormat !== false) {
- // This limitation is only for legacy message id support (defaults to true as of 9.0)
- throw new Error(
- 'Localization currently only supports using one type of translation file format for the entire application.',
- );
- }
-
- file.format = loadResult.format;
- file.integrity = loadResult.integrity;
-
- if (desc.translation) {
- // Merge translations
- for (const [id, message] of Object.entries(loadResult.translations)) {
- if (desc.translation[id] !== undefined) {
- context.logger.warn(
- `WARNING [${file.path}]: Duplicate translations for message '${id}' when merging`,
- );
- }
- desc.translation[id] = message;
- }
- } else {
- // First or only translation file
- desc.translation = loadResult.translations;
- }
+ } else {
+ // First or only translation file
+ desc.translation = loadResult.translations;
}
}
diff --git a/packages/angular_devkit/build_angular/src/utils/index-file/augment-index-html.ts b/packages/angular_devkit/build_angular/src/utils/index-file/augment-index-html.ts
index 26b3ca7b42c1..1799d261b338 100644
--- a/packages/angular_devkit/build_angular/src/utils/index-file/augment-index-html.ts
+++ b/packages/angular_devkit/build_angular/src/utils/index-file/augment-index-html.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
@@ -14,10 +14,8 @@ export type LoadOutputFileFunctionType = (file: string) => Promise;
export type CrossOriginValue = 'none' | 'anonymous' | 'use-credentials';
export interface AugmentIndexHtmlOptions {
- /* Input file name (e. g. index.html) */
- input: string;
/* Input contents */
- inputContent: string;
+ html: string;
baseHref?: string;
deployUrl?: string;
sri: boolean;
@@ -59,7 +57,7 @@ export interface FileInfo {
export async function augmentIndexHtml(params: AugmentIndexHtmlOptions): Promise {
const {
loadOutputFile, files, noModuleFiles = [], moduleFiles = [], entrypoints,
- sri, deployUrl = '', lang, baseHref, inputContent,
+ sri, deployUrl = '', lang, baseHref, html,
} = params;
let { crossOrigin = 'none' } = params;
@@ -89,7 +87,7 @@ export async function augmentIndexHtml(params: AugmentIndexHtmlOptions): Promise
}
}
- const scriptTags: string[] = [];
+ let scriptTags: string[] = [];
for (const script of scripts) {
const attrs = [`src="${deployUrl}${script}"`];
@@ -126,7 +124,7 @@ export async function augmentIndexHtml(params: AugmentIndexHtmlOptions): Promise
scriptTags.push(``);
}
- const linkTags: string[] = [];
+ let linkTags: string[] = [];
for (const stylesheet of stylesheets) {
const attrs = [
`rel="stylesheet"`,
@@ -145,8 +143,8 @@ export async function augmentIndexHtml(params: AugmentIndexHtmlOptions): Promise
linkTags.push(` `);
}
- const { rewriter, transformedContent } = await htmlRewritingStream(inputContent);
- const baseTagExists = inputContent.includes(' {
@@ -182,19 +180,30 @@ export async function augmentIndexHtml(params: AugmentIndexHtmlOptions): Promise
for (const linkTag of linkTags) {
rewriter.emitRaw(linkTag);
}
+
+ linkTags = [];
break;
case 'body':
// Add script tags
for (const scriptTag of scriptTags) {
rewriter.emitRaw(scriptTag);
}
+
+ scriptTags = [];
break;
}
rewriter.emitEndTag(tag);
});
- return transformedContent;
+ const content = await transformedContent;
+
+ if (linkTags.length || scriptTags.length) {
+ // In case no body/head tags are not present (dotnet partial templates)
+ return linkTags.join('') + scriptTags.join('') + content;
+ }
+
+ return content;
}
function generateSriAttributes(content: string): string {
diff --git a/packages/angular_devkit/build_angular/src/utils/index-file/augment-index-html_spec.ts b/packages/angular_devkit/build_angular/src/utils/index-file/augment-index-html_spec.ts
index f210c74ae951..3baed5fb5b26 100644
--- a/packages/angular_devkit/build_angular/src/utils/index-file/augment-index-html_spec.ts
+++ b/packages/angular_devkit/build_angular/src/utils/index-file/augment-index-html_spec.ts
@@ -1,6 +1,6 @@
/**
* @license
- * Copyright Google Inc. All Rights Reserved.
+ * Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
@@ -10,8 +10,7 @@ import { AugmentIndexHtmlOptions, FileInfo, augmentIndexHtml } from './augment-i
describe('augment-index-html', () => {
const indexGeneratorOptions: AugmentIndexHtmlOptions = {
- input: 'index.html',
- inputContent: '',
+ html: '',
baseHref: '/',
sri: false,
files: [],
@@ -52,7 +51,7 @@ describe('augment-index-html', () => {
it('should replace base href value', async () => {
const source = augmentIndexHtml({
...indexGeneratorOptions,
- inputContent: ' ',
+ html: ' ',
baseHref: '/Apps/',
});
@@ -167,4 +166,27 @@ describe('augment-index-html', () => {