diff --git a/.circleci/config.yml b/.circleci/config.yml index 461504d1b1..cb0ff18eaa 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -64,6 +64,12 @@ jobs: # See https://github.com/bazelbuild/bazel/issues/4257 - run: bazel query --output=label '//modules/... union //packages/... union //tools/...' | xargs bazel test --config=ci + # CircleCI will allow us to go back and view/download these artifacts from past builds. + # Also we can use a service like https://buildsize.org/ to automatically track binary size of these artifacts. + - store_artifacts: + path: dist/bin/packages/core/test/bundling/hello_world/bundle.min.js + destination: packages/core/test/bundling/hello_world/bundle.min.js + - save_cache: key: *cache_key paths: diff --git a/BUILD.bazel b/BUILD.bazel index f88b9e1a20..3ae5672cf0 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -32,6 +32,7 @@ filegroup( "reflect-metadata", "source-map-support", "minimist", + "tslib", ] for ext in [ "*.js", "*.json", diff --git a/WORKSPACE b/WORKSPACE index 2abf5f6cc8..c6034a4877 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -5,7 +5,7 @@ load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository") git_repository( name = "build_bazel_rules_nodejs", remote = "https://github.com/bazelbuild/rules_nodejs.git", - commit = "230d39a391226f51c03448f91eb61370e2e58c42", + commit = "5307b572d86a0764bd86a5681fc72cca016e9390", ) load("@build_bazel_rules_nodejs//:defs.bzl", "check_bazel_version", "node_repositories") @@ -56,3 +56,15 @@ http_archive( strip_prefix = "bazel-9755c72b48866ed034bd28aa033e9abd27431b1e", sha256 = "5b8443fc3481b5fcd9e7f348e1dd93c1397f78b223623c39eb56494c55f41962", ) + +# We have a source dependency on the Devkit repository, because it's built with +# Bazel. +# This allows us to edit sources and have the effect appear immediately without +# re-packaging or "npm link"ing. +# Even better, things like aspects will visit the entire graph including +# ts_library rules in the devkit repository. +git_repository( + name = "angular_devkit", + remote = "https://github.com/angular/devkit.git", + commit = "69fcdee61c5ff3f08aa609dec69155dfd29c809a", +) diff --git a/karma-js.conf.js b/karma-js.conf.js index 9088752d23..dcc82b06a7 100644 --- a/karma-js.conf.js +++ b/karma-js.conf.js @@ -60,6 +60,7 @@ module.exports = function(config) { 'dist/all/@angular/compiler-cli/**', 'dist/all/@angular/compiler/test/aot/**', 'dist/all/@angular/compiler/test/render3/**', + 'dist/all/@angular/core/test/bundling/**', 'dist/all/@angular/examples/**/e2e_test/*', 'dist/all/@angular/language-service/**', 'dist/all/@angular/router/test/**', diff --git a/package.json b/package.json index 06384f581d..855591fb4f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,5 @@ { "name": "angular-srcs", - "version": "TODO this should be 6.0.0-beta.0, workaround", "version": "6.0.0-beta.2", "private": true, "branchPattern": "2.0.*", diff --git a/packages/bazel/src/BUILD.bazel b/packages/bazel/src/BUILD.bazel index 00301d3053..db5ac06e45 100644 --- a/packages/bazel/src/BUILD.bazel +++ b/packages/bazel/src/BUILD.bazel @@ -1 +1,19 @@ -# Empty marker file, indicating this directory is a Bazel package. +load("@build_bazel_rules_nodejs//:defs.bzl", "nodejs_binary") + +nodejs_binary( + name = "rollup_with_build_optimizer", + data = ["@angular_devkit//packages/angular_devkit/build_optimizer:lib"], + # Since our rule extends the one in rules_nodejs, we use the same runtime + # dependency @build_bazel_rules_nodejs_rollup_deps. We don't need any + # additional npm dependencies when we run rollup or uglify. + entry_point = "build_bazel_rules_nodejs_rollup_deps/node_modules/rollup/bin/rollup", + node_modules = "@build_bazel_rules_nodejs_rollup_deps//:node_modules", + visibility = ["//visibility:public"], +) + +nodejs_binary( + name = "modify_tsconfig", + data = ["modify_tsconfig.js"], + entry_point = "angular/packages/bazel/src/modify_tsconfig.js", + visibility = ["//visibility:public"], +) diff --git a/packages/bazel/src/esm5.bzl b/packages/bazel/src/esm5.bzl new file mode 100644 index 0000000000..514ed7e357 --- /dev/null +++ b/packages/bazel/src/esm5.bzl @@ -0,0 +1,127 @@ +# 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 + +"""Provides ES5 syntax with ESModule import/exports. + +This exposes another flavor of output JavaScript, which is ES5 syntax +with ES2015 module syntax (import/export). +All Bazel rules should consume the standard dev or prod mode. +However we need to publish this flavor on NPM, so it's necessary to be able +to produce it. +""" + +# The provider downstream rules use to access the outputs +ESM5Info = provider( + doc = "Typescript compilation outputs in ES5 syntax with ES Modules", + fields = { + "transitive_output": """Dict of [rootDir, .js depset] entries. + + The value is a depset of the .js output files. + The key is the prefix that should be stripped off the files + when resolving modules, eg. for file + bazel-bin/[external/wkspc/]path/to/package/label.esm5/path/to/package/file.js + the rootdir would be + bazel-bin/[external/wkspc/]path/to/package/label.esm5""", + }, +) + +def _map_closure_path(file): + result = file.short_path[:-len(".closure.js")] + # short_path is meant to be used when accessing runfiles in a binary, where + # the CWD is inside the current repo. Therefore files in external repo have a + # short_path of ../external/wkspc/path/to/package + # We want to strip the first two segments from such paths. + if (result.startswith("../")): + result = "/".join(result.split("/")[2:]) + return result + ".js" + +def _join(array): + return "/".join([p for p in array if p]) + +def _esm5_outputs_aspect(target, ctx): + if not hasattr(target, "typescript"): + return [] + + # We create a new tsconfig.json file that will have our compilation settings + tsconfig = ctx.actions.declare_file("%s_esm5.tsconfig.json" % target.label.name) + + workspace = target.label.workspace_root if target.label.workspace_root else "" + + # re-root the outputs under a ".esm5" directory so the path don't collide + out_dir = ctx.label.name + ".esm5" + if workspace: + out_dir = out_dir + "/" + workspace + + outputs = [ctx.actions.declare_file(_join([out_dir, _map_closure_path(f)])) + for f in target.typescript.replay_params.outputs + if not f.short_path.endswith(".externs.js")] + + ctx.actions.run( + executable = ctx.executable._modify_tsconfig, + inputs = [target.typescript.replay_params.tsconfig], + outputs = [tsconfig], + arguments = [ + target.typescript.replay_params.tsconfig.path, + tsconfig.path, + _join([workspace, target.label.package, ctx.label.name + ".esm5"]), + ctx.bin_dir.path + ], + ) + + ctx.action( + progress_message = "Compiling TypeScript (ES5 with ES Modules) %s" % target.label, + inputs = target.typescript.replay_params.inputs + [tsconfig], + outputs = outputs, + arguments = [tsconfig.path], + executable = target.typescript.replay_params.compiler, + execution_requirements = { + "supports-workers": "0", + }, + ) + + root_dir = _join([ + ctx.bin_dir.path, + workspace, + target.label.package, + ctx.label.name + ".esm5", + ]) + + transitive_output={root_dir: depset(outputs)} + for dep in ctx.rule.attr.deps: + if ESM5Info in dep: + transitive_output.update(dep[ESM5Info].transitive_output) + + return [ESM5Info( + transitive_output = transitive_output, + )] + +# Downstream rules can use this aspect to access the ESM5 output flavor. +# Only terminal rules (those which expect never to be used in deps[]) should do +# this. +esm5_outputs_aspect = aspect( + implementation = _esm5_outputs_aspect, + # Recurse to the deps of any target we visit + attr_aspects = ['deps'], + attrs = { + "_modify_tsconfig": attr.label( + default = Label("//packages/bazel/src:modify_tsconfig"), + executable = True, + cfg = "host"), + # We must list tsc_wrapped here to ensure it's built before the action runs + # For some reason, having the compiler output as an input to the action above + # is not sufficient. + "_tsc_wrapped": attr.label( + default = Label("@build_bazel_rules_typescript//internal/tsc_wrapped:tsc_wrapped_bin"), + executable = True, + cfg = "host", + ), + # Same comment as for tsc_wrapped above. + "_ngc_wrapped": attr.label( + default = Label("//packages/bazel/src/ngc-wrapped"), + executable = True, + cfg = "host", + ), + }, +) diff --git a/packages/bazel/src/modify_tsconfig.js b/packages/bazel/src/modify_tsconfig.js new file mode 100644 index 0000000000..37ff67c06f --- /dev/null +++ b/packages/bazel/src/modify_tsconfig.js @@ -0,0 +1,38 @@ +/** + * @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 + */ + +/** + * @fileoverview Read a tsconfig.json file intended to produce production mode + * JS output, modify it to produce esm5 output instead, and write the result + * to disk. + */ +const fs = require('fs'); +const path = require('path'); + +function main(args) { + if (args.length < 3) { + console.error('Usage: $0 input.tsconfig.json output.tsconfig.json newRoot binDir'); + } + [input, output, newRoot, binDir] = args; + + const data = JSON.parse(fs.readFileSync(input, {encoding: 'utf-8'})); + data['compilerOptions']['target'] = 'es5'; + data['bazelOptions']['es5Mode'] = true; + data['bazelOptions']['tsickle'] = false; + data['compilerOptions']['outDir'] = path.join(data['compilerOptions']['outDir'], newRoot); + if (data['angularCompilerOptions']) { + data['angularCompilerOptions']['expectedOut'] = + data['angularCompilerOptions']['expectedOut'].map( + f => f.replace(/\.closure\.js$/, '.js').replace(binDir, path.join(binDir, newRoot))); + } + fs.writeFileSync(output, JSON.stringify(data)); +} + +if (require.main === module) { + process.exitCode = main(process.argv.slice(2)); +} diff --git a/packages/bazel/src/ng_module.bzl b/packages/bazel/src/ng_module.bzl index 71e74a3c0b..2e0aca1e74 100644 --- a/packages/bazel/src/ng_module.bzl +++ b/packages/bazel/src/ng_module.bzl @@ -180,6 +180,7 @@ def ngc_compile_action(ctx, label, inputs, outputs, messages_out, tsconfig_file, tsconfig = tsconfig_file, inputs = inputs, outputs = outputs, + compiler = ctx.executable.compiler, ) return None diff --git a/packages/bazel/src/ng_rollup_bundle.bzl b/packages/bazel/src/ng_rollup_bundle.bzl new file mode 100644 index 0000000000..9900e46794 --- /dev/null +++ b/packages/bazel/src/ng_rollup_bundle.bzl @@ -0,0 +1,70 @@ +# 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 + +"""This provides a variant of rollup_bundle that works better for Angular apps. + + It registers @angular-devkit/build-optimizer as a rollup plugin, to get + better optimization. It also uses ESM5 format inputs, as this is what + build-optimizer is hard-coded to look for and transform. +""" + +load("@build_bazel_rules_nodejs//internal/rollup:rollup_bundle.bzl", + "rollup_module_mappings_aspect", + "ROLLUP_ATTRS", + "ROLLUP_OUTPUTS", + "write_rollup_config", + "run_rollup", + "run_uglify") +load("@build_bazel_rules_nodejs//internal:collect_es6_sources.bzl", collect_es2015_sources = "collect_es6_sources") +load(":esm5.bzl", "esm5_outputs_aspect", "ESM5Info") + +PACKAGES=["core", "common"] +PLUGIN_CONFIG="{sideEffectFreeModules: [\n%s]}" % ",\n".join( + [" 'packages/{0}/{0}.esm5'".format(p) for p in PACKAGES]) +BO_ROLLUP="angular_devkit/packages/angular_devkit/build_optimizer/src/build-optimizer/rollup-plugin.js" +BO_PLUGIN="require('%s').default(%s)" % (BO_ROLLUP, PLUGIN_CONFIG) + +def _ng_rollup_bundle(ctx): + # We don't expect anyone to make use of this bundle yet, but it makes this rule + # compatible with rollup_bundle which allows them to be easily swapped back and + # forth. + esm2015_rollup_config = write_rollup_config(ctx, filename = "_%s.rollup_es6.conf.js") + run_rollup(ctx, collect_es2015_sources(ctx), esm2015_rollup_config, ctx.outputs.build_es6) + + esm5_sources = [] + root_dirs = [] + + for dep in ctx.attr.deps: + if ESM5Info in dep: + # TODO(alexeagle): we could make the module resolution in the rollup plugin + # faster if we kept the files grouped with their root dir. This approach just + # passes in both lists and requires multiple lookups (with expensive exception + # handling) to locate the files again. + transitive_output = dep[ESM5Info].transitive_output + root_dirs.extend(transitive_output.keys()) + esm5_sources.extend(transitive_output.values()) + + rollup_config = write_rollup_config(ctx, [BO_PLUGIN], root_dirs) + run_rollup(ctx, depset(transitive = esm5_sources).to_list(), rollup_config, ctx.outputs.build_es5) + + run_uglify(ctx, ctx.outputs.build_es5, ctx.outputs.build_es5_min) + run_uglify(ctx, ctx.outputs.build_es5, ctx.outputs.build_es5_min_debug, debug = True) + + return DefaultInfo(files=depset([ctx.outputs.build_es5_min])) + +ng_rollup_bundle = rule( + implementation = _ng_rollup_bundle, + attrs = dict(ROLLUP_ATTRS, **{ + "deps": attr.label_list(aspects = [ + rollup_module_mappings_aspect, + esm5_outputs_aspect, + ]), + "_rollup": attr.label( + executable = True, + cfg="host", + default = Label("@angular//packages/bazel/src:rollup_with_build_optimizer")), + }), + outputs = ROLLUP_OUTPUTS, +) \ No newline at end of file diff --git a/packages/core/test/bundling/hello_world/BUILD.bazel b/packages/core/test/bundling/hello_world/BUILD.bazel new file mode 100644 index 0000000000..08a739f3fc --- /dev/null +++ b/packages/core/test/bundling/hello_world/BUILD.bazel @@ -0,0 +1,45 @@ +package(default_visibility = ["//visibility:public"]) + +load("//tools:defaults.bzl", "ts_library") +load("//packages/bazel/src:ng_rollup_bundle.bzl", "ng_rollup_bundle") +load("@build_bazel_rules_nodejs//:defs.bzl", "jasmine_node_test") + +ts_library( + name = "hello_world", + srcs = ["index.ts"], + deps = [ + "//packages/core", + ], +) + +ng_rollup_bundle( + name = "bundle", + # TODO(alexeagle): This is inconsistent. + # We try to teach users to always have their workspace at the start of a + # path, to disambiguate from other workspaces. + # Here, the rule implementation is looking in an execroot where the layout + # has an "external" directory for external dependencies. + # This should probably start with "angular/" and let the rule deal with it. + entry_point = "packages/core/test/bundling/hello_world/index.js", + deps = [ + ":hello_world", + "//packages/core", + ], +) + +ts_library( + name = "test_lib", + testonly = 1, + srcs = ["domino_typings.d.ts"] + glob(["*_spec.ts"]), + deps = ["//packages:types"], +) + +jasmine_node_test( + name = "test", + data = [ + ":bundle", + ":bundle.js", + ":bundle.min_debug.js", + ], + deps = [":test_lib"], +) diff --git a/packages/core/test/bundling/hello_world/domino_typings.d.ts b/packages/core/test/bundling/hello_world/domino_typings.d.ts new file mode 100644 index 0000000000..ea774b9c78 --- /dev/null +++ b/packages/core/test/bundling/hello_world/domino_typings.d.ts @@ -0,0 +1,12 @@ +/** + * @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 + */ + +declare module 'domino' { + function createWindow(html: string, url: string): Window; + const impl: {Element: any}; +} diff --git a/packages/core/test/bundling/hello_world/index.ts b/packages/core/test/bundling/hello_world/index.ts new file mode 100644 index 0000000000..d18f10483a --- /dev/null +++ b/packages/core/test/bundling/hello_world/index.ts @@ -0,0 +1,27 @@ +/** + * @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 {ɵT as T, ɵb1 as b1, ɵdefineComponent as defineComponent, ɵrenderComponent as renderComponent, ɵt as t} from '@angular/core'; + +class HelloWorld { + name = 'World'; + + static ngComponentDef = defineComponent({ + type: HelloWorld, + tag: 'hello-world', + factory: () => new HelloWorld(), + template: function HelloWorldTemplate(ctx: HelloWorld, cm: boolean) { + if (cm) { + T(0); + } + t(0, b1('Hello ', ctx.name, '!')); + } + }); +} + +renderComponent(HelloWorld); diff --git a/packages/core/test/bundling/hello_world/treeshaking_spec.ts b/packages/core/test/bundling/hello_world/treeshaking_spec.ts new file mode 100644 index 0000000000..2d46617eb5 --- /dev/null +++ b/packages/core/test/bundling/hello_world/treeshaking_spec.ts @@ -0,0 +1,72 @@ +/** + * @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 * as fs from 'fs'; +import * as path from 'path'; + +const UTF8 = { + encoding: 'utf-8' +}; +const PACKAGE = 'angular/packages/core/test/bundling/hello_world'; + +import * as domino from 'domino'; + +describe('treeshaking with uglify', () => { + let content: string; + beforeAll(() => { + content = fs.readFileSync( + path.join(process.env['TEST_SRCDIR'], PACKAGE, 'bundle.min_debug.js'), UTF8); + }); + + it('should drop unused TypeScript helpers', + () => { expect(content).not.toContain('__asyncGenerator'); }); + + it('should not contain rxjs from commonjs distro', () => { + expect(content).not.toContain('commonjsGlobal'); + expect(content).not.toContain('createCommonjsModule'); + }); + + it('should not contain zone.js', () => { expect(content).not.toContain('scheduleMicroTask'); }); + + describe('functional test in domino', () => { + let document: Document; + + beforeEach(() => { + const window = domino.createWindow('', 'http://localhost'); + (global as any).document = document = window.document; + // Trick to avoid Event patching from + // https://github.com/angular/angular/blob/7cf5e95ac9f0f2648beebf0d5bd9056b79946970/packages/platform-browser/src/dom/events/dom_events.ts#L112-L132 + // It fails with Domino with TypeError: Cannot assign to read only property + // 'stopImmediatePropagation' of object '#' + (global as any).Event = null; + + document.body.innerHTML = ''; + }); + + afterEach(() => { + (global as any).document = undefined; + (global as any).Element = undefined; + }); + + + it('should render hello world when not minified', () => { + require(path.join(PACKAGE, 'bundle.js')); + expect(document.body.textContent).toEqual('Hello World!'); + }); + + it('should render hello world when debug minified', () => { + require(path.join(PACKAGE, 'bundle.min_debug.js')); + expect(document.body.textContent).toEqual('Hello World!'); + }); + + it('should render hello world when fully minified', () => { + require(path.join(PACKAGE, 'bundle.min.js')); + expect(document.body.textContent).toEqual('Hello World!'); + }); + }); +}); \ No newline at end of file diff --git a/tools/bazel.rc b/tools/bazel.rc index edb146aa0c..9337efddaf 100644 --- a/tools/bazel.rc +++ b/tools/bazel.rc @@ -14,13 +14,9 @@ test:debug --test_arg=--node_options=--inspect-brk --test_output=streamed --test # Filesystem interactions # ############################### -# Don't create bazel-* symlinks in the WORKSPACE directory. -# These require .gitignore and may scare users. -# Also, it's a workaround for https://github.com/bazelbuild/rules_typescript/issues/12 -# which affects the common case of having `tsconfig.json` in the WORKSPACE directory. -# -# Instead, you should run `bazel info bazel-bin` to find out where the outputs went. -build --symlink_prefix=/ +# Put bazel's symlinks under dist, so results go to dist/bin +# There is still a `bazel-out` symlink created in the project root. +build --symlink_prefix=dist/ # Performance: avoid stat'ing input files build --watchfs @@ -50,3 +46,6 @@ build:ci --noshow_progress # Don't run manual tests on CI test:ci --test_tag_filters=-manual + +# Retry in the event of flakes, eg. https://circleci.com/gh/angular/angular/31309 +test:ci --flaky_test_attempts=2 \ No newline at end of file diff --git a/tools/cjs-jasmine/index.ts b/tools/cjs-jasmine/index.ts index a054257769..94662573c6 100644 --- a/tools/cjs-jasmine/index.ts +++ b/tools/cjs-jasmine/index.ts @@ -59,6 +59,7 @@ var specFiles: any = '@angular/examples/**', '@angular/platform-browser/**', '@angular/platform-browser-dynamic/**', + '@angular/core/test/bundling/**', '@angular/core/test/zone/**', '@angular/core/test/render3/**', '@angular/core/test/fake_async_spec.*',