build: set up ts_project interop for rules_js migration (#61087)

The `ts_project` interop rule that we've built was also used in the
Angular CLI migration, and it allows us to mix `ts_project` and
`ts_library` targets; enabling an incremental migration. Additionally
set up the `ng_project` to replace `ng_module`.

PR Close #61087
This commit is contained in:
Joey Perrott 2025-05-01 14:57:38 +00:00 committed by Andrew Kushnir
parent 1c7f669f33
commit 72b7de0d73
7 changed files with 305 additions and 36 deletions

View file

@ -2,6 +2,7 @@ workspace(
name = "angular",
)
load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository")
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
load("//:yarn.bzl", "YARN_LABEL")
@ -129,9 +130,18 @@ http_archive(
load("@aspect_rules_ts//ts:repositories.bzl", "rules_ts_dependencies")
rules_ts_dependencies(
# Obtained by: curl --silent https://registry.npmjs.org/typescript/5.8.2 | jq -r '.dist.integrity'
ts_integrity = "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==",
ts_version_from = "//:package.json",
)
http_archive(
name = "aspect_rules_rollup",
sha256 = "c4062681968f5dcd3ce01e09e4ba73670c064744a7046211763e17c98ab8396e",
strip_prefix = "rules_rollup-2.0.0",
url = "https://github.com/aspect-build/rules_rollup/releases/download/v2.0.0/rules_rollup-v2.0.0.tar.gz",
)
load("@aspect_bazel_lib//lib:repositories.bzl", "aspect_bazel_lib_dependencies")
aspect_bazel_lib_dependencies()
@ -231,3 +241,38 @@ yarn_install(
yarn = YARN_LABEL,
yarn_lock = "//packages/core/schematics/migrations/signal-migration/test/ts-versions:yarn.lock",
)
git_repository(
name = "devinfra",
commit = "c4f7d3cdec164044284139182b709dfd4be339ed",
remote = "https://github.com/angular/dev-infra.git",
)
load("@devinfra//bazel:setup_dependencies_1.bzl", "setup_dependencies_1")
setup_dependencies_1()
load("@devinfra//bazel:setup_dependencies_2.bzl", "setup_dependencies_2")
setup_dependencies_2()
git_repository(
name = "rules_angular",
commit = "0a54fca16350cab2b823908f1725aec175fcfeb2",
remote = "https://github.com/devversion/rules_angular.git",
)
load("@rules_angular//setup:step_1.bzl", "rules_angular_step1")
rules_angular_step1()
load("@rules_angular//setup:step_2.bzl", "rules_angular_step2")
rules_angular_step2()
load("@rules_angular//setup:step_3.bzl", "rules_angular_step3")
rules_angular_step3(
angular_compiler_cli = "//:node_modules/@angular/compiler-cli",
typescript = "//:node_modules/typescript",
)

View file

@ -37,7 +37,18 @@ ts_config(
rules_js_tsconfig(
name = "build-tsconfig",
src = "tsconfig-build.json",
deps = [],
deps = [
"//:node_modules/tslib",
],
)
rules_js_tsconfig(
name = "test-tsconfig",
src = "tsconfig-test.json",
deps = [
":build-tsconfig",
"//:node_modules/@types/jasmine",
],
)
exports_files([

0
tools/bazel/BUILD.bazel Normal file
View file

View file

@ -0,0 +1,27 @@
def compute_module_name(testonly):
""" Provide better defaults for package names.
e.g. rather than angular/packages/core/testing we want @angular/core/testing
"""
pkg = native.package_name()
if testonly:
# Some tests currently rely on the long-form package names
return None
if pkg.startswith("packages/bazel"):
# Avoid infinite recursion in the ViewEngine compiler. Error looks like:
# Compiling Angular templates (ngc) //packages/bazel/test/ngc-wrapped/empty:empty failed (Exit 1)
# : RangeError: Maximum call stack size exceeded
# at normalizeString (path.js:57:25)
# at Object.normalize (path.js:1132:12)
# at Object.join (path.js:1167:18)
# at resolveModule (execroot/angular/bazel-out/host/bin/packages/bazel/src/ngc-wrapped/ngc-wrapped.runfiles/angular/packages/compiler-cli/src/metadata/bundler.js:582:50)
# at MetadataBundler.exportAll (execroot/angular/bazel-out/host/bin/packages/bazel/src/ngc-wrapped/ngc-wrapped.runfiles/angular/packages/compiler-cli/src/metadata/bundler.js:119:42)
# at MetadataBundler.exportAll (execroot/angular/bazel-out/host/bin/packages/bazel/src/ngc-wrapped/ngc-wrapped.runfiles/angular/packages/compiler-cli/src/metadata/bundler.js:121:52)
return None
if pkg.startswith("packages/"):
return "@angular/" + pkg[len("packages/"):]
return None

View file

@ -0,0 +1,173 @@
load("@aspect_rules_js//js:providers.bzl", "JsInfo", "js_info")
load("@build_bazel_rules_nodejs//:providers.bzl", "DeclarationInfo", "JSEcmaScriptModuleInfo", "JSModuleInfo", "LinkablePackageInfo")
load("@devinfra//bazel/ts_project:index.bzl", "strict_deps_test")
load("@rules_angular//src/ts_project:index.bzl", _ts_project = "ts_project")
def _ts_deps_interop_impl(ctx):
types = []
sources = []
runfiles = ctx.runfiles(files = [])
for dep in ctx.attr.deps:
if not DeclarationInfo in dep:
fail("Expected target with DeclarationInfo: %s", dep)
types.append(dep[DeclarationInfo].transitive_declarations)
if not JSModuleInfo in dep:
fail("Expected target with JSModuleInfo: %s", dep)
sources.append(dep[JSModuleInfo].sources)
if not DefaultInfo in dep:
fail("Expected target with DefaultInfo: %s", dep)
runfiles = runfiles.merge(dep[DefaultInfo].default_runfiles)
return [
DefaultInfo(runfiles = runfiles),
## NOTE: We don't need to propagate module mappings FORTUNATELY!
# because rules_nodejs supports tsconfig path mapping, given that
# everything is nicely compiled from `bazel-bin/`!
js_info(
target = ctx.label,
transitive_types = depset(transitive = types),
transitive_sources = depset(transitive = sources),
),
]
ts_deps_interop = rule(
implementation = _ts_deps_interop_impl,
attrs = {
"deps": attr.label_list(providers = [DeclarationInfo], mandatory = True),
},
)
def _ts_project_module_impl(ctx):
# Forward runfiles. e.g. JSON files on `ts_project#data`. The jasmine
# consuming rules may rely on this, or the linker due to its symlinks then.
runfiles = ctx.attr.dep[DefaultInfo].default_runfiles
info = ctx.attr.dep[JsInfo]
# Filter runfiles to not include `node_modules` from Aspect as this interop
# target is supposed to be used downstream by `rules_nodejs` consumers,
# and mixing pnpm-style node modules with linker node modules is incompatible.
filtered_runfiles = []
for f in runfiles.files.to_list():
if f.short_path.startswith("node_modules/"):
continue
filtered_runfiles.append(f)
runfiles = ctx.runfiles(files = filtered_runfiles)
providers = [
DefaultInfo(
runfiles = runfiles,
),
JSModuleInfo(
direct_sources = info.sources,
sources = depset(transitive = [info.transitive_sources]),
),
JSEcmaScriptModuleInfo(
direct_sources = info.sources,
sources = depset(transitive = [info.transitive_sources]),
),
DeclarationInfo(
declarations = _filter_types_depset(info.types),
transitive_declarations = _filter_types_depset(info.transitive_types),
type_blocklisted_declarations = depset(),
),
]
if ctx.attr.module_name:
providers.append(
LinkablePackageInfo(
package_name = ctx.attr.module_name,
package_path = "",
path = "%s/%s/%s" % (ctx.bin_dir.path, ctx.label.workspace_root, ctx.label.package),
files = info.sources,
),
)
return providers
ts_project_module = rule(
implementation = _ts_project_module_impl,
attrs = {
"dep": attr.label(providers = [JsInfo], mandatory = True),
# Noop attribute for aspect propagation of the linker interop deps; so
# that transitive linker dependencies are discovered.
"deps": attr.label_list(),
# Note: The module aspect from consuming `ts_library` targets will
# consume the module mappings automatically.
"module_name": attr.string(),
"module_root": attr.string(),
},
)
def ts_project(
name,
module_name = None,
deps = [],
interop_deps = [],
tsconfig = None,
testonly = False,
visibility = None,
ignore_strict_deps = False,
enable_runtime_rnjs_interop = True,
rule_impl = _ts_project,
**kwargs):
# Pull in the `rules_nodejs` variants of dependencies we know are "hybrid". This
# is necessary as we can't mix `npm/node_modules` from RNJS with the pnpm-style
# symlink-dependent node modules. In addition, we need to extract `_rjs` interop
# dependencies so that we can forward and capture the module mappings for runtime
# execution, with regards to first-party dependency linking.
rjs_modules_to_rnjs = []
if enable_runtime_rnjs_interop:
for d in deps:
if d.startswith("//:node_modules/"):
rjs_modules_to_rnjs.append(d.replace("//:node_modules/", "@npm//"))
if d.endswith("_rjs"):
rjs_modules_to_rnjs.append(d.replace("_rjs", ""))
ts_deps_interop(
name = "%s_interop_deps" % name,
deps = [] + interop_deps + rjs_modules_to_rnjs,
visibility = visibility,
testonly = testonly,
)
rule_impl(
name = "%s_rjs" % name,
testonly = testonly,
declaration = True,
tsconfig = tsconfig,
visibility = visibility,
deps = [":%s_interop_deps" % name] + deps,
**kwargs
)
if not ignore_strict_deps:
strict_deps_test(
name = "%s_strict_deps_test" % name,
srcs = kwargs.get("srcs", []),
deps = deps,
)
ts_project_module(
name = name,
testonly = testonly,
visibility = visibility,
dep = "%s_rjs" % name,
# Forwarded dependencies for linker module mapping aspect.
# RJS deps can also transitively pull in module mappings from their `interop_deps`.
deps = [] + ["%s_interop_deps" % name] + deps,
module_name = module_name,
)
# Filter type provider to not include `.json` files. `ts_config`
# targets are included in `ts_project` and their tsconfig json file
# is included as type. See:
# https://github.com/aspect-build/rules_ts/blob/main/ts/private/ts_config.bzl#L55C63-L55C68.
def _filter_types_depset(types_depset):
types = []
for t in types_depset.to_list():
if t.short_path.endswith(".json"):
continue
types.append(t)
return depset(types)

View file

@ -20,6 +20,7 @@ load("@npm//typescript:index.bzl", "tsc")
load("@rules_pkg//:pkg.bzl", "pkg_tar")
load("//adev/shared-docs/pipeline/api-gen:generate_api_docs.bzl", _generate_api_docs = "generate_api_docs")
load("//packages/bazel:index.bzl", _ng_module = "ng_module", _ng_package = "ng_package")
load("//tools/bazel:module_name.bzl", "compute_module_name")
load("//tools/esm-interop:index.bzl", "enable_esm_node_module_loader", _nodejs_binary = "nodejs_binary", _nodejs_test = "nodejs_test")
_DEFAULT_TSCONFIG_TEST = "//packages:tsconfig-test"
@ -65,37 +66,6 @@ PKG_GROUP_REPLACEMENTS = {
]""" % ",\n ".join(["\"%s\"" % s for s in ANGULAR_SCOPED_PACKAGES]),
}
def _default_module_name(testonly):
""" Provide better defaults for package names.
e.g. rather than angular/packages/core/testing we want @angular/core/testing
TODO(alexeagle): we ought to supply a default module name for every library in the repo.
But we short-circuit below in cases that are currently not working.
"""
pkg = native.package_name()
if testonly:
# Some tests currently rely on the long-form package names
return None
if pkg.startswith("packages/bazel"):
# Avoid infinite recursion in the ViewEngine compiler. Error looks like:
# Compiling Angular templates (ngc) //packages/bazel/test/ngc-wrapped/empty:empty failed (Exit 1)
# : RangeError: Maximum call stack size exceeded
# at normalizeString (path.js:57:25)
# at Object.normalize (path.js:1132:12)
# at Object.join (path.js:1167:18)
# at resolveModule (execroot/angular/bazel-out/host/bin/packages/bazel/src/ngc-wrapped/ngc-wrapped.runfiles/angular/packages/compiler-cli/src/metadata/bundler.js:582:50)
# at MetadataBundler.exportAll (execroot/angular/bazel-out/host/bin/packages/bazel/src/ngc-wrapped/ngc-wrapped.runfiles/angular/packages/compiler-cli/src/metadata/bundler.js:119:42)
# at MetadataBundler.exportAll (execroot/angular/bazel-out/host/bin/packages/bazel/src/ngc-wrapped/ngc-wrapped.runfiles/angular/packages/compiler-cli/src/metadata/bundler.js:121:52)
return None
if pkg.startswith("packages/"):
return "@angular/" + pkg[len("packages/"):]
return None
ts_config = _ts_config
def ts_library(
@ -118,13 +88,13 @@ def ts_library(
tsconfig = _DEFAULT_TSCONFIG_TEST
if not module_name:
module_name = _default_module_name(testonly)
module_name = compute_module_name(testonly)
# If no `package_name` is explicitly set, we use the default module name as package
# name, so that the target can be resolved within NodeJS executions, by activating
# the Bazel NodeJS linker. See: https://github.com/bazelbuild/rules_nodejs/pull/2799.
if not package_name:
package_name = _default_module_name(testonly)
package_name = compute_module_name(testonly)
default_module = "esnext"
@ -160,13 +130,13 @@ def ng_module(name, tsconfig = None, entry_point = None, testonly = False, deps
tsconfig = _DEFAULT_TSCONFIG_TEST
if not module_name:
module_name = _default_module_name(testonly)
module_name = compute_module_name(testonly)
# If no `package_name` is explicitly set, we use the default module name as package
# name, so that the target can be resolved within NodeJS executions, by activating
# the Bazel NodeJS linker. See: https://github.com/bazelbuild/rules_nodejs/pull/2799.
if not package_name:
package_name = _default_module_name(testonly)
package_name = compute_module_name(testonly)
if not entry_point:
entry_point = "public_api.ts"

43
tools/defaults2.bzl Normal file
View file

@ -0,0 +1,43 @@
load("@rules_angular//src/ng_project:index.bzl", _ng_project = "ng_project")
load("//tools/bazel:module_name.bzl", "compute_module_name")
load("//tools/bazel:ts_project_interop.bzl", _ts_project = "ts_project")
def ts_project(
name,
source_map = True,
testonly = False,
tsconfig = None,
**kwargs):
module_name = kwargs.pop("module_name", compute_module_name(testonly))
if tsconfig == None and native.package_name().startswith("packages"):
tsconfig = "//packages:test-tsconfig" if testonly else "//packages:build-tsconfig"
_ts_project(
name,
source_map = source_map,
module_name = module_name,
testonly = testonly,
tsconfig = tsconfig,
**kwargs
)
def ng_project(
name,
source_map = True,
testonly = False,
tsconfig = None,
**kwargs):
module_name = kwargs.pop("module_name", compute_module_name(testonly))
if tsconfig == None and native.package_name().startswith("packages"):
tsconfig = "//packages:test-tsconfig" if testonly else "//packages:build-tsconfig"
_ts_project(
name,
source_map = source_map,
module_name = module_name,
rule_impl = _ng_project,
testonly = testonly,
tsconfig = tsconfig,
**kwargs
)