aspect-build / aspect-build/rules_ts
[FR]: `types_only()`
- Dominant language
- Starlark
- Stars
- 138
- Forks
- 87
- Avg merge
- 2d 8h
- Merged PRs (30d)
- 3
Description
### What is the current behavior?
`ts_project()` generally provides a `JsInfo` with `.d.ts` declarations _and_ `.js` sources, which is typically what you want in most situations. However, sometimes you want to type check against a dependency _without_ including the implementation of that dependency. This can happen because you expect the implementation to be provided as an input or linked separately. In my particular situation, I would like to type check a tool against my own `npm_package()` built at HEAD but _not_ include it in the output tool's implementation, since it will be dynamically linked against the same `npm_package()` in the user's workspace. See https://github.com/dgp1130/rules_prerender/issues/48#issuecomment-1182656780 for more context.
You can technically do this today with [`emit_declaration_only`](https://docs.aspect.build/rules/aspect_rules_ts/docs/rules/#emit_declaration_only) however this has two problems when applied to this use case:
1. It only applies to `ts_project()`, not `npm_package()`.
2. It must be applied to the `ts_project()` dependency, not the _usage_ of that dependency. When package `foo` is built, it should contain its full implementation. However a particular dependent of it may want to limit itself to just consuming the types of `foo`, rather than the full `*.js`.
You can also do this with `import type` which makes TypeScript prevent value references to that import. However there is nothing stopping misguided developers from _adding_ a value import in the future. _Also_ `import type` is not known to the Bazel layer and does not prevent emitting the JavaScript. This will still pull JavaScript from the type-only dependency, even if the import is elided at runtime. This can complicate dependency resolution, particularly in cases of multiple workspaces. You'd need to pass through a bundler to fully tree shake the unused JS implementation, and that shouldn't be needed for something like this.
### Describe the feature
I propose a `types_only()` rule which effectively strips the JS implementation from the `JsInfo` provider. You would use it like so:
```starlark
load("@aspect_rules_ts//ts:defs.bzl", "ts_project", "types_only")
ts_project(
name = "lib",
srcs = ["lib.d.ts"],
declaration = True,
# ...
)
# Use normally with JS sources included.
ts_project(
name = "app",
deps = [":lib"],
)
types_only(
name = "lib_types",
dep = ":lib",
)
# Depend only on the types, `lib.js` is not in the transitive output.
# `tsc` will error on value usages of the dependency.
ts_project(
name = "app_with_lib_types",
deps = [":lib_types"],
)
```
It implementation is pretty straightforward:
```starlark
load("@aspect_rules_js//js:providers.bzl", "JsInfo", "js_info")
def _types_only_impl(ctx):
info = ctx.attr.dep[JsInfo]
return js_info(
declarations = info.declarations,
npm_linked_package_files = depset(),
npm_linked_packages = depset(),
npm_package_store_deps = depset(),
sources = depset(),
transitive_declarations = info.transitive_declarations,
transitive_npm_linked_package_files = depset(),
transitive_npm_linked_packages = depset(),
transitive_sources = depset(),
)
types_only = rule(
implementation = _types_only_impl,
attrs = {
"dep": attr.label(
mandatory = True,
providers = [JsInfo],
doc = "`ts_project()` to remove JS implementation from."
),
},
doc = "Provides only the types of the given dependency, no implementation.",
)
```
Since `npm_link_package()` returns a `JsInfo` as well, this same implementation works there. However it has the caveat that NPM packages are implemented with a `TreeArtifact` in the `declarations` / `transitive_declarations` property. That `TreeArtifact` still maintains `*.js` sources, even if all the other properties of the provider are dropped. That means that if you use `types_only()` on an `npm_link_package()`, `tsc` will actually pass though the output won't include the `*.js` files. Ideally the `*.js` files would be removed from the NPM package and the `package.json` would be limited to just `.d.ts`, though I don't see an easily feasible way of doing that. Not sure what the best approach is there beyond accepting the foot gun that `tsc` will allow value references for dependents of `types_only()` of NPM packages.
### Fund our work
- [ ] Sponsor our open source work by donating a [feature bounty](https://opencollective.com/aspect-build/)
Contributor guide
Research direction
Start by tracing the JsInfo providers returned by ts_project() and npm_link_package(), focusing on how declarations, sources, and transitive fields are consumed. Compare the proposed types_only() implementation with the existing rule entry points, then verify that type-only dependencies omit JavaScript from transitive output while preserving declarations.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- build-system
- Issue type
- Feature
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100