build: fix linking of cdk/material in devtools and simplify code (#60516)

This commit fixes the linking of CDK/Material which recently broke
because the CDK/Material package now comes with potential shared FESM
chunks; that our current hard-coded, manual linking process doesn't know
about. This ultimately resulted in duplicate code, breaking
Material/CDK.

This commit fixes that.

In addition to the fix, we simplify our linking significantly and reduce
the rather large complexity around linking, or having to specify every
entry-point manually, by linking the full package and putting it into
a different location. This is also what we conceptually are doing in
Angular Material as part of the `rules_js` migration.

PR Close #60516
This commit is contained in:
Paul Gschwendtner 2025-03-21 13:48:04 +00:00 committed by Alex Rickabaugh
parent 7408a1f58b
commit 115c2f8d38
8 changed files with 159 additions and 136 deletions

View file

@ -1,51 +0,0 @@
_CDK_ENTRY_POINTS = [
"scrolling",
"tree",
"keycodes",
"collections",
"overlay",
"table",
"text-field",
"accordion",
"drag-drop",
"a11y",
"platform",
]
_MATERIAL_ENTRY_POINTS = [
"dialog",
"menu",
"slide-toggle",
"grid-list",
"tree",
"expansion",
"checkbox",
"select",
"input",
"button",
"core",
"progress-bar",
"snack-bar",
"icon",
"progress-spinner",
"tabs",
"card",
"form-field",
"tooltip",
"toolbar",
]
ANGULAR_PACKAGES_CONFIG = [
("@angular/cdk", struct(entry_points = _CDK_ENTRY_POINTS)),
("@angular/material", struct(entry_points = _MATERIAL_ENTRY_POINTS)),
]
ANGULAR_PACKAGES = [
struct(
name = name[len("@angular/"):],
entry_points = config.entry_points,
platform = config.platform if hasattr(config, "platform") else "browser",
module_name = name,
)
for name, config in ANGULAR_PACKAGES_CONFIG
]

View file

@ -1,3 +1,19 @@
load("//devtools/tools/linking:index.bzl", "link_package")
package(default_visibility = ["//devtools:__subpackages__"])
exports_files([
"bazel-karma-local-config.js",
])
link_package(
name = "angular_cdk",
package_name = "@angular/cdk",
npm_package = "@npm//@angular/cdk",
)
link_package(
name = "angular_material",
package_name = "@angular/material",
npm_package = "@npm//@angular/material",
)

View file

@ -1,6 +1,5 @@
load("@build_bazel_rules_nodejs//:index.bzl", "js_library")
load("//tools:defaults.bzl", "esbuild_config")
load(":index.bzl", "create_angular_bundle_targets")
package(default_visibility = ["//visibility:public"])
@ -46,5 +45,3 @@ esbuild_config(
":esbuild_base",
],
)
create_angular_bundle_targets()

View file

@ -1,84 +1,4 @@
load("@build_bazel_rules_nodejs//:providers.bzl", "ExternalNpmPackageInfo", "JSModuleInfo")
load("@build_bazel_rules_nodejs//internal/linker:link_node_modules.bzl", "LinkerPackageMappingInfo")
load("@npm//@angular/build-tooling/bazel/esbuild:index.bzl", "esbuild")
load("//devtools:packages.bzl", "ANGULAR_PACKAGES")
"""
Starlark file exposing a definition for generating Angular linker-processed ESM bundles
for all entry-points the Angular framework packages expose.
These linker-processed ESM bundles are useful as they can be integrated into the
spec bundling, or dev-app to avoid unnecessary re-linking of framework entry-points
every time the bundler executes. This helps with the overall development turnaround and
is more idiomatic as it allows caching of the Angular framework packages.
"""
def _linker_mapping_impl(ctx):
return [
# Pass through the `ExternalNpmPackageInfo` which is needed for the linker
# resolving dependencies which might be external. e.g. `rxjs` for `@angular/core`.
ctx.attr.package[ExternalNpmPackageInfo],
JSModuleInfo(
direct_sources = depset(ctx.files.srcs),
sources = depset(ctx.files.srcs),
),
LinkerPackageMappingInfo(
mappings = depset([
struct(
package_name = ctx.attr.module_name,
package_path = "",
link_path = "%s/%s" % (ctx.label.package, ctx.attr.subpath),
),
]),
node_modules_roots = depset([]),
),
]
_linker_mapping = rule(
implementation = _linker_mapping_impl,
attrs = {
"package": attr.label(),
"srcs": attr.label_list(allow_files = False),
"subpath": attr.string(),
"module_name": attr.string(),
},
)
def _get_target_name_base(pkg, entry_point):
return "%s%s" % (pkg.name, "_%s" % entry_point if entry_point else "")
def _create_bundle_targets(pkg, entry_point, module_name):
target_name_base = _get_target_name_base(pkg, entry_point)
fesm_bundle_path = "fesm2022/%s.mjs" % (entry_point if entry_point else pkg.name)
esbuild(
name = "%s_linked_bundle" % target_name_base,
output = "%s/index.mjs" % target_name_base,
platform = pkg.platform,
entry_point = "@npm//:node_modules/@angular/%s/%s" % (pkg.name, fesm_bundle_path),
deps = ["@npm//@angular/%s" % pkg.name],
config = "//devtools/tools/esbuild:esbuild_config_esm",
# List of dependencies which should never be bundled into these linker-processed bundles.
external = ["rxjs", "@angular", "domino", "xhr2", "@material"],
)
_linker_mapping(
name = "%s_linked" % target_name_base,
srcs = [":%s_linked_bundle" % target_name_base],
package = "@npm//@angular/%s" % pkg.name,
module_name = module_name,
subpath = target_name_base,
)
def create_angular_bundle_targets():
for pkg in ANGULAR_PACKAGES:
_create_bundle_targets(pkg, None, pkg.module_name)
for entry_point in pkg.entry_points:
_create_bundle_targets(pkg, entry_point, "%s/%s" % (pkg.module_name, entry_point))
LINKER_PROCESSED_FW_PACKAGES = [
"//devtools/tools/esbuild:%s_linked" % _get_target_name_base(pkg, entry_point)
for pkg in ANGULAR_PACKAGES
for entry_point in [None] + pkg.entry_points
"//devtools/tools:angular_cdk",
"//devtools/tools:angular_material",
]

View file

@ -0,0 +1,19 @@
load("@build_bazel_rules_nodejs//:index.bzl", "copy_to_bin")
load("//tools:defaults.bzl", "nodejs_binary")
copy_to_bin(
name = "linker_srcs",
srcs = ["index.mjs"],
)
nodejs_binary(
name = "linker_bin",
data = [
":linker_srcs",
"//packages/compiler-cli/linker/babel",
"@npm//@babel/core",
"@npm//tinyglobby",
],
entry_point = ":index.mjs",
visibility = ["//devtools/tools:__subpackages__"],
)

View file

@ -0,0 +1,19 @@
load("@build_bazel_rules_nodejs//:index.bzl", "npm_package_bin")
load("//devtools/tools/linking:linker_mapping.bzl", "linker_mapping")
def link_package(name, package_name, npm_package):
npm_package_bin(
name = "%s_package_out" % name,
data = [npm_package],
args = ["./external/npm/node_modules/%s" % package_name, "$(@D)"],
output_dir = True,
tool = "//devtools/tools/linking:linker_bin",
)
linker_mapping(
name = name,
srcs = [":%s_package_out" % name],
package = npm_package,
module_name = package_name,
subpath = "./%s_package_out" % name,
)

View file

@ -0,0 +1,70 @@
/**
* @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.dev/license
*/
/**
* @fileoverview
*
* Script that copies the npm package contents of e.g. `@angular/cdk` over into
* a new output directory while performing Angular linking using the local
* Angular compiler-cli version.
*
* This is necessary for the devtools as we don't want to rely on JIT compilation,
* and consumed libraries like Angular CDK, or Angular Material are only shipping
* partial compilation output to npm.
*/
import linkerBabelPlugin from '../../../packages/compiler-cli/linker/babel/index.mjs';
import {transformAsync} from '@babel/core';
import {readFile, writeFile, mkdir} from 'node:fs/promises';
import {globSync} from 'tinyglobby';
import path from 'path';
async function main() {
const [packageDir, outDir] = process.argv.slice(2);
// Copy without preserving readonly permissions from Bazel.
await Promise.all(
globSync('**/*', {cwd: packageDir}).map(async (filePath) => {
await mkdir(path.dirname(path.join(outDir, filePath)), {recursive: true});
await writeFile(path.join(outDir, filePath), await readFile(path.join(packageDir, filePath)));
}),
);
const fesmBundles = globSync('fesm2022/**/*.mjs', {cwd: outDir});
const tasks = [];
const babelOptions = {
plugins: [
[
linkerBabelPlugin,
{
// We compile with an unstamped version of the compiler, so ignore.
unknownDeclarationVersionHandling: 'ignore',
},
],
],
};
for (const bundleFile of fesmBundles) {
tasks.push(
(async () => {
const filePath = path.join(outDir, bundleFile);
const content = await readFile(filePath, 'utf8');
const result = await transformAsync(content, {...babelOptions, filename: filePath});
await writeFile(path.join(outDir, bundleFile), result.code);
})(),
);
}
await Promise.all(tasks);
}
main().catch((e) => {
console.error(e, e.stack);
process.exitCode = 1;
});

View file

@ -0,0 +1,33 @@
load("@build_bazel_rules_nodejs//:providers.bzl", "ExternalNpmPackageInfo", "JSModuleInfo")
load("@build_bazel_rules_nodejs//internal/linker:link_node_modules.bzl", "LinkerPackageMappingInfo")
def _linker_mapping_impl(ctx):
return [
# Pass through the `ExternalNpmPackageInfo` which is needed for the linker
# resolving dependencies which might be external. e.g. `rxjs` for `@angular/core`.
ctx.attr.package[ExternalNpmPackageInfo],
JSModuleInfo(
direct_sources = depset(ctx.files.srcs),
sources = depset(ctx.files.srcs),
),
LinkerPackageMappingInfo(
mappings = depset([
struct(
package_name = ctx.attr.module_name,
package_path = "",
link_path = "%s/%s" % (ctx.label.package, ctx.attr.subpath),
),
]),
node_modules_roots = depset([]),
),
]
linker_mapping = rule(
implementation = _linker_mapping_impl,
attrs = {
"package": attr.label(),
"srcs": attr.label_list(allow_files = False),
"subpath": attr.string(),
"module_name": attr.string(),
},
)