build(ivy): create hello world rollup (#22004)

This is a customization of the rollup_bundle rule from rules_nodejs
which adds the build-optimizer as a plugin.

Add a functional test with fast round-trip that asserts the minified app
still works.

Publish the min.js artifact on circleCI so we can track its size.

PR Close #22004
This commit is contained in:
Alex Eagle 2018-02-02 15:25:33 -08:00 committed by Alex Rickabaugh
parent 2707012181
commit 370ab66c4f
16 changed files with 439 additions and 10 deletions

View File

@ -64,6 +64,12 @@ jobs:
# See https://github.com/bazelbuild/bazel/issues/4257 # See https://github.com/bazelbuild/bazel/issues/4257
- run: bazel query --output=label '//modules/... union //packages/... union //tools/...' | xargs bazel test --config=ci - 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: - save_cache:
key: *cache_key key: *cache_key
paths: paths:

View File

@ -32,6 +32,7 @@ filegroup(
"reflect-metadata", "reflect-metadata",
"source-map-support", "source-map-support",
"minimist", "minimist",
"tslib",
] for ext in [ ] for ext in [
"*.js", "*.js",
"*.json", "*.json",

View File

@ -5,7 +5,7 @@ load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository")
git_repository( git_repository(
name = "build_bazel_rules_nodejs", name = "build_bazel_rules_nodejs",
remote = "https://github.com/bazelbuild/rules_nodejs.git", remote = "https://github.com/bazelbuild/rules_nodejs.git",
commit = "230d39a391226f51c03448f91eb61370e2e58c42", commit = "5307b572d86a0764bd86a5681fc72cca016e9390",
) )
load("@build_bazel_rules_nodejs//:defs.bzl", "check_bazel_version", "node_repositories") load("@build_bazel_rules_nodejs//:defs.bzl", "check_bazel_version", "node_repositories")
@ -56,3 +56,15 @@ http_archive(
strip_prefix = "bazel-9755c72b48866ed034bd28aa033e9abd27431b1e", strip_prefix = "bazel-9755c72b48866ed034bd28aa033e9abd27431b1e",
sha256 = "5b8443fc3481b5fcd9e7f348e1dd93c1397f78b223623c39eb56494c55f41962", 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",
)

View File

@ -60,6 +60,7 @@ module.exports = function(config) {
'dist/all/@angular/compiler-cli/**', 'dist/all/@angular/compiler-cli/**',
'dist/all/@angular/compiler/test/aot/**', 'dist/all/@angular/compiler/test/aot/**',
'dist/all/@angular/compiler/test/render3/**', 'dist/all/@angular/compiler/test/render3/**',
'dist/all/@angular/core/test/bundling/**',
'dist/all/@angular/examples/**/e2e_test/*', 'dist/all/@angular/examples/**/e2e_test/*',
'dist/all/@angular/language-service/**', 'dist/all/@angular/language-service/**',
'dist/all/@angular/router/test/**', 'dist/all/@angular/router/test/**',

View File

@ -1,6 +1,5 @@
{ {
"name": "angular-srcs", "name": "angular-srcs",
"version": "TODO this should be 6.0.0-beta.0, workaround",
"version": "6.0.0-beta.2", "version": "6.0.0-beta.2",
"private": true, "private": true,
"branchPattern": "2.0.*", "branchPattern": "2.0.*",

View File

@ -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"],
)

127
packages/bazel/src/esm5.bzl Normal file
View File

@ -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",
),
},
)

View File

@ -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));
}

View File

@ -180,6 +180,7 @@ def ngc_compile_action(ctx, label, inputs, outputs, messages_out, tsconfig_file,
tsconfig = tsconfig_file, tsconfig = tsconfig_file,
inputs = inputs, inputs = inputs,
outputs = outputs, outputs = outputs,
compiler = ctx.executable.compiler,
) )
return None return None

View File

@ -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,
)

View File

@ -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"],
)

View File

@ -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};
}

View File

@ -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);

View File

@ -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 '#<Event>'
(global as any).Event = null;
document.body.innerHTML = '<hello-world></hello-world>';
});
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!');
});
});
});

View File

@ -14,13 +14,9 @@ test:debug --test_arg=--node_options=--inspect-brk --test_output=streamed --test
# Filesystem interactions # # Filesystem interactions #
############################### ###############################
# Don't create bazel-* symlinks in the WORKSPACE directory. # Put bazel's symlinks under dist, so results go to dist/bin
# These require .gitignore and may scare users. # There is still a `bazel-out` symlink created in the project root.
# Also, it's a workaround for https://github.com/bazelbuild/rules_typescript/issues/12 build --symlink_prefix=dist/
# 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=/
# Performance: avoid stat'ing input files # Performance: avoid stat'ing input files
build --watchfs build --watchfs
@ -50,3 +46,6 @@ build:ci --noshow_progress
# Don't run manual tests on CI # Don't run manual tests on CI
test:ci --test_tag_filters=-manual 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

View File

@ -59,6 +59,7 @@ var specFiles: any =
'@angular/examples/**', '@angular/examples/**',
'@angular/platform-browser/**', '@angular/platform-browser/**',
'@angular/platform-browser-dynamic/**', '@angular/platform-browser-dynamic/**',
'@angular/core/test/bundling/**',
'@angular/core/test/zone/**', '@angular/core/test/zone/**',
'@angular/core/test/render3/**', '@angular/core/test/render3/**',
'@angular/core/test/fake_async_spec.*', '@angular/core/test/fake_async_spec.*',