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"