Skip to content

Commit

Permalink
build(ivy): create hello world rollup (#22004)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
alexeagle authored and alxhub committed Feb 6, 2018
1 parent 2707012 commit 370ab66
Show file tree
Hide file tree
Showing 16 changed files with 439 additions and 10 deletions.
6 changes: 6 additions & 0 deletions .circleci/config.yml
Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions BUILD.bazel
Expand Up @@ -32,6 +32,7 @@ filegroup(
"reflect-metadata",
"source-map-support",
"minimist",
"tslib",
] for ext in [
"*.js",
"*.json",
Expand Down
14 changes: 13 additions & 1 deletion WORKSPACE
Expand Up @@ -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")
Expand Down Expand Up @@ -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",
)
1 change: 1 addition & 0 deletions karma-js.conf.js
Expand Up @@ -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/**',
Expand Down
1 change: 0 additions & 1 deletion 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.*",
Expand Down
20 changes: 19 additions & 1 deletion 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"],
)
127 changes: 127 additions & 0 deletions 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",
),
},
)
38 changes: 38 additions & 0 deletions 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));
}
1 change: 1 addition & 0 deletions packages/bazel/src/ng_module.bzl
Expand Up @@ -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
Expand Down
70 changes: 70 additions & 0 deletions 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,
)
45 changes: 45 additions & 0 deletions 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"],
)
12 changes: 12 additions & 0 deletions 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};
}

0 comments on commit 370ab66

Please sign in to comment.