diff --git a/WORKSPACE b/WORKSPACE index ba91b14c33e..9c1bb2337ff 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -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", +) diff --git a/packages/BUILD.bazel b/packages/BUILD.bazel index b3ce929e674..befd74ca1e9 100644 --- a/packages/BUILD.bazel +++ b/packages/BUILD.bazel @@ -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([ diff --git a/tools/bazel/BUILD.bazel b/tools/bazel/BUILD.bazel new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tools/bazel/module_name.bzl b/tools/bazel/module_name.bzl new file mode 100644 index 00000000000..ada283760cb --- /dev/null +++ b/tools/bazel/module_name.bzl @@ -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 diff --git a/tools/bazel/ts_project_interop.bzl b/tools/bazel/ts_project_interop.bzl new file mode 100644 index 00000000000..f9c7552fb26 --- /dev/null +++ b/tools/bazel/ts_project_interop.bzl @@ -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) diff --git a/tools/defaults.bzl b/tools/defaults.bzl index 4e15ac8b50f..796c506c3b9 100644 --- a/tools/defaults.bzl +++ b/tools/defaults.bzl @@ -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" diff --git a/tools/defaults2.bzl b/tools/defaults2.bzl new file mode 100644 index 00000000000..fdc86c74bb2 --- /dev/null +++ b/tools/defaults2.bzl @@ -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 + )