-
Notifications
You must be signed in to change notification settings - Fork 53
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(bazel): introduce rule for bundling specs with optional angular …
…linker We exposea macro for bundling Bazel targets with spec files into a single spec IIFE/AMD IIFE file. Bundling is helpful as it avoids unnecessary complexity with module resolution at runtime with loaders such as SystemJS or RequireJS. Additionally, given that Angular framework packages do no longer ship UMD bundles, bundling simplifies the integration of those FW packages significantly. It also helps with incorporating the Angular linker-processed output of FW ESM bundles.
- Loading branch information
1 parent
32a04e0
commit e67feed
Showing
15 changed files
with
764 additions
and
12 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
package(default_visibility = ["//visibility:public"]) | ||
|
||
exports_files(["esbuild.config-tmpl.mjs"]) | ||
|
||
# Make source files available for distribution via pkg_npm | ||
filegroup( | ||
name = "files", | ||
srcs = glob(["*"]), | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
def _spec_bundle_config_file_impl(ctx): | ||
ctx.actions.expand_template( | ||
template = ctx.file._template, | ||
output = ctx.outputs.output_name, | ||
substitutions = { | ||
"TMPL_RUN_LINKER": "true" if ctx.attr.run_angular_linker else "false", | ||
}, | ||
) | ||
|
||
spec_bundle_config_file = rule( | ||
implementation = _spec_bundle_config_file_impl, | ||
doc = "Generates an ESBuild configuration file for bundling specs", | ||
attrs = { | ||
"run_angular_linker": attr.bool( | ||
doc = "Whether the Angular linker should process all files.", | ||
default = False, | ||
), | ||
"output_name": attr.output( | ||
mandatory = True, | ||
doc = "Name of the file where the config should be written to.", | ||
), | ||
"_template": attr.label( | ||
allow_single_file = True, | ||
default = "//bazel/spec-bundling:esbuild.config-tmpl.mjs", | ||
), | ||
}, | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
/** | ||
* @license | ||
* Copyright Google LLC 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 | ||
*/ | ||
|
||
/** | ||
* Loads and creates the ESBuild linker plugin. | ||
* | ||
* The plugin is not loaded at top-level as not all spec bundle targets rely | ||
* on the linker and this would slow-down bundling. | ||
*/ | ||
async function fetchAndCreateLinkerEsbuildPlugin() { | ||
// Note: This needs to be a NPM module path as this ESBuild config is generated and can | ||
// end up in arbitrary Bazel packages or differently-named consumer workspaces. | ||
const {createLinkerEsbuildPlugin} = await import( | ||
'@angular/dev-infra-private/shared-scripts/angular-linker/esbuild-plugin.mjs' | ||
); | ||
return await createLinkerEsbuildPlugin(/.*/, /* ensureNoPartialDeclaration */ true); | ||
} | ||
|
||
// Based on the Bazel action and its substitutions, we run the linker for all inputs. | ||
const plugins = TMPL_RUN_LINKER ? [await fetchAndCreateLinkerEsbuildPlugin()] : []; | ||
|
||
export default { | ||
// `tslib` sets the `module` condition to resolve to ESM. | ||
conditions: ['es2020', 'es2015', 'module'], | ||
// This ensures that we prioritize ES2020. RxJS would otherwise use the ESM5 output. | ||
mainFields: ['es2020', 'es2015', 'module', 'main'], | ||
// Use the `iife` format for the test entry-point as tests should run immediately. | ||
// For browser tests which are wrapped in an AMD header and footer, this works as well. | ||
format: 'iife', | ||
plugins, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
load("@build_bazel_rules_nodejs//:index.bzl", "js_library") | ||
load("//bazel/esbuild:index.bzl", "esbuild", "esbuild_amd", "esbuild_config") | ||
load("//bazel/spec-bundling:spec-entrypoint.bzl", "spec_entrypoint") | ||
load("//bazel/spec-bundling:bundle-config.bzl", "spec_bundle_config_file") | ||
|
||
""" | ||
Starlark file exposing a macro for bundling Bazel targets with spec files into | ||
a single spec ESM/AMD file. Bundling is helpful as it avoids unnecessary complexity | ||
with module resolution at runtime with loaders such as SystemJS or RequireJS. | ||
Additionally, given that Angular framework packages do no longer ship UMD bundles, | ||
bundling simplifies the integration of those FW packages significantly. It also helps | ||
with incorporating Angular linker-processed output of library ESM files. | ||
""" | ||
|
||
def spec_bundle( | ||
name, | ||
deps, | ||
platform, | ||
run_angular_linker = False, | ||
# We cannot use `ES2017` or higher as that would result in `async/await` not being downleveled. | ||
# ZoneJS needs to be able to intercept these as otherwise change detection would not work properly. | ||
target = "es2016", | ||
workspace_name = None, | ||
**kwargs): | ||
""" | ||
Macro that will bundle all test files, with their respective transitive dependencies, | ||
into a single bundle file that can be loaded within Karma or NodeJS directly. Test files | ||
are bundled as Angular framework packages do not ship UMD files and to avoid overall | ||
complexity with maintaining a runtime loader such as RequireJS or SystemJS. | ||
""" | ||
|
||
is_browser_test = platform == "browser" | ||
package_name = native.package_name() | ||
|
||
spec_entrypoint( | ||
name = "%s_spec_entrypoint" % name, | ||
deps = deps, | ||
testonly = True, | ||
) | ||
|
||
spec_bundle_config_file( | ||
name = "%s_config_file" % name, | ||
testonly = True, | ||
output_name = "%s_config.mjs" % name, | ||
run_angular_linker = run_angular_linker, | ||
) | ||
|
||
esbuild_config( | ||
name = "%s_config" % name, | ||
config_file = ":%s_config_file" % name, | ||
testonly = True, | ||
deps = ["//shared-scripts/angular-linker:js_lib"], | ||
) | ||
|
||
if is_browser_test and not workspace_name: | ||
fail("The spec-bundling target %s is declared as browser test. In order to be able " + | ||
"to construct an AMD module name, the `workspace_name` attribute needs to be set.") | ||
|
||
# Browser tests (Karma) need named AMD modules to load. | ||
# TODO(devversion): consider updating `@bazel/concatjs` to support loading JS files directly. | ||
esbuild_rule = esbuild_amd if is_browser_test else esbuild | ||
amd_name = "%s/%s/%s" % (workspace_name, package_name, name + "_spec") if is_browser_test else None | ||
|
||
esbuild_rule( | ||
name = "%s_bundle" % name, | ||
testonly = True, | ||
config = ":%s_config" % name, | ||
entry_point = ":%s_spec_entrypoint" % name, | ||
module_name = amd_name, | ||
output = "%s_spec.js" % name, | ||
target = target, | ||
platform = platform, | ||
deps = deps + [":%s_spec_entrypoint" % name], | ||
link_workspace_root = True, | ||
**kwargs | ||
) | ||
|
||
js_library( | ||
name = name, | ||
testonly = True, | ||
named_module_srcs = [":%s_bundle" % name], | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
load("@build_bazel_rules_nodejs//:providers.bzl", "JSModuleInfo") | ||
|
||
def _is_spec_file(file): | ||
"""Gets whether the given file is a spec file.""" | ||
basename = file.basename | ||
|
||
# External files (from other workspaces) should never run as specs. | ||
if (file.short_path.startswith("../")): | ||
return False | ||
|
||
# `spec.js` or `spec.mjs` files will be imported in the entry-point. | ||
return basename.endswith("spec.js") or basename.endswith("spec.mjs") | ||
|
||
def _filter_spec_files(files): | ||
"""Filters the given list of files to only contain spec files.""" | ||
result = [] | ||
for file in files: | ||
if _is_spec_file(file): | ||
result.append(file) | ||
return result | ||
|
||
def _create_entrypoint_file(base_package, spec_files): | ||
"""Creates the contents of the spec entry-point ESM file which imports | ||
all individual spec files so that these are bundled and loaded by Node/Karma.""" | ||
output = "" | ||
for file in spec_files: | ||
base_dir_segments = "/".join([".."] * len(base_package.split("/"))) | ||
output += """import "%s/%s";\n""" % (base_dir_segments, file.short_path) | ||
return output | ||
|
||
def _spec_entrypoint_impl(ctx): | ||
output = ctx.actions.declare_file("%s.mjs" % ctx.attr.name) | ||
spec_depsets = [] | ||
|
||
for dep in ctx.attr.deps: | ||
if JSModuleInfo in dep: | ||
spec_depsets.append(dep[JSModuleInfo].sources) | ||
else: | ||
spec_depsets.append(dep[DefaultInfo].files) | ||
|
||
spec_files = [] | ||
|
||
for spec_depset in spec_depsets: | ||
# Note: `to_list()` is an expensive operation but we need to do this for every | ||
# dependency here in order to be able to filter out spec files from depsets. | ||
spec_files.extend(_filter_spec_files(spec_depset.to_list())) | ||
|
||
ctx.actions.write( | ||
output = output, | ||
content = _create_entrypoint_file(ctx.label.package, spec_files), | ||
) | ||
|
||
out_depset = depset([output]) | ||
|
||
return [ | ||
DefaultInfo(files = out_depset), | ||
JSModuleInfo( | ||
direct_sources = out_depset, | ||
sources = depset(transitive = [out_depset] + spec_depsets), | ||
), | ||
] | ||
|
||
spec_entrypoint = rule( | ||
implementation = _spec_entrypoint_impl, | ||
attrs = { | ||
"deps": attr.label_list(allow_files = False, mandatory = True), | ||
}, | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
load("//bazel/spec-bundling:index.bzl", "spec_bundle") | ||
load("//tools:defaults.bzl", "jasmine_node_test", "ts_library") | ||
|
||
ts_library( | ||
name = "test_lib", | ||
testonly = True, | ||
srcs = glob(["**/*.spec.ts"]), | ||
deps = [ | ||
"@npm//@angular/core", | ||
], | ||
) | ||
|
||
spec_bundle( | ||
name = "test_bundle", | ||
platform = "node", | ||
run_angular_linker = True, | ||
deps = [":test_lib"], | ||
) | ||
|
||
jasmine_node_test( | ||
name = "test", | ||
specs = [ | ||
":test_bundle", | ||
], | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
// This is an ESM import that would usually break within `jasmine_node_test` because it | ||
// consumes devmode CommonJS sources and `rules_nodejs` does not support ESM well yet. | ||
import {VERSION} from '@angular/core'; | ||
import * as core from '@angular/core'; | ||
|
||
describe('@angular/core ESM import', () => { | ||
it('should work', () => { | ||
expect(VERSION.major).toBeGreaterThanOrEqual(13); | ||
}); | ||
|
||
it('should have run the linker', () => { | ||
expect(() => { | ||
class TestCmp {} | ||
core.ɵɵngDeclareComponent({ | ||
version: '0.0.0', | ||
minVersion: '12.0.0', | ||
type: TestCmp, | ||
selector: 'test', | ||
ngImport: core, | ||
template: `<span>Test template</span>`, | ||
} as any); | ||
}).not.toThrow(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
package(default_visibility = ["//visibility:public"]) | ||
|
||
filegroup( | ||
name = "static_files", | ||
srcs = [ | ||
"//shared-scripts/angular-linker:static_files", | ||
], | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
load("@build_bazel_rules_nodejs//:index.bzl", "js_library") | ||
|
||
package(default_visibility = ["//visibility:public"]) | ||
|
||
filegroup( | ||
name = "static_files", | ||
srcs = glob(["*"]), | ||
) | ||
|
||
js_library( | ||
name = "js_lib", | ||
package_name = "@angular/dev-infra-private/shared-scripts/angular-linker", | ||
srcs = ["esbuild-plugin.mjs"], | ||
deps = [ | ||
"@npm//@angular/compiler-cli", | ||
"@npm//@babel/core", | ||
], | ||
) |
Oops, something went wrong.