Skip to content

Commit 79cb078

Browse files
committed
build: set up ts_project interop for rules_js migration
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.
1 parent 89342f9 commit 79cb078

File tree

8 files changed

+269
-24
lines changed

8 files changed

+269
-24
lines changed

WORKSPACE

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,5 +187,44 @@ http_archive(
187187
load("@aspect_rules_ts//ts:repositories.bzl", "rules_ts_dependencies")
188188

189189
rules_ts_dependencies(
190+
# Obtained by: curl --silent https://registry.npmjs.org/typescript/5.8.2 | jq -r '.dist.integrity'
191+
ts_integrity = "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==",
190192
ts_version_from = "//:package.json",
191193
)
194+
195+
http_archive(
196+
name = "aspect_rules_rollup",
197+
sha256 = "c4062681968f5dcd3ce01e09e4ba73670c064744a7046211763e17c98ab8396e",
198+
strip_prefix = "rules_rollup-2.0.0",
199+
url = "https://github.com/aspect-build/rules_rollup/releases/download/v2.0.0/rules_rollup-v2.0.0.tar.gz",
200+
)
201+
202+
load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository")
203+
204+
git_repository(
205+
name = "devinfra",
206+
commit = "47572aba6019f368057c00966ac7ce354b1d65bc",
207+
remote = "https://github.com/angular/dev-infra.git",
208+
)
209+
210+
load("@devinfra//bazel:setup_dependencies_1.bzl", "setup_dependencies_1")
211+
212+
setup_dependencies_1()
213+
214+
load("@devinfra//bazel:setup_dependencies_2.bzl", "setup_dependencies_2")
215+
216+
setup_dependencies_2()
217+
218+
git_repository(
219+
name = "rules_angular",
220+
commit = "b1e419e5f6b5e897c07260780cfef2b0aac128fb",
221+
remote = "https://github.com/devversion/rules_angular.git",
222+
)
223+
224+
load("@rules_angular//setup:step_1.bzl", "step_1")
225+
226+
step_1()
227+
228+
load("@rules_angular//setup:step_2.bzl", "step_2")
229+
230+
step_2()

src/BUILD.bazel

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,12 @@ rules_js_tsconfig(
6565
src = "bazel-tsconfig-build.json",
6666
deps = [],
6767
)
68+
69+
rules_js_tsconfig(
70+
name = "test-tsconfig",
71+
src = "bazel-tsconfig-test.json",
72+
deps = [
73+
":build-tsconfig",
74+
"//:node_modules/@types/jasmine",
75+
],
76+
)

src/bazel-tsconfig-build.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,10 @@
3030
"inlineSources": true,
3131
"target": "es2022",
3232
"lib": ["es2020", "dom"],
33-
"skipLibCheck": true
33+
"skipLibCheck": true,
34+
"paths": {
35+
"@angular/cdk/*": ["./cdk/*"]
36+
}
3437
},
3538
"angularCompilerOptions": {
3639
"extendedDiagnostics": {

tools/bazel/BUILD.bazel

Whitespace-only changes.

tools/bazel/module_name.bzl

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
def compute_module_name(testonly):
2+
current_pkg = native.package_name()
3+
4+
# For test-only targets we do not compute any module name as
5+
# those are not publicly exposed through the `@angular` scope.
6+
if testonly:
7+
return None
8+
9+
# We generate no module name for files outside of `src/<pkg>` (usually tools).
10+
if not current_pkg.startswith("src/"):
11+
return None
12+
13+
# Skip module name generation for internal apps which are not built as NPM package
14+
# and not scoped under `@angular/`. This includes e2e-app, dev-app and universal-app.
15+
if "-app" in current_pkg:
16+
return None
17+
18+
# Construct module names based on the current Bazel package. e.g. if a target is
19+
# defined within `src/cdk/a11y` then the module name will be `@angular/cdk/a11y`.
20+
return "@angular/%s" % current_pkg[len("src/"):]

tools/bazel/ts_project_interop.bzl

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
load("@aspect_rules_js//js:providers.bzl", "JsInfo", "js_info")
2+
load("@aspect_rules_ts//ts:defs.bzl", _ts_project = "ts_project")
3+
load("@build_bazel_rules_nodejs//:providers.bzl", "DeclarationInfo", "JSEcmaScriptModuleInfo", "JSModuleInfo", "LinkablePackageInfo")
4+
load("@devinfra//bazel/ts_project:index.bzl", "strict_deps_test")
5+
6+
def _ts_deps_interop_impl(ctx):
7+
types = []
8+
sources = []
9+
runfiles = ctx.runfiles(files = [])
10+
for dep in ctx.attr.deps:
11+
if not DeclarationInfo in dep:
12+
fail("Expected target with DeclarationInfo: %s", dep)
13+
types.append(dep[DeclarationInfo].transitive_declarations)
14+
if not JSModuleInfo in dep:
15+
fail("Expected target with JSModuleInfo: %s", dep)
16+
sources.append(dep[JSModuleInfo].sources)
17+
if not DefaultInfo in dep:
18+
fail("Expected target with DefaultInfo: %s", dep)
19+
runfiles = runfiles.merge(dep[DefaultInfo].default_runfiles)
20+
21+
return [
22+
DefaultInfo(runfiles = runfiles),
23+
## NOTE: We don't need to propagate module mappings FORTUNATELY!
24+
# because rules_nodejs supports tsconfig path mapping, given that
25+
# everything is nicely compiled from `bazel-bin/`!
26+
js_info(
27+
target = ctx.label,
28+
transitive_types = depset(transitive = types),
29+
transitive_sources = depset(transitive = sources),
30+
),
31+
]
32+
33+
ts_deps_interop = rule(
34+
implementation = _ts_deps_interop_impl,
35+
attrs = {
36+
"deps": attr.label_list(providers = [DeclarationInfo], mandatory = True),
37+
},
38+
)
39+
40+
def _ts_project_module_impl(ctx):
41+
# Forward runfiles. e.g. JSON files on `ts_project#data`. The jasmine
42+
# consuming rules may rely on this, or the linker due to its symlinks then.
43+
runfiles = ctx.attr.dep[DefaultInfo].default_runfiles
44+
info = ctx.attr.dep[JsInfo]
45+
46+
# Filter runfiles to not include `node_modules` from Aspect as this interop
47+
# target is supposed to be used downstream by `rules_nodejs` consumers,
48+
# and mixing pnpm-style node modules with linker node modules is incompatible.
49+
filtered_runfiles = []
50+
for f in runfiles.files.to_list():
51+
if f.short_path.startswith("node_modules/"):
52+
continue
53+
filtered_runfiles.append(f)
54+
runfiles = ctx.runfiles(files = filtered_runfiles)
55+
56+
providers = [
57+
DefaultInfo(
58+
runfiles = runfiles,
59+
),
60+
JSModuleInfo(
61+
direct_sources = info.sources,
62+
sources = depset(transitive = [info.transitive_sources]),
63+
),
64+
JSEcmaScriptModuleInfo(
65+
direct_sources = info.sources,
66+
sources = depset(transitive = [info.transitive_sources]),
67+
),
68+
DeclarationInfo(
69+
declarations = _filter_types_depset(info.types),
70+
transitive_declarations = _filter_types_depset(info.transitive_types),
71+
type_blocklisted_declarations = depset(),
72+
),
73+
]
74+
75+
if ctx.attr.module_name:
76+
providers.append(
77+
LinkablePackageInfo(
78+
package_name = ctx.attr.module_name,
79+
package_path = "",
80+
path = "%s/%s/%s" % (ctx.bin_dir.path, ctx.label.workspace_root, ctx.label.package),
81+
files = info.sources,
82+
),
83+
)
84+
85+
return providers
86+
87+
ts_project_module = rule(
88+
implementation = _ts_project_module_impl,
89+
attrs = {
90+
"dep": attr.label(providers = [JsInfo], mandatory = True),
91+
# Noop attribute for aspect propagation of the linker interop deps; so
92+
# that transitive linker dependencies are discovered.
93+
"deps": attr.label_list(),
94+
# Note: The module aspect from consuming `ts_library` targets will
95+
# consume the module mappings automatically.
96+
"module_name": attr.string(),
97+
"module_root": attr.string(),
98+
},
99+
)
100+
101+
def ts_project(
102+
name,
103+
module_name = None,
104+
deps = [],
105+
interop_deps = [],
106+
tsconfig = None,
107+
testonly = False,
108+
visibility = None,
109+
ignore_strict_deps = False,
110+
enable_runtime_rnjs_interop = True,
111+
**kwargs):
112+
# Pull in the `rules_nodejs` variants of dependencies we know are "hybrid". This
113+
# is necessary as we can't mix `npm/node_modules` from RNJS with the pnpm-style
114+
# symlink-dependent node modules. In addition, we need to extract `_rjs` interop
115+
# dependencies so that we can forward and capture the module mappings for runtime
116+
# execution, with regards to first-party dependency linking.
117+
rjs_modules_to_rnjs = []
118+
if enable_runtime_rnjs_interop:
119+
for d in deps:
120+
if d.startswith("//:node_modules/"):
121+
rjs_modules_to_rnjs.append(d.replace("//:node_modules/", "@npm//"))
122+
if d.endswith("_rjs"):
123+
rjs_modules_to_rnjs.append(d.replace("_rjs", ""))
124+
125+
if tsconfig == None:
126+
tsconfig = "//src:test-tsconfig" if testonly else "//src:build-tsconfig"
127+
128+
ts_deps_interop(
129+
name = "%s_interop_deps" % name,
130+
deps = [] + interop_deps + rjs_modules_to_rnjs,
131+
visibility = visibility,
132+
testonly = testonly,
133+
)
134+
135+
_ts_project(
136+
name = "%s_rjs" % name,
137+
testonly = testonly,
138+
declaration = True,
139+
tsconfig = tsconfig,
140+
visibility = visibility,
141+
# Use the worker from our own Angular rules, as the default worker
142+
# from `rules_ts` is incompatible with TS5+ and abandoned. We need
143+
# worker for efficient and fast DX.
144+
supports_workers = 1,
145+
tsc_worker = "@rules_angular//worker:worker_vanilla_ts",
146+
deps = [":%s_interop_deps" % name] + deps,
147+
**kwargs
148+
)
149+
150+
if not ignore_strict_deps:
151+
strict_deps_test(
152+
name = "%s_strict_deps_test" % name,
153+
srcs = kwargs.get("srcs", []),
154+
deps = deps,
155+
)
156+
157+
ts_project_module(
158+
name = name,
159+
testonly = testonly,
160+
visibility = visibility,
161+
dep = "%s_rjs" % name,
162+
# Forwarded dependencies for linker module mapping aspect.
163+
# RJS deps can also transitively pull in module mappings from their `interop_deps`.
164+
deps = [] + ["%s_interop_deps" % name] + deps,
165+
module_name = module_name,
166+
)
167+
168+
# Filter type provider to not include `.json` files. `ts_config`
169+
# targets are included in `ts_project` and their tsconfig json file
170+
# is included as type. See:
171+
# https://github.com/aspect-build/rules_ts/blob/main/ts/private/ts_config.bzl#L55C63-L55C68.
172+
def _filter_types_depset(types_depset):
173+
types = []
174+
175+
for t in types_depset.to_list():
176+
if t.short_path.endswith(".json"):
177+
continue
178+
types.append(t)
179+
180+
return depset(types)

tools/defaults.bzl

Lines changed: 3 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ load("//:pkg-externals.bzl", "PKG_EXTERNALS")
1919
load("//tools/markdown-to-html:index.bzl", _markdown_to_html = "markdown_to_html")
2020
load("//tools/extract-tokens:index.bzl", _extract_tokens = "extract_tokens")
2121
load("//tools/angular:index.bzl", "LINKER_PROCESSED_FW_PACKAGES")
22+
load("//tools/bazel:module_name.bzl", "compute_module_name")
2223

2324
_DEFAULT_TSCONFIG_BUILD = "//src:bazel-tsconfig-build.json"
2425
_DEFAULT_TSCONFIG_TEST = "//src:tsconfig-test"
@@ -47,27 +48,6 @@ def _make_tsec_test(target):
4748
tsconfig = "//src:tsec_config",
4849
)
4950

50-
def _compute_module_name(testonly):
51-
current_pkg = native.package_name()
52-
53-
# For test-only targets we do not compute any module name as
54-
# those are not publicly exposed through the `@angular` scope.
55-
if testonly:
56-
return None
57-
58-
# We generate no module name for files outside of `src/<pkg>` (usually tools).
59-
if not current_pkg.startswith("src/"):
60-
return None
61-
62-
# Skip module name generation for internal apps which are not built as NPM package
63-
# and not scoped under `@angular/`. This includes e2e-app, dev-app and universal-app.
64-
if "-app" in current_pkg:
65-
return None
66-
67-
# Construct module names based on the current Bazel package. e.g. if a target is
68-
# defined within `src/cdk/a11y` then the module name will be `@angular/cdk/a11y`.
69-
return "@angular/%s" % current_pkg[len("src/"):]
70-
7151
def _getDefaultTsConfig(testonly):
7252
if testonly:
7353
return _DEFAULT_TSCONFIG_TEST
@@ -104,7 +84,7 @@ def ts_library(
10484
tsconfig = _getDefaultTsConfig(testonly)
10585

10686
# Compute an AMD module name for the target.
107-
module_name = _compute_module_name(testonly)
87+
module_name = compute_module_name(testonly)
10888

10989
_ts_library(
11090
# `module_name` is used for AMD module names within emitted JavaScript files.
@@ -140,7 +120,7 @@ def ng_module(
140120
tsconfig = _getDefaultTsConfig(testonly)
141121

142122
# Compute an AMD module name for the target.
143-
module_name = _compute_module_name(testonly)
123+
module_name = compute_module_name(testonly)
144124

145125
local_deps = [
146126
# Add tslib because we use import helpers for all public packages.

tools/defaults2.bzl

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
load("//tools/bazel:ts_project_interop.bzl", _ts_project = "ts_project")
2+
load("//tools/bazel:module_name.bzl", "compute_module_name")
3+
4+
def ts_project(
5+
name,
6+
source_map = True,
7+
testonly = False,
8+
**kwargs):
9+
_ts_project(
10+
name,
11+
source_map = source_map,
12+
module_name = compute_module_name(testonly),
13+
**kwargs
14+
)

0 commit comments

Comments
 (0)