Open Josmithr opened 6 months ago
We discussed this topic. I think it breaks down into some subproblems:
Providing a more formalized way to develop a collection of packages with mixed release tags: If my company has an SDK with 10 packages, and we want to release a "public" and "beta" version of them, today API Extractor recommends to publish separate NPM SemVer versions for the 1.2.3
and 1.2.3-beta
releases. The RushJS workflow for that is not formalized (is it two separate CI jobs? is it one build that produces two publishing outputs?), so we could work out this model in more detail, and give a more explicit recommendation. @Josmithr has suggested a different approach leveraging package.json exports, which did not exist when API Extractor first introduced this feature -- maybe there is a solution that doesn't rely on publishing separate NPM versions.
Importing both @beta
and @public
in the same app: This GitHub issue is really asking about how to support an app that wants to import both versions of the API simultaneously. With structural typing (duck typing), the TypeScript compiler will mostly allow this (although behind the scenes it can kill compiler performance), but in cases of nominal types (e.g. branded types, classes with private
members, etc) you will get confusing compiler errors. There are ways to design an API that models @beta
as adding new declarations to the existing @public
definitions, but generally this is a hard problem; maybe it is really a TypeScript problem and not an API Extractor problem.
Supporting import-level trimming: @Josmithr described an internal tool his team created, that uses @public
and @beta
to determine which top-level API exports are reachable. API Extractor could support this, but only if we disable the ability to use @beta
on members of interfaces/classes. That way each API item has a single definition, and @beta
is really just giving access to more imports. However, I think we still might want to trim @internal
so customers can't access it at all. Thus, the design might be something like my-api.internal.d.ts
and my-api.alpha.d.ts
-- and then my-api.beta.d.ts
and my-api.public.d.ts
would reexport selective subsets of my-api.alpha.d.ts
. Although this is somewhat different from API Extractor's release tag model today, I think it's feasible to implement, and within the scope of our mission.
Overview
My team is currently leveraging package.json exports combined with trimmed type rollups to manage our package APIs. We have our packages configured with something like the following:
By default, consumers of our packages only see the
public
types, but we provide explicit entrypoints for ourbeta
andalpha
APIs that can be used from the same package dependency.This is extremely convenient, as we neither need to maintain multiple packages for different release scopes, nor do customers have to change dependencies to opt into experimental features.
While this pattern works fine for structurally typed exports, it breaks down for nominally typed exports.
Example Scenario
Consider the following example, the code for which can be found here:
A repository contains 2 packages with the following dependency relationship:
package-b
-depends-on->package-a
.package-a
exports the following nominally typed class:package-a
is leveraging package.json exports to surface 2 variations of its API:.
)/internal
.package-b
consumes nominally typed classFoo
, but it does so via a mix of the two export paths (this is a contrived example, but it reflects a scenario that occurs easily with more code modules).package-b
has the following code:Ideally, this would work fine - both of the rollups being leveraged refer to the same underlying type. Unfortunately, since API-Extractor generates the rollups via redeclarations, TypeScript deems the 2 nominally typed class declarations as being incompatible. And, in fact, the above code in
package-b
fails to build.Feature Request
Ideally, API-Extractor could surface nominally typed exports in such a way that guarantees compatibility.
A naive solution, which we have adopted internally, is to simply re-export existing declarations from the
d.ts
entrypoint API-Extractor consumes, rather than redeclaring them. It isn't immediately clear to me if such a pattern is compatible with API-Extractor's use-cases or not.Would love to hear thoughts from the maintainers on this.