diff --git a/.circleci/config.yml b/.circleci/config.yml
index 261c3d6a3d60..93f62f2662d1 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -514,8 +514,8 @@ jobs:
- *setup_bazel_remote_execution
- *yarn_install
- *setup_bazel_binary
- # Integration tests run with --config=view-engine because we release with View Engine.
- - run: bazel test integration/... --build_tests_only --config=view-engine
+ - run: yarn integration-tests
+ - run: yarn integration-tests:view-engine
# ----------------------------------------------------------------------------
# Job that runs all Bazel tests against material-components-web@canary
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index b3f4d74792e4..3d423e46a985 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -319,3 +319,5 @@
/.vscode/** @angular/dev-infra-components @mmalerba
/src/* @jelbourn
/goldens/ts-circular-deps.json @angular/dev-infra-components
+/goldens/size-test.json @jelbourn @mmalerba @crisbeto
+/goldens/* @angular/dev-infra-components
diff --git a/WORKSPACE b/WORKSPACE
index 9dda87b249d0..84c17d08b8f0 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -8,8 +8,8 @@ load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
# Add NodeJS rules
http_archive(
name = "build_bazel_rules_nodejs",
- sha256 = "d14076339deb08e5460c221fae5c5e9605d2ef4848eee1f0c81c9ffdc1ab31c1",
- urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/1.6.1/rules_nodejs-1.6.1.tar.gz"],
+ sha256 = "84abf7ac4234a70924628baa9a73a5a5cbad944c4358cf9abdb4aab29c9a5b77",
+ urls = ["https://github.com/bazelbuild/rules_nodejs/releases/download/1.7.0/rules_nodejs-1.7.0.tar.gz"],
)
# Add sass rules
diff --git a/goldens/BUILD.bazel b/goldens/BUILD.bazel
new file mode 100644
index 000000000000..9dc126ec82a8
--- /dev/null
+++ b/goldens/BUILD.bazel
@@ -0,0 +1,3 @@
+exports_files([
+ "size-test.json",
+])
diff --git a/goldens/size-test.json b/goldens/size-test.json
new file mode 100644
index 000000000000..6c61b305ec21
--- /dev/null
+++ b/goldens/size-test.json
@@ -0,0 +1,3 @@
+{
+ "material/list": 215881
+}
\ No newline at end of file
diff --git a/integration/size-test/BUILD.bazel b/integration/size-test/BUILD.bazel
new file mode 100644
index 000000000000..bbeaa8dc256a
--- /dev/null
+++ b/integration/size-test/BUILD.bazel
@@ -0,0 +1,17 @@
+load("//tools:defaults.bzl", "ts_library")
+
+package(default_visibility = ["//visibility:public"])
+
+exports_files([
+ "rollup.config.js",
+ "terser-config.json",
+])
+
+ts_library(
+ name = "check-size",
+ srcs = ["check-size.ts"],
+ deps = [
+ "@npm//@types/node",
+ "@npm//chalk",
+ ],
+)
diff --git a/integration/size-test/check-size.ts b/integration/size-test/check-size.ts
new file mode 100644
index 000000000000..80bd9ccfc618
--- /dev/null
+++ b/integration/size-test/check-size.ts
@@ -0,0 +1,75 @@
+/*
+ * Script that measures the size of a given test file and compares it
+ * with an entry in a file size golden. If the size deviates by certain
+ * amount, the script will fail with a non-zero exit code.
+ */
+
+import chalk from 'chalk';
+import {readFileSync, statSync, writeFileSync} from 'fs';
+
+/**
+ * Absolute byte deviation from the expected value that is allowed. If the
+ * size deviates by 500 bytes of the expected value, the script will fail.
+ */
+const ABSOLUTE_BYTE_THRESHOLD = 500;
+/**
+ * Percentage deviation from the expected value that is allowed. If the
+ * size deviates by 1% of the expected value, the script will fail.
+ */
+const PERCENTAGE_DEVIATION_THRESHOLD = 1;
+
+/**
+ * Extracted command line arguments specified by the Bazel NodeJS binaries:
+ * - `testId`: Unique id for the given test file that is used as key in the golden.
+ * - `testFileRootpath`: Bazel rootpath that resolves to the test file that should be measured.
+ * - `isApprove`: Whether the script runs in approve mode, and the golden should be updated
+ * with the actual measured size.
+ */
+const [testId, testFileRootpath, isApprove] = process.argv.slice(2);
+const testFilePath = require.resolve(`angular_material/${testFileRootpath}`);
+const goldenFilePath = require.resolve('../../goldens/size-test.json');
+
+const golden = JSON.parse(readFileSync(goldenFilePath, 'utf8'));
+const fileStat = statSync(testFilePath);
+const actualSize = fileStat.size;
+
+// If in approve mode, update the golden to reflect the new size.
+if (isApprove) {
+ golden[testId] = actualSize;
+ writeFileSync(goldenFilePath, JSON.stringify(golden, null, 2));
+ console.info(chalk.green(`Approved golden size for "${testId}"`));
+ process.exit(0);
+}
+
+// If no size has been collected for the test id, report an error.
+if (golden[testId] === undefined) {
+ console.error(`No golden size for "${testId}". Please create a new entry.`);
+ printApproveCommand();
+ process.exit(1);
+}
+
+const expectedSize = Number(golden[testId]);
+const absoluteSizeDiff = Math.abs(actualSize - expectedSize);
+const deviatedByPercentage =
+ absoluteSizeDiff > (expectedSize * PERCENTAGE_DEVIATION_THRESHOLD / 100);
+const deviatedByAbsoluteDiff = absoluteSizeDiff > ABSOLUTE_BYTE_THRESHOLD;
+
+// Always print the expected and actual size so that it's easier to find culprit
+// commits when the size slowly moves toward the threshold boundaries.
+console.info(chalk.yellow(`Expected: ${expectedSize}, but got: ${actualSize}`));
+
+if (deviatedByPercentage) {
+ console.error(`Actual size deviates by more than 1% of the expected size. `);
+ printApproveCommand();
+ process.exit(1);
+} else if (deviatedByAbsoluteDiff) {
+ console.error(`Actual size deviates by more than 500 bytes from the expected.`);
+ printApproveCommand();
+ process.exit(1);
+}
+
+/** Prints the command for approving the current test. */
+function printApproveCommand() {
+ console.info(chalk.yellow('You can approve the golden by running the following command:'));
+ console.info(chalk.yellow(` bazel run ${process.env.BAZEL_TARGET}.approve`));
+}
diff --git a/integration/size-test/index.bzl b/integration/size-test/index.bzl
new file mode 100644
index 000000000000..65b8b27831cb
--- /dev/null
+++ b/integration/size-test/index.bzl
@@ -0,0 +1,74 @@
+load("@build_bazel_rules_nodejs//:index.bzl", "nodejs_binary", "nodejs_test")
+load("@npm_bazel_rollup//:index.bzl", "rollup_bundle")
+load("@npm_bazel_terser//:index.bzl", "terser_minified")
+load("//tools:defaults.bzl", "ng_module")
+
+"""
+ Performs size measurements for the specified file. The file will be built as part
+ of a `ng_module` and then will be optimized with build-optimizer, rollup and Terser.
+
+ The resulting size will be validated against a golden file to ensure that we don't
+ regress in payload size, or that we can improvements to payload size.
+"""
+
+def size_test(name, file, deps):
+ # Generates an unique id for the given size test. e.g. if the macro is called
+ # within the `integration/size-test/material` package with `name = list`, then
+ # the unique id will be set to `material/list`.
+ test_id = "%s/%s" % (native.package_name()[len("integration/size-test/"):], name)
+
+ ng_module(
+ name = "%s_lib" % name,
+ srcs = ["%s.ts" % name],
+ testonly = True,
+ deps = [
+ "@npm//@angular/core",
+ "@npm//@angular/platform-browser-dynamic",
+ ] + deps,
+ )
+
+ rollup_bundle(
+ name = "%s_bundle" % name,
+ config_file = "//integration/size-test:rollup.config.js",
+ testonly = True,
+ entry_points = {
+ (file): "%s_bundled" % name,
+ },
+ deps = [
+ ":%s_lib" % name,
+ "@npm//rollup-plugin-node-resolve",
+ "@npm//@angular-devkit/build-optimizer",
+ ],
+ sourcemap = "false",
+ )
+
+ terser_minified(
+ testonly = True,
+ name = "%s_bundle_min" % name,
+ src = ":%s_bundle" % name,
+ config_file = "//integration/size-test:terser-config.json",
+ sourcemap = False,
+ )
+
+ nodejs_test(
+ name = name,
+ data = [
+ "//goldens:size-test.json",
+ "//integration/size-test:check-size",
+ ":%s_bundle_min" % name,
+ ],
+ entry_point = "//integration/size-test:check-size.ts",
+ args = [test_id, "$(rootpath :%s_bundle_min)" % name],
+ )
+
+ nodejs_binary(
+ name = "%s.approve" % name,
+ testonly = True,
+ data = [
+ "//goldens:size-test.json",
+ "//integration/size-test:check-size",
+ ":%s_bundle_min" % name,
+ ],
+ entry_point = "//integration/size-test:check-size.ts",
+ args = [test_id, "$(rootpath :%s_bundle_min)" % name, "true"],
+ )
diff --git a/integration/size-test/material/BUILD.bazel b/integration/size-test/material/BUILD.bazel
new file mode 100644
index 000000000000..487138c903ab
--- /dev/null
+++ b/integration/size-test/material/BUILD.bazel
@@ -0,0 +1,7 @@
+load("//integration/size-test:index.bzl", "size_test")
+
+size_test(
+ name = "list",
+ file = "list.ts",
+ deps = ["//src/material/list"],
+)
diff --git a/integration/size-test/material/list.ts b/integration/size-test/material/list.ts
new file mode 100644
index 000000000000..935402c38c49
--- /dev/null
+++ b/integration/size-test/material/list.ts
@@ -0,0 +1,28 @@
+import {Component, NgModule} from '@angular/core';
+import {platformBrowser} from '@angular/platform-browser';
+import {MatListModule} from '@angular/material/list';
+
+/**
+ * Basic component using `MatNavList` and `MatListItem`. Other parts of the list
+ * module such as `MatList`, `MatSelectionList` or `MatListOption` are not used
+ * and should be tree-shaken away.
+ */
+@Component({
+ template: `
+
+
+ hello
+
+
+ `,
+})
+export class TestComponent {}
+
+@NgModule({
+ imports: [MatListModule],
+ declarations: [TestComponent],
+ bootstrap: [TestComponent],
+})
+export class AppModule {}
+
+platformBrowser().bootstrapModule(AppModule);
diff --git a/integration/size-test/rollup.config.js b/integration/size-test/rollup.config.js
new file mode 100644
index 000000000000..33366cdd2672
--- /dev/null
+++ b/integration/size-test/rollup.config.js
@@ -0,0 +1,31 @@
+const {buildOptimizer} = require('@angular-devkit/build-optimizer');
+const node = require('rollup-plugin-node-resolve');
+
+const buildOptimizerPlugin = {
+ name: 'build-optimizer',
+ transform: (content, id) => {
+ const {content: code, sourceMap: map} = buildOptimizer({
+ content,
+ inputFilePath: id,
+ emitSourceMap: true,
+ isSideEffectFree: true,
+ isAngularCoreFile: false,
+ });
+ if (!code) {
+ return null;
+ }
+ if (!map) {
+ throw new Error('No sourcemap produced by build optimizer');
+ }
+ return {code, map};
+ },
+};
+
+module.exports = {
+ plugins: [
+ buildOptimizerPlugin,
+ node({
+ mainFields: ['es2015_ivy_ngcc', 'module_ivy_ngcc','es2015', 'module'],
+ }),
+ ],
+};
diff --git a/integration/size-test/terser-config.json b/integration/size-test/terser-config.json
new file mode 100644
index 000000000000..8521b30d51a9
--- /dev/null
+++ b/integration/size-test/terser-config.json
@@ -0,0 +1,16 @@
+{
+ "output": {
+ "ecma": "es2015",
+ "comments": false
+ },
+ "compress": {
+ "global_defs": {
+ "ngDevMode": false,
+ "ngI18nClosureMode": false,
+ "ngJitMode": false
+ },
+ "passes": 3,
+ "pure_getters": true
+ },
+ "mangle": true
+}
diff --git a/integration/size-test/tsconfig.json b/integration/size-test/tsconfig.json
new file mode 100644
index 000000000000..bbd7a5761215
--- /dev/null
+++ b/integration/size-test/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "compilerOptions": {
+ "experimentalDecorators": true,
+ "lib": ["es2015"],
+ "types": ["node"]
+ }
+}
diff --git a/integration/ts-compat/BUILD.bazel b/integration/ts-compat/BUILD.bazel
index 30d878e8efa9..8e455b12645b 100644
--- a/integration/ts-compat/BUILD.bazel
+++ b/integration/ts-compat/BUILD.bazel
@@ -40,7 +40,9 @@ typescript_version_packages = [
],
entry_point = "test.js",
tags = [
- "integration",
+ # These tests run in view engine only as the release packages are
+ # built with View Engine and we want to reproduce this here.
+ "view-engine-only",
],
)
for ts_pkg_name in typescript_version_packages
diff --git a/package.json b/package.json
index c52fdd186bbf..b339a704d319 100644
--- a/package.json
+++ b/package.json
@@ -44,7 +44,9 @@
"ts-circular-deps:check": "yarn -s ts-circular-deps check --config ./src/circular-deps-test.conf.js",
"ts-circular-deps:approve": "yarn -s ts-circular-deps approve --config ./src/circular-deps-test.conf.js",
"merge": "ng-dev pr merge",
- "approve-api": "node ./scripts/approve-api-golden.js"
+ "approve-api": "node ./scripts/approve-api-golden.js",
+ "integration-tests": "bazel test //integration/... --test_tag_filters=-view-engine-only --build_tests_only",
+ "integration-tests:view-engine": "bazel test //integration/... --test_tag_filters=view-engine-only --build_tests_only --config=view-engine"
},
"version": "10.0.0-rc.1",
"dependencies": {
@@ -79,11 +81,12 @@
"@bazel/bazelisk": "^1.4.0",
"@bazel/buildifier": "^2.2.1",
"@bazel/ibazel": "^0.13.0",
- "@bazel/jasmine": "^1.6.0",
- "@bazel/karma": "^1.6.0",
- "@bazel/protractor": "^1.6.0",
- "@bazel/terser": "^1.4.1",
- "@bazel/typescript": "^1.6.0",
+ "@bazel/jasmine": "^1.7.0",
+ "@bazel/karma": "^1.7.0",
+ "@bazel/protractor": "^1.7.0",
+ "@bazel/rollup": "^1.7.0",
+ "@bazel/terser": "^1.7.0",
+ "@bazel/typescript": "^1.7.0",
"@firebase/app-types": "^0.3.2",
"@octokit/rest": "16.28.7",
"@schematics/angular": "^10.0.0-rc.2",
diff --git a/src/material/list/list.ts b/src/material/list/list.ts
index 661640e8d7be..27ac6af0f6f2 100644
--- a/src/material/list/list.ts
+++ b/src/material/list/list.ts
@@ -22,6 +22,8 @@ import {
OnDestroy,
ChangeDetectorRef,
Input,
+ InjectionToken,
+ Inject,
} from '@angular/core';
import {
CanDisable,
@@ -48,6 +50,20 @@ class MatListItemBase {}
const _MatListItemMixinBase: CanDisableRippleCtor & typeof MatListItemBase =
mixinDisableRipple(MatListItemBase);
+/**
+ * Injection token that can be used to inject instances of `MatList`. It serves as
+ * alternative token to the actual `MatList` class which could cause unnecessary
+ * retention of the class and its component metadata.
+ */
+export const MAT_LIST = new InjectionToken('MatList');
+
+/**
+ * Injection token that can be used to inject instances of `MatNavList`. It serves as
+ * alternative token to the actual `MatNavList` class which could cause unnecessary
+ * retention of the class and its component metadata.
+ */
+export const MAT_NAV_LIST = new InjectionToken('MatNavList');
+
@Component({
selector: 'mat-nav-list',
exportAs: 'matNavList',
@@ -60,6 +76,7 @@ const _MatListItemMixinBase: CanDisableRippleCtor & typeof MatListItemBase =
inputs: ['disableRipple', 'disabled'],
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
+ providers: [{provide: MAT_NAV_LIST, useExisting: MatNavList}],
})
export class MatNavList extends _MatListMixinBase implements CanDisable, CanDisableRipple,
OnChanges, OnDestroy {
@@ -89,6 +106,7 @@ export class MatNavList extends _MatListMixinBase implements CanDisable, CanDisa
inputs: ['disableRipple', 'disabled'],
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
+ providers: [{provide: MAT_LIST, useExisting: MatList}],
})
export class MatList extends _MatListMixinBase implements CanDisable, CanDisableRipple, OnChanges,
OnDestroy {
@@ -187,8 +205,8 @@ export class MatListItem extends _MatListItemMixinBase implements AfterContentIn
constructor(private _element: ElementRef,
_changeDetectorRef: ChangeDetectorRef,
- @Optional() navList?: MatNavList,
- @Optional() list?: MatList) {
+ @Optional() @Inject(MAT_NAV_LIST) navList?: MatNavList,
+ @Optional() @Inject(MAT_LIST) list?: MatList) {
super();
this._isInteractiveList = !!(navList || (list && list._getListType() === 'action-list'));
this._list = navList || list;
diff --git a/tools/public_api_guard/material/list.d.ts b/tools/public_api_guard/material/list.d.ts
index 9e25cd7309a9..14c317e3ab1d 100644
--- a/tools/public_api_guard/material/list.d.ts
+++ b/tools/public_api_guard/material/list.d.ts
@@ -1,3 +1,7 @@
+export declare const MAT_LIST: InjectionToken;
+
+export declare const MAT_NAV_LIST: InjectionToken;
+
export declare const MAT_SELECTION_LIST_VALUE_ACCESSOR: any;
export declare class MatList extends _MatListMixinBase implements CanDisable, CanDisableRipple, OnChanges, OnDestroy {
diff --git a/yarn.lock b/yarn.lock
index bd7620b13fbd..2395c26ad660 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -400,37 +400,42 @@
resolved "https://registry.yarnpkg.com/@bazel/ibazel/-/ibazel-0.13.0.tgz#2307bdf9ba0ad9835ee603b8fb521822bf3b8a11"
integrity sha512-SFyS6oiibp8KW9BgAU+Tk0DhrH/sJx9om/OlB/Mg4hDEn8F8dj2QRqJE6CVhBCtLDcZDeD7WIAZKQDxKRYIShw==
-"@bazel/jasmine@^1.6.0":
- version "1.6.0"
- resolved "https://registry.yarnpkg.com/@bazel/jasmine/-/jasmine-1.6.0.tgz#c469ab8725d9a2e48c0c3c965861ff8add9272ac"
- integrity sha512-WtOQDtIMHKTxlp0+FcdrADV6LMrpJV7eEGZippSNFPL5YhwwrPfCSOs5WkooatsrjL5YEszswzqQXFjvC7EZKQ==
+"@bazel/jasmine@^1.7.0":
+ version "1.7.0"
+ resolved "https://registry.yarnpkg.com/@bazel/jasmine/-/jasmine-1.7.0.tgz#429df76e6628aa139176340434729cc091e371d7"
+ integrity sha512-LXq6nfBBEczjsDLwFW9kesGdewRrnFiAOZzXAAivCV3xtq516xK4QnVWA9tQGq+R1DnY50IaODpCJhh8PDezdg==
dependencies:
jasmine "~3.5.0"
jasmine-core "~3.5.0"
jasmine-reporters "~2.3.2"
v8-coverage "1.0.9"
-"@bazel/karma@^1.6.0":
- version "1.6.0"
- resolved "https://registry.yarnpkg.com/@bazel/karma/-/karma-1.6.0.tgz#98950b71114dd9ec169e6778a35d31ae1f578655"
- integrity sha512-9cX0E1SiMWwA70ZMFnMzeqSRn3biduGx03bGV77FSUYKocZpyfU2cOEygYGfxAqHnyM7x4cS8nflRv3+ZE0Aqg==
+"@bazel/karma@^1.7.0":
+ version "1.7.0"
+ resolved "https://registry.yarnpkg.com/@bazel/karma/-/karma-1.7.0.tgz#ec7e97a2629f5af0b2abe9a99ae30363a34af97d"
+ integrity sha512-mGYVD9DldB3v/DjxJpS39X1vUD6M32Al96DMoilwW3TSAazcRWwUAC6HY9z5Wtyeqwxyk8BY1Mg1/berWpoTxg==
dependencies:
tmp "0.1.0"
-"@bazel/protractor@^1.6.0":
- version "1.6.0"
- resolved "https://registry.yarnpkg.com/@bazel/protractor/-/protractor-1.6.0.tgz#cf095a1dbc038def7031c513a3b87f4e79bedb00"
- integrity sha512-gPiRv0oUJbVPpQ9nrwe5vjkffAc8VsYJhpTGgG+8aPdOaTLWgmBP/sy4BdfijU9O1Z/mNYojQCZgMzQz6kAvdg==
+"@bazel/protractor@^1.7.0":
+ version "1.7.0"
+ resolved "https://registry.yarnpkg.com/@bazel/protractor/-/protractor-1.7.0.tgz#1ced325a64d77bccca4bf881e62982d017d6b639"
+ integrity sha512-sLbejWwmwTupCS3JKdBeiZMUbylLpJxJdlrz8sZ9t4KV6YiFAXNOloCScrrdOkeiJz5QQZRG3p3rqHbIszUAwQ==
+
+"@bazel/rollup@^1.7.0":
+ version "1.7.0"
+ resolved "https://registry.yarnpkg.com/@bazel/rollup/-/rollup-1.7.0.tgz#5c0f0d51d2f3f14e78781a4b9e6a9ffba87f1579"
+ integrity sha512-Pp5aCJw3gwu77zn6/fQgZ39ArrWEI5O3dja5wKadBnfOQ66PImIEr+bf7JgROoWvACH1kGxaS423rq51fiuCsA==
-"@bazel/terser@^1.4.1":
+"@bazel/terser@^1.7.0":
version "1.7.0"
resolved "https://registry.yarnpkg.com/@bazel/terser/-/terser-1.7.0.tgz#c43e711e13b9a71c7abd3ade04fb4650d547ad01"
integrity sha512-u/UXk0WUinvkk1g5xxfqGieBz3r12Bj2y2m25lC5GjHBgCpGk7DyeGGi9H3QQNO1Wmpw51QSE9gaPzKzjUVGug==
-"@bazel/typescript@^1.6.0":
- version "1.6.0"
- resolved "https://registry.yarnpkg.com/@bazel/typescript/-/typescript-1.6.0.tgz#8dfd29e71bcf917d5f9cb67f19ac4dcfc9082439"
- integrity sha512-vAKuwy1Hgl+t3M3sH/G0oqHRYN35TdENj+0lsCI3x7EbSzyI6cbA3YQrLrlyvdScksqOpZa3PZ3UBGqfJJq2DA==
+"@bazel/typescript@^1.7.0":
+ version "1.7.0"
+ resolved "https://registry.yarnpkg.com/@bazel/typescript/-/typescript-1.7.0.tgz#8dc02b8a161f4fff3285186066b5f73666793452"
+ integrity sha512-M6JPXJZ+W6457QZfPHmGg/Mejnp7//YTnffGmnmeK9vDqybXeCCRWW1/iEOwopLJYQViBHfaoulde0VXelx9sA==
dependencies:
protobufjs "6.8.8"
semver "5.6.0"