From 47220997e1238b67c660644e597fcd34c7072e67 Mon Sep 17 00:00:00 2001 From: Alex Eagle Date: Mon, 21 Aug 2017 08:23:47 -0700 Subject: [PATCH] build: add bazel integration test (#18733) It includes sass compilation, and building the bazel package distribution. PR Close #18733 --- WORKSPACE | 2 +- build.sh | 31 ++- integration/bazel/BUILD.bazel | 10 + integration/bazel/WORKSPACE | 24 ++ integration/bazel/angular.tsconfig.json | 21 ++ integration/bazel/package.json | 26 +++ integration/bazel/src/BUILD.bazel | 11 + integration/bazel/src/app.module.ts | 9 + integration/bazel/src/hello-world/BUILD.bazel | 19 ++ .../hello-world/hello-world.component.scss | 12 + .../src/hello-world/hello-world.component.ts | 15 ++ .../src/hello-world/hello-world.module.ts | 8 + integration/bazel/src/shared/BUILD.bazel | 13 ++ integration/bazel/src/shared/_colors.scss | 2 + integration/bazel/src/shared/_fonts.scss | 2 + integration/bazel/src/tsconfig.json | 12 + packages/bazel/WORKSPACE | 2 +- packages/bazel/src/ng_module.bzl | 219 ++++++++++-------- packages/bazel/src/ngc-wrapped/BUILD.bazel | 5 + packages/bazel/src/ngc-wrapped/index.ts | 181 ++++++++++++++- packages/bazel/src/ngc-wrapped/tsconfig.json | 5 + packages/bazel/src/rules_typescript.bzl | 9 + packages/compiler-cli/src/transformers/api.ts | 5 - .../compiler-cli/src/transformers/program.ts | 24 +- scripts/ci/install.sh | 2 +- 25 files changed, 532 insertions(+), 137 deletions(-) create mode 100644 integration/bazel/BUILD.bazel create mode 100644 integration/bazel/WORKSPACE create mode 100644 integration/bazel/angular.tsconfig.json create mode 100644 integration/bazel/package.json create mode 100644 integration/bazel/src/BUILD.bazel create mode 100644 integration/bazel/src/app.module.ts create mode 100644 integration/bazel/src/hello-world/BUILD.bazel create mode 100644 integration/bazel/src/hello-world/hello-world.component.scss create mode 100644 integration/bazel/src/hello-world/hello-world.component.ts create mode 100644 integration/bazel/src/hello-world/hello-world.module.ts create mode 100644 integration/bazel/src/shared/BUILD.bazel create mode 100644 integration/bazel/src/shared/_colors.scss create mode 100644 integration/bazel/src/shared/_fonts.scss create mode 100644 integration/bazel/src/tsconfig.json create mode 100644 packages/bazel/src/ngc-wrapped/tsconfig.json create mode 100644 packages/bazel/src/rules_typescript.bzl diff --git a/WORKSPACE b/WORKSPACE index ba15d08389..57803226a9 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -3,7 +3,7 @@ load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository") git_repository( name = "build_bazel_rules_typescript", remote = "https://github.com/bazelbuild/rules_typescript.git", - tag = "0.0.5", + tag = "0.0.6", ) load("@build_bazel_rules_typescript//:defs.bzl", "node_repositories") diff --git a/build.sh b/build.sh index 293bcf729e..b587140875 100755 --- a/build.sh +++ b/build.sh @@ -327,6 +327,16 @@ mapSources() { fi } +updateVersionReferences() { + NPM_DIR="$1" + ( + echo "====== VERSION: Updating version references in ${NPM_DIR}" + cd ${NPM_DIR} + echo "====== EXECUTE: perl -p -i -e \"s/0\.0\.0\-PLACEHOLDER/${VERSION}/g\" $""(grep -ril 0\.0\.0\-PLACEHOLDER .)" + perl -p -i -e "s/0\.0\.0\-PLACEHOLDER/${VERSION}/g" $(grep -ril 0\.0\.0\-PLACEHOLDER .) < /dev/null 2> /dev/null + ) +} + VERSION="${VERSION_PREFIX}${VERSION_SUFFIX}" echo "====== BUILDING: Version ${VERSION}" @@ -419,11 +429,15 @@ if [[ ${BUILD_TOOLS} == true || ${BUILD_ALL} == true ]]; then $(npm bin)/tsc -p packages/tsc-wrapped/tsconfig-build.json cp ./packages/tsc-wrapped/package.json ./dist/packages-dist/tsc-wrapped cp ./packages/tsc-wrapped/README.md ./dist/packages-dist/tsc-wrapped - ( - cd dist/packages-dist/tsc-wrapped - echo "====== EXECUTE: perl -p -i -e \"s/0\.0\.0\-PLACEHOLDER/${VERSION}/g\" $""(grep -ril 0\.0\.0\-PLACEHOLDER .)" - perl -p -i -e "s/0\.0\.0\-PLACEHOLDER/${VERSION}/g" $(grep -ril 0\.0\.0\-PLACEHOLDER .) < /dev/null 2> /dev/null - ) + updateVersionReferences dist/packages-dist/tsc-wrapped + + rsync -a packages/bazel/ ./dist/packages-dist/bazel + # Re-write nodejs import paths + perl -p -i -e "s#__main__/packages/bazel#angular#g" $(grep -ril __main__ dist/packages-dist/bazel) < /dev/null 2> /dev/null + # Remove BEGIN-INTERNAL...END-INTERAL blocks + # https://stackoverflow.com/questions/24175271/how-can-i-match-multi-line-patterns-in-the-command-line-with-perl-style-regex + perl -0777 -n -i -e "s/(?m)^.*BEGIN-INTERNAL[\w\W]*END-INTERNAL.*\n//g; print" $(grep -ril BEGIN-INTERNAL dist/packages-dist/bazel) < /dev/null 2> /dev/null + updateVersionReferences dist/packages-dist/bazel fi for PACKAGE in ${PACKAGES[@]} @@ -489,12 +503,7 @@ do if [[ -d ${NPM_DIR} ]]; then - ( - echo "====== VERSION: Updating version references" - cd ${NPM_DIR} - echo "====== EXECUTE: perl -p -i -e \"s/0\.0\.0\-PLACEHOLDER/${VERSION}/g\" $""(grep -ril 0\.0\.0\-PLACEHOLDER .)" - perl -p -i -e "s/0\.0\.0\-PLACEHOLDER/${VERSION}/g" $(grep -ril 0\.0\.0\-PLACEHOLDER .) < /dev/null 2> /dev/null - ) + updateVersionReferences ${NPM_DIR} fi travisFoldEnd "build package: ${PACKAGE}" diff --git a/integration/bazel/BUILD.bazel b/integration/bazel/BUILD.bazel new file mode 100644 index 0000000000..629c7aae5a --- /dev/null +++ b/integration/bazel/BUILD.bazel @@ -0,0 +1,10 @@ +package(default_visibility = ["//visibility:public"]) + +filegroup( + name = "node_modules", + srcs = glob([ + "node_modules/**/*.js", + "node_modules/**/*.d.ts", + "node_modules/**/*.json", + ]) +) diff --git a/integration/bazel/WORKSPACE b/integration/bazel/WORKSPACE new file mode 100644 index 0000000000..57e7580e73 --- /dev/null +++ b/integration/bazel/WORKSPACE @@ -0,0 +1,24 @@ +load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository") + +git_repository( + name = "build_bazel_rules_typescript", + remote = "https://github.com/bazelbuild/rules_typescript.git", + tag = "0.0.6", +) +load("@build_bazel_rules_typescript//:defs.bzl", "node_repositories") +node_repositories(package_json = "//:package.json") + +local_repository( + name = "angular", + path = "node_modules/@angular/bazel" +) + +git_repository( + name = "io_bazel_rules_sass", + remote = "https://github.com/bazelbuild/rules_sass.git", + tag = "0.0.2", +) + +load("@io_bazel_rules_sass//sass:sass.bzl", "sass_repositories") + +sass_repositories() \ No newline at end of file diff --git a/integration/bazel/angular.tsconfig.json b/integration/bazel/angular.tsconfig.json new file mode 100644 index 0000000000..b080935ebf --- /dev/null +++ b/integration/bazel/angular.tsconfig.json @@ -0,0 +1,21 @@ +// WORKAROUND https://github.com/angular/angular/issues/18810 +// This file is required to run ngc on angular libraries, to write files like +// node_modules/@angular/core/core.ngsummary.json +{ + "compilerOptions": { + "lib": [ + "dom", + "es2015" + ], + "experimentalDecorators": true, + "types": [] + }, + "include": [ + "node_modules/@angular/**/*" + ], + "exclude": [ + "node_modules/@angular/bazel/**", + "node_modules/@angular/compiler-cli/**", + "node_modules/@angular/tsc-wrapped/**" + ] +} diff --git a/integration/bazel/package.json b/integration/bazel/package.json new file mode 100644 index 0000000000..716f073811 --- /dev/null +++ b/integration/bazel/package.json @@ -0,0 +1,26 @@ +{ + "name": "angular-bazel", + "description": "example and integration test for building Angular apps with Bazel", + "version": "0.0.0", + "license": "MIT", + "dependencies": { + "@angular/animations": "file:../../dist/packages-dist/animations", + "@angular/common": "file:../../dist/packages-dist/common", + "@angular/compiler": "file:../../dist/packages-dist/compiler", + "@angular/core": "file:../../dist/packages-dist/core", + "@angular/platform-browser": "file:../../dist/packages-dist/platform-browser", + "rxjs": "5.3.1", + "zone.js": "0.8.6" + }, + "devDependencies": { + "@angular/bazel": "file:../../dist/packages-dist/bazel", + "@angular/compiler-cli": "file:../../dist/packages-dist/compiler-cli", + "@types/node": "^7.0.18", + "protobufjs": "5.0.0", + "typescript": "~2.3.1" + }, + "scripts": { + "postinstall": "ngc -p angular.tsconfig.json", + "test": "bazel build ..." + } +} \ No newline at end of file diff --git a/integration/bazel/src/BUILD.bazel b/integration/bazel/src/BUILD.bazel new file mode 100644 index 0000000000..3451c3ba90 --- /dev/null +++ b/integration/bazel/src/BUILD.bazel @@ -0,0 +1,11 @@ +load("@angular//:index.bzl", "ng_module") + +# Allow targets under sub-packages to reference the tsconfig.json file +exports_files(["tsconfig.json"]) + +ng_module( + name = "app", + srcs = ["app.module.ts"], + deps = ["//src/hello-world"], + tsconfig = ":tsconfig.json", +) \ No newline at end of file diff --git a/integration/bazel/src/app.module.ts b/integration/bazel/src/app.module.ts new file mode 100644 index 0000000000..14e2760d75 --- /dev/null +++ b/integration/bazel/src/app.module.ts @@ -0,0 +1,9 @@ +import {HelloWorldModule} from './hello-world/hello-world.module'; + +import {NgModule} from '@angular/core'; +import {BrowserModule} from '@angular/platform-browser'; + +@NgModule({ + imports: [BrowserModule, HelloWorldModule] +}) +export class AppModule {} diff --git a/integration/bazel/src/hello-world/BUILD.bazel b/integration/bazel/src/hello-world/BUILD.bazel new file mode 100644 index 0000000000..e0cb4a41ab --- /dev/null +++ b/integration/bazel/src/hello-world/BUILD.bazel @@ -0,0 +1,19 @@ +package(default_visibility = ["//visibility:public"]) +load("@angular//:index.bzl", "ng_module") +load("@io_bazel_rules_sass//sass:sass.bzl", "sass_binary") + +sass_binary( + name = "styles", + src = "hello-world.component.scss", + deps = [ + "//src/shared:colors", + "//src/shared:fonts", + ], +) + +ng_module( + name = "hello-world", + srcs = glob(["*.ts"]), + tsconfig = "//src:tsconfig.json", + assets = [":styles"], +) diff --git a/integration/bazel/src/hello-world/hello-world.component.scss b/integration/bazel/src/hello-world/hello-world.component.scss new file mode 100644 index 0000000000..2db21ad42e --- /dev/null +++ b/integration/bazel/src/hello-world/hello-world.component.scss @@ -0,0 +1,12 @@ +@import "src/shared/fonts"; +@import "src/shared/colors"; + +html { + body { + font-family: $default-font-stack; + h1 { + font-family: $modern-font-stack; + color: $example-red; + } + } +} diff --git a/integration/bazel/src/hello-world/hello-world.component.ts b/integration/bazel/src/hello-world/hello-world.component.ts new file mode 100644 index 0000000000..75eb306a32 --- /dev/null +++ b/integration/bazel/src/hello-world/hello-world.component.ts @@ -0,0 +1,15 @@ + +import {Component, NgModule} from '@angular/core'; + +@Component({ + selector: 'hello-world-app', + template: ` +
Hello {{ name }}!
+ + `, + // TODO: might be better to point to .scss so this looks valid at design-time + styleUrls: ['./styles.css'] +}) +export class HelloWorldComponent { + name: string = 'world'; +} diff --git a/integration/bazel/src/hello-world/hello-world.module.ts b/integration/bazel/src/hello-world/hello-world.module.ts new file mode 100644 index 0000000000..ceb62649a4 --- /dev/null +++ b/integration/bazel/src/hello-world/hello-world.module.ts @@ -0,0 +1,8 @@ +import {HelloWorldComponent} from './hello-world.component'; +import {NgModule} from '@angular/core'; + +@NgModule({ + declarations: [HelloWorldComponent], + bootstrap: [HelloWorldComponent], +}) +export class HelloWorldModule {} diff --git a/integration/bazel/src/shared/BUILD.bazel b/integration/bazel/src/shared/BUILD.bazel new file mode 100644 index 0000000000..488025b1ab --- /dev/null +++ b/integration/bazel/src/shared/BUILD.bazel @@ -0,0 +1,13 @@ +package(default_visibility = ["//visibility:public"]) + +load("@io_bazel_rules_sass//sass:sass.bzl", "sass_library") + +sass_library( + name = "colors", + srcs = ["_colors.scss"], +) + +sass_library( + name = "fonts", + srcs = ["_fonts.scss"], +) diff --git a/integration/bazel/src/shared/_colors.scss b/integration/bazel/src/shared/_colors.scss new file mode 100644 index 0000000000..7584a9b844 --- /dev/null +++ b/integration/bazel/src/shared/_colors.scss @@ -0,0 +1,2 @@ +$example-blue: #0000ff; +$example-red: #ff0000; diff --git a/integration/bazel/src/shared/_fonts.scss b/integration/bazel/src/shared/_fonts.scss new file mode 100644 index 0000000000..d17c15dc1d --- /dev/null +++ b/integration/bazel/src/shared/_fonts.scss @@ -0,0 +1,2 @@ +$default-font-stack: Cambria, "Hoefler Text", Utopia, "Liberation Serif", "Nimbus Roman No9 L Regular", Times, "Times New Roman", serif; +$modern-font-stack: Constantia, "Lucida Bright", Lucidabright, "Lucida Serif", Lucida, "DejaVu Serif", "Bitstream Vera Serif", "Liberation Serif", Georgia, serif; diff --git a/integration/bazel/src/tsconfig.json b/integration/bazel/src/tsconfig.json new file mode 100644 index 0000000000..068cd10fcc --- /dev/null +++ b/integration/bazel/src/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "experimentalDecorators": true, + "lib": [ + "dom", + "es5", + "es2015.collection", + "es2015.iterable", + "es2015.promise" + ] + } +} \ No newline at end of file diff --git a/packages/bazel/WORKSPACE b/packages/bazel/WORKSPACE index c0c1418247..873e7584e7 100644 --- a/packages/bazel/WORKSPACE +++ b/packages/bazel/WORKSPACE @@ -13,7 +13,7 @@ load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository") git_repository( name = "build_bazel_rules_typescript", remote = "https://github.com/bazelbuild/rules_typescript.git", - tag = "0.0.5", + tag = "0.0.6", ) load("@build_bazel_rules_typescript//:defs.bzl", "node_repositories") diff --git a/packages/bazel/src/ng_module.bzl b/packages/bazel/src/ng_module.bzl index 16d66c3a74..13629865b3 100644 --- a/packages/bazel/src/ng_module.bzl +++ b/packages/bazel/src/ng_module.bzl @@ -3,72 +3,113 @@ # 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 -load("@build_bazel_rules_typescript//internal:build_defs.bzl", "tsc_wrapped_tsconfig") - -load( - "@build_bazel_rules_typescript//internal:common/compilation.bzl", - "COMMON_ATTRIBUTES", "compile_ts", "ts_providers_dict_to_struct" +load(":rules_typescript.bzl", + "tsc_wrapped_tsconfig", + "COMMON_ATTRIBUTES", + "compile_ts", + "DEPS_ASPECTS", + "ts_providers_dict_to_struct", + "json_marshal", ) -load("@build_bazel_rules_typescript//internal:common/json_marshal.bzl", "json_marshal") - # Calculate the expected output of the template compiler for every source in # in the library. Most of these will be produced as empty files but it is # unknown, without parsing, which will be empty. -def _expected_outs(ctx): - result = [] +def _expected_outs(ctx, label): + devmode_js_files = [] + closure_js_files = [] + declaration_files = [] + summary_files = [] + + codegen_inputs = ctx.files.srcs + + for src in ctx.files.srcs + ctx.files.assets: + if src.short_path.endswith(".ts") and not src.short_path.endswith(".d.ts"): + basename = src.short_path[len(ctx.label.package) + 1:-len(".ts")] + devmode_js = [ + ".ngfactory.js", + ".ngsummary.js", + ".js", + ] + summaries = [".ngsummary.json"] - for src in ctx.files.srcs: - if src.short_path.endswith(".ts"): - basename = src.short_path[len(ctx.label.package) + 1:-3] - result += [ctx.new_file(ctx.bin_dir, basename + ext) for ext in [ - ".ngfactory.js", - ".ngfactory.d.ts", - ".ngsummary.js", - ".ngsummary.d.ts", - ".ngsummary.json", - ]] elif src.short_path.endswith(".css"): - basename = src.short_path[len(ctx.label.package) + 1:-4] - result += [ctx.new_file(ctx.bin_dir, basename + ext) for ext in [ - ".css.shim.ngstyle.js", - ".css.shim.ngstyle.d.ts", - ".css.ngstyle.js", - ".css.ngstyle.d.ts", - ]] - return result + basename = src.short_path[len(ctx.label.package) + 1:-len(".css")] + devmode_js = [ + ".css.shim.ngstyle.js", + ".css.ngstyle.js", + ] + summaries = [] + + closure_js = [f.replace(".js", ".closure.js") for f in devmode_js] + declarations = [f.replace(".js", ".d.ts") for f in devmode_js] + + devmode_js_files += [ctx.new_file(ctx.bin_dir, basename + ext) for ext in devmode_js] + closure_js_files += [ctx.new_file(ctx.bin_dir, basename + ext) for ext in closure_js] + declaration_files += [ctx.new_file(ctx.bin_dir, basename + ext) for ext in declarations] + summary_files += [ctx.new_file(ctx.bin_dir, basename + ext) for ext in summaries] + + return struct( + closure_js = closure_js_files, + devmode_js = devmode_js_files, + declarations = declaration_files, + summaries = summary_files, + ) def _ngc_tsconfig(ctx, files, srcs, **kwargs): + outs = _expected_outs(ctx, ctx.label) + if "devmode_manifest" in kwargs: + expected_outs = outs.devmode_js + outs.declarations + outs.summaries + else: + expected_outs = outs.closure_js + return dict(tsc_wrapped_tsconfig(ctx, files, srcs, **kwargs), **{ "angularCompilerOptions": { - "expectedOut": [o.path for o in _expected_outs(ctx)], + "generateCodeForLibraries": False, + # FIXME: wrong place to de-dupe + "expectedOut": depset([o.path for o in expected_outs]).to_list() } }) +def _collect_summaries_aspect_impl(target, ctx): + results = target.angular.summaries if hasattr(target, "angular") else depset() + + # If we are visiting empty-srcs ts_library, this is a re-export + srcs = target.srcs if hasattr(target, "srcs") else [] + + # "re-export" rules should expose all the files of their deps + if not srcs: + for dep in ctx.rule.attr.deps: + if (hasattr(dep, "angular")): + results += dep.angular.summaries + + return struct(collect_summaries_aspect_result = results) + +_collect_summaries_aspect = aspect( + implementation = _collect_summaries_aspect_impl, + attr_aspects = ["deps"], +) + def _compile_action(ctx, inputs, outputs, config_file_path): - externs_files = [] - non_externs_files = [] - for output in outputs: - if output.basename.endswith(".es5.MF"): - ctx.file_action(output, content="") - else: - non_externs_files.append(output) + summaries = depset() + for dep in ctx.attr.deps: + if hasattr(dep, "collect_summaries_aspect_result"): + summaries += dep.collect_summaries_aspect_result - # TODO(alexeagle): For now we mock creation of externs files - for externs_file in externs_files: - ctx.file_action(output=externs_file, content="") + action_inputs = inputs + summaries.to_list() + ctx.files.assets + # print("ASSETS", [a.path for a in ctx.files.assets]) + # print("INPUTS", ctx.label, [o.path for o in summaries if o.path.find("core/src") > 0]) - action_inputs = inputs if hasattr(ctx.attr, "node_modules"): action_inputs += [f for f in ctx.files.node_modules if f.path.endswith(".ts") or f.path.endswith(".json")] - if ctx.file.tsconfig: + if hasattr(ctx.attr, "tsconfig") and ctx.file.tsconfig: action_inputs += [ctx.file.tsconfig] # One at-sign makes this a params-file, enabling the worker strategy. # Two at-signs escapes the argument so it's passed through to ngc # rather than the contents getting expanded. - if ctx.attr.supports_workers: + if ctx.attr._supports_workers: arguments = ["@@" + config_file_path] else: arguments = ["-p", config_file_path] @@ -77,79 +118,75 @@ def _compile_action(ctx, inputs, outputs, config_file_path): progress_message = "Compiling Angular templates (ngc) %s" % ctx.label, mnemonic = "AngularTemplateCompile", inputs = action_inputs, - outputs = non_externs_files, + outputs = outputs, arguments = arguments, executable = ctx.executable.compiler, execution_requirements = { - "supports-workers": str(int(ctx.attr.supports_workers)), + "supports-workers": str(int(ctx.attr._supports_workers)), }, ) +def _prodmode_compile_action(ctx, inputs, outputs, config_file_path): + outs = _expected_outs(ctx, ctx.label) + _compile_action(ctx, inputs, outputs + outs.closure_js, config_file_path) + def _devmode_compile_action(ctx, inputs, outputs, config_file_path): - # TODO(alexeagle): compile for feeding to Closure Compiler - _compile_action(ctx, inputs, outputs + _expected_outs(ctx), config_file_path) + outs = _expected_outs(ctx, ctx.label) + _compile_action(ctx, inputs, outputs + outs.devmode_js + outs.declarations + outs.summaries, config_file_path) -def _compile_ng(ctx): - declarations = [] - for dep in ctx.attr.deps: - if hasattr(dep, "typescript"): - declarations += dep.typescript.transitive_declarations +def ng_module_impl(ctx, ts_compile_actions): + providers = ts_compile_actions( + ctx, is_library=True, compile_action=_prodmode_compile_action, + devmode_compile_action=_devmode_compile_action, + tsc_wrapped_tsconfig=_ngc_tsconfig, + outputs = _expected_outs) - tsconfig_json = ctx.new_file(ctx.label.name + "_tsconfig.json") - ctx.file_action(output=tsconfig_json, content=json_marshal( - _ngc_tsconfig(ctx, ctx.files.srcs + declarations, ctx.files.srcs))) - - _devmode_compile_action(ctx, ctx.files.srcs + declarations + [tsconfig_json], [], tsconfig_json.path) - - return { - "files": depset(_expected_outs(ctx)), - "typescript": { - # FIXME: expose the right outputs so this looks like a ts_library - "declarations": [], - "transitive_declarations": [], - "type_blacklisted_declarations": [], - }, + #addl_declarations = [_expected_outs(ctx)] + #providers["typescript"]["declarations"] += addl_declarations + #providers["typescript"]["transitive_declarations"] += addl_declarations + providers["angular"] = { + "summaries": _expected_outs(ctx, ctx.label).summaries } + return providers + def _ng_module_impl(ctx): - if ctx.attr.write_ng_outputs_only: - ts_providers = _compile_ng(ctx) - else: - ts_providers = compile_ts(ctx, is_library=True, - compile_action=_compile_action, - devmode_compile_action=_devmode_compile_action, - tsc_wrapped_tsconfig=_ngc_tsconfig) + return ts_providers_dict_to_struct(ng_module_impl(ctx, compile_ts)) - addl_declarations = [o for o in _expected_outs(ctx) if o.path.endswith(".d.ts")] - ts_providers["typescript"]["declarations"] += addl_declarations - ts_providers["typescript"]["transitive_declarations"] += addl_declarations +NG_MODULE_ATTRIBUTES = { + "srcs": attr.label_list(allow_files = [".ts"]), - return ts_providers_dict_to_struct(ts_providers) + "deps": attr.label_list(aspects = DEPS_ASPECTS + [_collect_summaries_aspect]), + "assets": attr.label_list(allow_files = [ + ".css", + # TODO(alexeagle): change this to ".ng.html" when usages updated + ".html", + ]), + + # TODO(alexeagle): wire up when we have i18n in bazel + "no_i18n": attr.bool(default = False), + + "compiler": attr.label( + default = Label("//src/ngc-wrapped"), + executable = True, + cfg = "host", + ), + + # TODO(alexeagle): enable workers for ngc + "_supports_workers": attr.bool(default = False), +} ng_module = rule( implementation = _ng_module_impl, - attrs = dict(COMMON_ATTRIBUTES, **{ - "srcs": attr.label_list(allow_files = True), - - # To be used only to bootstrap @angular/core compilation, - # since we want to compile @angular/core with ngc, but ngc depends on - # @angular/core typescript output. - "write_ng_outputs_only": attr.bool(default = False), + attrs = COMMON_ATTRIBUTES + NG_MODULE_ATTRIBUTES + { "tsconfig": attr.label(allow_files = True, single_file = True), - "no_i18n": attr.bool(default = False), - # TODO(alexeagle): enable workers for ngc - "supports_workers": attr.bool(default = False), - "compiler": attr.label( - default = Label("//internal/ngc"), - executable = True, - cfg = "host", - ), + # @// is special syntax for the "main" repository # The default assumes the user specified a target "node_modules" in their # root BUILD file. "node_modules": attr.label( default = Label("@//:node_modules") ), - }), + }, ) \ No newline at end of file diff --git a/packages/bazel/src/ngc-wrapped/BUILD.bazel b/packages/bazel/src/ngc-wrapped/BUILD.bazel index 00dc24e7e2..b5f1368870 100644 --- a/packages/bazel/src/ngc-wrapped/BUILD.bazel +++ b/packages/bazel/src/ngc-wrapped/BUILD.bazel @@ -6,9 +6,14 @@ ts_library( name = "ngc_lib", srcs = ["index.ts"], deps = [ + # BEGIN-INTERNAL + # Only needed when compiling within the Angular repo. + # Users will get this dependency from node_modules. "//packages/compiler-cli", + # END-INTERNAL "@build_bazel_rules_typescript//internal/tsc_wrapped" ], + tsconfig = ":tsconfig.json", ) nodejs_binary( diff --git a/packages/bazel/src/ngc-wrapped/index.ts b/packages/bazel/src/ngc-wrapped/index.ts index 9fc36702a9..a5d26bfeda 100644 --- a/packages/bazel/src/ngc-wrapped/index.ts +++ b/packages/bazel/src/ngc-wrapped/index.ts @@ -5,28 +5,185 @@ * 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 */ - -// TODO(chuckj): Remove the requirement for a fake 'reflect` implementation from +// TODO(chuckj): Remove the requirment for a fake 'reflect` implementation from // the compiler -import 'reflect-metadata'; +import 'reflect-metadata'; // from //third_party/javascript/node_modules/reflect_decorators:ts -import {calcProjectFileAndBasePath, createNgCompilerOptions, formatDiagnostics, performCompilation} from '@angular/compiler-cli'; +import * as ng from '@angular/compiler-cli'; import * as fs from 'fs'; import * as path from 'path'; -// Note, the tsc_wrapped module comes from rules_typescript, not from @angular/tsc-wrapped -import {parseTsconfig} from 'tsc_wrapped'; +// Note, the tsc_wrapped module comes from rules_typescript, not from npm +import {CompilerHost, UncachedFileLoader, parseTsconfig} from 'tsc_wrapped'; +import * as tsickle from 'tsickle'; import * as ts from 'typescript'; -function main(args: string[]) { +const EXT = /(\.ts|\.d\.ts|\.js|\.jsx|\.tsx)$/; +// FIXME: we should be able to add the assets to the tsconfig so FileLoader +// knows about them +const NGC_NON_TS_INPUTS = + /(\.(ngsummary|ngstyle|ngfactory)(\.d)?\.ts|\.ngsummary\.json|\.css|\.html)$/; +// FIXME should need only summary, css, html + +function topologicalSort( + result: tsickle.FileMap, current: string, modulesManifest: tsickle.ModulesManifest, + visiting: tsickle.FileMap) { + const referencedModules = modulesManifest.getReferencedModules(current); + if (!referencedModules) return; // not in the local set of sources. + for (const referencedModule of referencedModules) { + const referencedFileName = modulesManifest.getFileNameFromModule(referencedModule); + if (!referencedFileName) continue; // Ambient modules. + if (!result[referencedFileName]) { + if (visiting[referencedFileName]) { + const path = current + ' -> ' + Object.keys(visiting).join(' -> '); + throw new Error('Cyclical dependency between files:\n' + path); + } + visiting[referencedFileName] = true; + topologicalSort(result, referencedFileName, modulesManifest, visiting); + delete visiting[referencedFileName]; + } + } + result[current] = true; +} +// TODO(alexeagle): move to tsc-wrapped in third_party so it's shared +export function constructManifest( + modulesManifest: tsickle.ModulesManifest, + host: {flattenOutDir: (f: string) => string}): string { + const result: tsickle.FileMap = {}; + for (const file of modulesManifest.fileNames) { + topologicalSort(result, file, modulesManifest, {}); + } + + // NB: The object literal maintains insertion order. + return Object.keys(result).map(fn => host.flattenOutDir(fn)).join('\n') + '\n'; +} + +export function main(args) { const project = args[1]; const [{options: tsOptions, bazelOpts, files, config}] = parseTsconfig(project); - const {basePath} = calcProjectFileAndBasePath(project); - const ngOptions = createNgCompilerOptions(basePath, config, tsOptions); - const {diagnostics} = performCompilation({rootNames: files, options: ngOptions}); - if (diagnostics.length) { - console.error(formatDiagnostics(ngOptions, diagnostics)); + const {basePath} = ng.calcProjectFileAndBasePath(project); + const ngOptions = ng.createNgCompilerOptions(basePath, config, tsOptions); + if (!bazelOpts.es5Mode) { + ngOptions.annotateForClosureCompiler = true; + ngOptions.annotationsAs = 'static fields'; } + + function relativeToRootDir(filePath: string): string { + if (tsOptions.rootDir) { + const rel = path.relative(tsOptions.rootDir, filePath); + if (rel.indexOf('.') != 0) return rel; + } + return filePath; + } + const expectedOuts = [...config['angularCompilerOptions']['expectedOut']]; + const tsHost = ts.createCompilerHost(tsOptions, true); + + const originalWriteFile = tsHost.writeFile.bind(tsHost); + tsHost.writeFile = + (fileName: string, content: string, writeByteOrderMark: boolean, + onError?: (message: string) => void, sourceFiles?: ts.SourceFile[]) => { + const relative = relativeToRootDir(fileName); + const expectedIdx = expectedOuts.findIndex(o => o === relative); + if (expectedIdx >= 0) { + expectedOuts.splice(expectedIdx, 1); + originalWriteFile(fileName, content, writeByteOrderMark, onError, sourceFiles); + } + }; + + + // Patch fileExists when resolving modules, so that ngc can ask TypeScript to + // resolve non-existing generated files that don't exist on disk, but are + // synthetic and added to the `programWithStubs` based on real inputs. + const generatedFileModuleResolverHost = Object.create(tsHost); + generatedFileModuleResolverHost.fileExists = (fileName: string) => { + const match = /^(.*?)\.(ngfactory|ngsummary|ngstyle|shim\.ngstyle)(.*)$/.exec(fileName); + if (match) { + const [, file, suffix, ext] = match; + // Performance: skip looking for files other than .d.ts or .ts + if (ext !== '.ts' && ext !== '.d.ts') return false; + if (suffix.indexOf('ngstyle') >= 0) { + // Look for foo.css on disk + fileName = file; + } else { + // Look for foo.d.ts or foo.ts on disk + fileName = file + (ext || ''); + } + } + return tsHost.fileExists(fileName); + }; + + function generatedFileModuleResolver( + moduleName: string, containingFile: string, + compilerOptions: ts.CompilerOptions): ts.ResolvedModuleWithFailedLookupLocations { + return ts.resolveModuleName( + moduleName, containingFile, compilerOptions, generatedFileModuleResolverHost); + } + + const bazelHost = new CompilerHost( + files, tsOptions, bazelOpts, tsHost, new UncachedFileLoader(), generatedFileModuleResolver); + bazelHost.allowNonHermeticRead = (filePath: string) => + NGC_NON_TS_INPUTS.test(filePath) || filePath.split(path.sep).indexOf('node_modules') != -1; + bazelHost.shouldSkipTsickleProcessing = (fileName: string): boolean => + bazelOpts.compilationTargetSrc.indexOf(fileName) === -1 && !NGC_NON_TS_INPUTS.test(fileName); + + const ngHost = ng.createCompilerHost({options: ngOptions, tsHost: bazelHost}); + + ngHost.fileNameToModuleName = (importedFilePath: string, containingFilePath: string) => + relativeToRootDir(importedFilePath).replace(EXT, ''); + ngHost.toSummaryFileName = (fileName: string, referringSrcFileName: string) => + ngHost.fileNameToModuleName(fileName, referringSrcFileName); + + const tsickleOpts = { + googmodule: bazelOpts.googmodule, + es5Mode: bazelOpts.es5Mode, + prelude: bazelOpts.prelude, + untyped: bazelOpts.untyped, + typeBlackListPaths: new Set(bazelOpts.typeBlackListPaths), + transformDecorators: bazelOpts.tsickle, + transformTypesToClosure: bazelOpts.tsickle, + }; + const emitCallback: ng.TsEmitCallback = ({ + program, + targetSourceFile, + writeFile, + cancellationToken, + emitOnlyDtsFiles, + customTransformers = {}, + }) => + tsickle.emitWithTsickle( + program, bazelHost, tsickleOpts, bazelHost, ngOptions, targetSourceFile, writeFile, + cancellationToken, emitOnlyDtsFiles, { + beforeTs: customTransformers.before, + afterTs: customTransformers.after, + }); + + const {diagnostics, emitResult} = + ng.performCompilation({rootNames: files, options: ngOptions, host: ngHost, emitCallback}); + const tsickleEmitResult = emitResult as tsickle.EmitResult; + let externs = '/** @externs */\n'; + if (diagnostics.length) { + console.error(ng.formatDiagnostics(ngOptions, diagnostics)); + } else { + if (bazelOpts.tsickleGenerateExterns) { + externs += tsickle.getGeneratedExterns(tsickleEmitResult.externs); + } + if (bazelOpts.manifest) { + const manifest = constructManifest(tsickleEmitResult.modulesManifest, bazelHost); + fs.writeFileSync(bazelOpts.manifest, manifest); + } + } + + if (bazelOpts.tsickleExternsPath) { + // Note: when tsickleExternsPath is provided, we always write a file as a + // marker that compilation succeeded, even if it's empty (just containing an + // @externs). + fs.writeFileSync(bazelOpts.tsickleExternsPath, externs); + } + + for (const missing of expectedOuts) { + originalWriteFile(missing, '', false); + } + return diagnostics.some(d => d.category === ts.DiagnosticCategory.Error) ? 1 : 0; } diff --git a/packages/bazel/src/ngc-wrapped/tsconfig.json b/packages/bazel/src/ngc-wrapped/tsconfig.json new file mode 100644 index 0000000000..fd217e560a --- /dev/null +++ b/packages/bazel/src/ngc-wrapped/tsconfig.json @@ -0,0 +1,5 @@ +{ + "compilerOptions": { + "lib": ["es5", "es2015.collection", "es2015.core"] + } +} diff --git a/packages/bazel/src/rules_typescript.bzl b/packages/bazel/src/rules_typescript.bzl new file mode 100644 index 0000000000..77f37b9abb --- /dev/null +++ b/packages/bazel/src/rules_typescript.bzl @@ -0,0 +1,9 @@ +# Allows different paths for these imports in google3 +load("@build_bazel_rules_typescript//internal:build_defs.bzl", "tsc_wrapped_tsconfig") + +load( + "@build_bazel_rules_typescript//internal:common/compilation.bzl", + "COMMON_ATTRIBUTES", "compile_ts", "DEPS_ASPECTS", "ts_providers_dict_to_struct" +) + +load("@build_bazel_rules_typescript//internal:common/json_marshal.bzl", "json_marshal") diff --git a/packages/compiler-cli/src/transformers/api.ts b/packages/compiler-cli/src/transformers/api.ts index cc8860968c..ebd51c931d 100644 --- a/packages/compiler-cli/src/transformers/api.ts +++ b/packages/compiler-cli/src/transformers/api.ts @@ -91,11 +91,6 @@ export interface CompilerOptions extends ts.CompilerOptions { // position. disableExpressionLowering?: boolean; - // The list of expected files, when provided: - // - extra files are filtered out, - // - missing files are created empty. - expectedOut?: string[]; - // Locale of the application i18nOutLocale?: string; // Export format (xlf, xlf2 or xmb) diff --git a/packages/compiler-cli/src/transformers/program.ts b/packages/compiler-cli/src/transformers/program.ts index ee81730f98..acb76edda3 100644 --- a/packages/compiler-cli/src/transformers/program.ts +++ b/packages/compiler-cli/src/transformers/program.ts @@ -143,22 +143,13 @@ class AngularCompilerProgram implements Program { }): ts.EmitResult { const emitMap = new Map(); - const expectedOut = this.options.expectedOut ? - this.options.expectedOut.map(f => path.resolve(process.cwd(), f)) : - undefined; - - // Ensure that expected output files exist. - for (const out of expectedOut || []) { - this.host.writeFile(out, '', false); - } - const emitResult = emitCallback({ program: this.programWithStubs, host: this.host, options: this.options, targetSourceFile: undefined, - writeFile: - createWriteFileCallback(emitFlags, this.host, this.metadataCache, emitMap, expectedOut), + writeFile: createWriteFileCallback( + emitFlags, this.host, this.metadataCache, emitMap, this.generatedFiles), cancellationToken, emitOnlyDtsFiles: (emitFlags & (EmitFlags.DTS | EmitFlags.JS)) == EmitFlags.DTS, customTransformers: this.calculateTransforms(customTransformers) @@ -399,7 +390,9 @@ function writeMetadata( function createWriteFileCallback( emitFlags: EmitFlags, host: ts.CompilerHost, metadataCache: LowerMetadataCache, - emitMap: Map, expectedOut?: string[]) { + emitMap: Map, generatedFiles: GeneratedFile[]) { + const genFileToSrcFile = new Map(); + generatedFiles.forEach(f => genFileToSrcFile.set(f.genFileUrl, f.srcFileUrl)); return (fileName: string, data: string, writeByteOrderMark: boolean, onError?: (message: string) => void, sourceFiles?: ts.SourceFile[]) => { @@ -407,14 +400,15 @@ function createWriteFileCallback( if (sourceFiles && sourceFiles.length == 1) { srcFile = sourceFiles[0]; - emitMap.set(srcFile.fileName, fileName); + const originalSrcFile = genFileToSrcFile.get(srcFile.fileName) || srcFile.fileName; + emitMap.set(originalSrcFile, fileName); } const absFile = path.resolve(process.cwd(), fileName); const generatedFile = GENERATED_FILES.test(fileName); - // Don't emit unexpected files nor empty generated files - if ((!expectedOut || expectedOut.indexOf(absFile) > -1) && (!generatedFile || data)) { + // Don't emit empty generated files + if (!generatedFile || data) { host.writeFile(fileName, data, writeByteOrderMark, onError, sourceFiles); if (srcFile && !generatedFile && (emitFlags & EmitFlags.Metadata) != 0) { diff --git a/scripts/ci/install.sh b/scripts/ci/install.sh index 45dce806e2..784f021ca1 100755 --- a/scripts/ci/install.sh +++ b/scripts/ci/install.sh @@ -61,7 +61,7 @@ if [[ ${TRAVIS} && (${CI_MODE} == "aio" || ${CI_MODE} == "aio_e2e" || ${CI_MODE} fi # Install bazel -if [[ ${TRAVIS} && ${CI_MODE} == "bazel" ]]; then +if [[ ${TRAVIS} && (${CI_MODE} == "bazel" || ${CI_MODE} == "e2e_2") ]]; then travisFoldStart "bazel-install" ( mkdir tmp