Skip to content

Commit

Permalink
build(docs-infra): fix version mismatch of local built packages
Browse files Browse the repository at this point in the history
There is quite some trickery going on with the adev build related to
local packages:

- Adev builds using npm packages from `/node_modules`
- At runtime, we are adding `HEAD` packages for e.g. `@angular/core` to
  the bundles.
- At build time, the CLI, or Angular devkit may accidentally resolve to
  `@angular/core` from `/node_modules/`— which is the core version from
  npm, transitively installed via `@angular/docs`.

This causes a version mismatch, leading to issues like:

- CLI throwing because of a mismatch. angular#54858 (comment)
- Compiler changes not being picked up. angular#54858 (comment)

This commit attempts to fix this by:

- Linking all Angular `HEAD` packages into `adev/node_modules`. The
  current logic attempts to link into `/node_modules`, but this does not
  override existing `@angular/core`!
- Linking all direct external NPM packages, like
  `@angular_devkit/build-angular` into `adev/node_modules` without their
  transitive deps. This allows proper resolution of e.g. compiler as
  node looks in `adev/node_modules` first, and falls back for the rest
  to the execroot `node_modules`, or symlink target destination (if
  `preserveSymlinks=false`).

Note: This is still not 100% ideal because a direct external NPM
dependency may have a transitive dependency that has another transitive
dependency on `@angular/core`. In those cases, the may be a conflict
that is not resolvable until we switch to a Bazel toolchain with better
first party resolution support.
  • Loading branch information
devversion committed Apr 10, 2024
1 parent cae993c commit efd8235
Show file tree
Hide file tree
Showing 5 changed files with 162 additions and 36 deletions.
45 changes: 28 additions & 17 deletions adev/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
load("//:packages.bzl", "link_packages")
load("@npm//@angular/build-tooling/bazel/remote-execution:index.bzl", "ENABLE_NETWORK")
load("@bazel_skylib//rules:common_settings.bzl", "bool_flag")
load("@build_bazel_rules_nodejs//:index.bzl", "copy_to_bin")
load("@npm//@angular-devkit/architect-cli:index.bzl", "architect", "architect_test")
load("@bazel_skylib//lib:collections.bzl", "collections")
load("//adev/tools/local_deps:index.bzl", "ensure_local_package_deps", "link_local_packages")

package(default_visibility = ["//visibility:public"])

Expand Down Expand Up @@ -30,7 +31,17 @@ TEST_FILES = APPLICATION_FILES + [
["**/*.spec.ts"],
)

APPLICATION_DEPS = link_packages([
APPLICATION_ASSETS = [
"//adev/src/assets/images",
"//adev/src/assets/textures",
"//adev/src/assets/previews",
"//adev/src/assets:tutorials",
"//adev/src/assets/icons",
"//adev/src/assets:api",
"//adev/src/assets:content",
]

APPLICATION_DEPS = [
"@npm//@angular-devkit/build-angular",
"@npm//@angular/animations",
"@npm//@angular/cdk",
Expand All @@ -50,13 +61,6 @@ APPLICATION_DEPS = link_packages([
"@npm//ogl",
"@npm//rxjs",
"@npm//typescript",
"//adev/src/assets/images",
"//adev/src/assets/textures",
"//adev/src/assets/previews",
"//adev/src/assets:tutorials",
"//adev/src/assets/icons",
"//adev/src/assets:api",
"//adev/src/assets:content",
"@npm//@typescript/vfs",
"@npm//@codemirror/state",
"@npm//@codemirror/view",
Expand All @@ -76,11 +80,10 @@ APPLICATION_DEPS = link_packages([
"@npm//@xterm/xterm",
"@npm//xterm-addon-fit",
"@npm//angular-split",
])
]

TEST_DEPS = APPLICATION_DEPS + link_packages([
TEST_DEPS = APPLICATION_DEPS + [
"@npm//@angular/platform-browser-dynamic",
"@npm//@angular/build-tooling/bazel/browsers/chromium",
"@npm//@types/jasmine",
"@npm//@types/node",
"@npm//assert",
Expand All @@ -90,8 +93,13 @@ TEST_DEPS = APPLICATION_DEPS + link_packages([
"@npm//karma-coverage",
"@npm//karma-jasmine",
"@npm//karma-jasmine-html-reporter",
"//aio/tools:windows-chromium-path",
])
]

# Create `npm_link` targets for all dependencies that correspond to a
# first-party Angular package that can be built from `HEAD`.
link_local_packages(
all_deps = collections.uniq(APPLICATION_DEPS + TEST_DEPS),
)

copy_to_bin(
name = "application_files_bin",
Expand Down Expand Up @@ -129,7 +137,7 @@ architect(
"--output-path=build",
] + config_based_architect_flags,
chdir = "$(RULEDIR)",
data = APPLICATION_DEPS + [
data = ensure_local_package_deps(APPLICATION_DEPS) + APPLICATION_ASSETS + [
":application_files_bin",
],
# Network is required to inline fonts.
Expand All @@ -149,7 +157,7 @@ architect(
"--watch",
],
chdir = package_name(),
data = APPLICATION_DEPS + [
data = ensure_local_package_deps(APPLICATION_DEPS) + APPLICATION_ASSETS + [
":application_files_bin",
],
tags = [
Expand All @@ -164,7 +172,10 @@ architect_test(
"--no-watch",
],
chdir = package_name(),
data = TEST_DEPS + TEST_FILES,
data = ensure_local_package_deps(TEST_DEPS) + TEST_FILES + APPLICATION_ASSETS + [
"//aio/tools:windows-chromium-path",
"@npm//@angular/build-tooling/bazel/browsers/chromium",
],
env = {
"CHROME_BIN": "../$(CHROMIUM)",
},
Expand Down
Empty file.
50 changes: 50 additions & 0 deletions adev/tools/local_deps/filter_external_npm_deps.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
"""Rule for filtering external NPM dependency targets to not include
transitive dependencies onto first-party linked `HEAD` dependencies."""

load("@build_bazel_rules_nodejs//:providers.bzl", "ExternalNpmPackageInfo", "LinkablePackageInfo")

def _filter_external_npm_deps_impl(ctx):
problematic_paths = ["external/npm/node_modules/%s" % pkg for pkg in ctx.attr.angular_packages]
filtered_deps = []

# Note: to_list() is expensive; we need to invoke it here to get the path
# of each transitive dependency to check if it's an angular npm package.
for file in ctx.attr.target[DefaultInfo].default_runfiles.files.to_list():
if not any([file.path.startswith(path) for path in problematic_paths]):
filtered_deps.append(file)
filtered_depset = depset(filtered_deps)

providers = [
DefaultInfo(files = filtered_depset),
]

# Re-route all direct dependency external NPM packages into `adev/node_modules` without
# their transitive packages. This allows transitive dependency resolution to first look for
# e.g. `@angular/core` in `adev/node_modules`, and falls back to top-level node modules.
if ctx.attr.target.label.workspace_name == "npm":
providers.append(LinkablePackageInfo(
package_name = ctx.attr.target.label.package,
package_path = "adev",
path = "external/npm/node_modules/%s" % ctx.attr.target.label.package,
files = ctx.attr.target[ExternalNpmPackageInfo].direct_sources,
))
else:
fail("Unknown workspace")

return providers

filter_external_npm_deps = rule(
doc = "Filter out transitive angular dependencies from a target",
implementation = _filter_external_npm_deps_impl,
attrs = {
"angular_packages": attr.string_list(
mandatory = True,
doc = "Angular packages to filter (useful for sandbox environments without linker)",
),
"target": attr.label(
mandatory = True,
doc = "Target to filter",
providers = [ExternalNpmPackageInfo],
),
},
)
84 changes: 84 additions & 0 deletions adev/tools/local_deps/index.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
load("//:packages.bzl", "ALL_PACKAGES", "to_package_label")
load("@build_bazel_rules_nodejs//internal/linker:npm_link.bzl", "npm_link")
load("//adev/tools/local_deps:filter_external_npm_deps.bzl", "filter_external_npm_deps")

def ensure_local_package_deps(deps):
"""Replaces dependencies with their local-linked variants."""
return [":%s" % _filtered_transitives_name(dep) for dep in deps]

def link_local_packages(all_deps):
"""Create targets needed for building adev against local angular packages.
Creates targets that link Angular packages, as well as targets to be used
in place of any deps required to build and test adev. These targets filter
out any transitive deps on the npm packages and must be used in place of
any original list of deps.
Use the helper `ensure_local_package_deps()` to translate a list of deps
to the equivalent "filtered" target that this rule creates.
Args:
all_deps: label list of all deps required to build and test adev
"""

local_angular_deps = [dep for dep in all_deps if _is_angular_dep(dep)]
local_angular_package_names = [_angular_dep_to_pkg_name(dep) for dep in local_angular_deps]

# Link local angular packages in place of their npm equivalent
for dep in local_angular_deps:
pkg_name = _angular_dep_to_pkg_name(dep)
npm_link(
name = _npm_link_name(pkg_name),
target = to_package_label(pkg_name),
package_name = pkg_name,
package_path = native.package_name(),
tags = ["manual"],
)

# Special case deps that must be testonly
testonly_deps = [
"@npm//@angular/build-tooling/bazel/browsers/chromium",
]

# Stamp a corresponding target for each dep that filters out transitive
# dependencies on external npm packages. This help the rules_nodejs linker,
# which fails to link local packages into transitive dependencies of npm deps.
for dep in all_deps:
target = dep
if dep in local_angular_deps:
pkg_name = _angular_dep_to_pkg_name(dep)

# We don't need to filter transitives on local packages as they
# depend on each other locally.
native.alias(
name = _filtered_transitives_name(dep),
actual = ":%s" % _npm_link_name(pkg_name),
tags = ["manual"],
)
else:
filter_external_npm_deps(
name = _filtered_transitives_name(dep),
target = target,
testonly = True if dep in testonly_deps else False,
angular_packages = local_angular_package_names,
tags = ["manual"],
)

def _is_angular_dep(dep):
"""Check if a dep , e.g., @npm//@angular/core corresonds to a local Angular pacakge."""
return dep.startswith("@npm//") and (_angular_dep_to_pkg_name(dep) in ALL_PACKAGES)

def _angular_dep_to_pkg_name(dep):
"""E.g., @npm//@angular/core => '@angular/core'"""
label = Label(dep)
return label.package

def _npm_link_name(pkg_name):
return "local_head_%s" % pkg_name.replace("@", "_").replace("/", "_")

def _filtered_transitives_name(dep):
if dep.startswith(":"):
return "%s_without_transitive_deps" % dep[1:]
else:
label = Label(dep)
return "%s_without_transitive_deps" % label.package.replace("@", "_").replace("/", "_")
19 changes: 0 additions & 19 deletions packages.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@
# found in the LICENSE file at https://angular.io/license
"""Packages published to npm"""

load("@build_bazel_rules_nodejs//internal/linker:npm_link.bzl", "npm_link")

def to_package_label(package_name):
"""Get a label corresponding to the npm_package target for the package name"""
if package_name == "angular-in-memory-web-api":
Expand All @@ -19,23 +17,6 @@ def _exclude_pkgs(packages, *args):
modified_packages.remove(pkg)
return modified_packages

def link_packages(packages = []):
linked_packages = []
for package in packages:
pkg_name = Label(package).package
if pkg_name in ALL_PACKAGES:
name = "%s_linked" % pkg_name.replace("@angular/", "").replace("/", "_")
npm_link(
name = name,
target = to_package_label(pkg_name),
package_name = pkg_name,
tags = ["manual"],
)
linked_packages += [":%s" % name]
else:
linked_packages += [package]
return linked_packages

# All framework packages published to NPM.
ALL_PACKAGES = [
"@angular/animations",
Expand Down

0 comments on commit efd8235

Please sign in to comment.