-
Notifications
You must be signed in to change notification settings - Fork 612
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[api-extractor] API Extractor now properly runs on projects with tsconfig path mappings. #3321
Conversation
@octogonz I remember you saying a while back that path mappings are strongly deprecated/discouraged within the Rush Stack world. Can you remind me why that's the case? |
@rbuckton @DanielRosenwasser Do you have any opinions about this TypeScript API question? Changing the semantics of |
I suppose there are few reasons:
|
Thanks for sharing these points - responses inline.
Agreed that there's complexity here. Although there are benefits for developers too:
Do you have an example of how this might happen? I don't work often in open source TS development or in the Rush Stack ecosystem so I'm not the best person to come up with one. But my understanding is path mappings are project-internal? If a package with path mappings ships its directory structure as part of its API (e.g. by having multiple entry points), other packages don't need to use those same path mappings. But I'm not confident of this.
Agreed that these are good things to strive for in monorepos. But I wonder if these could also be accomplished by all packages within a monorepo using the same path mappings and these path mappings being minimal & sensible in their own way. This is basically the state of the monorepo I work in. We have a single trivial path mapping so that all imports are from the same root path, and then a small set of additional path mappings for special types of 3P imports. |
Here's a simple experiment, which compiles with src/a/A.ts export class A { } src/b/B.ts import { A } from 'a/A';
export class B extends A { } src/index.ts export * from 'a/A';
export * from 'b/B'; It produces declarations like this: lib/index.d.ts export * from 'a/A';
export * from 'b/B'; lib/b/B.d.ts import { A } from 'a/A'; Now if you publish that as an NPM package called external-consumer.ts import { A } from 'example'; It will fail because the consumer's TypeScript compiler does not know how to resolve This seems to make path mapping hijinks untenable for an NPM package, and only usable for end consumers. If I understand right.
Sure, this is fine if your monorepo has its own proprietary structure. By contrast Rush models each project in a monorepo as a conventional NPM package. This better fits the assumptions of community tools, and it also helps adoption by making it easy to migrate projects in/out of monorepos and between monorepos. Your original question was about "the Rush Stack world" -- I don't mean to imply that everyone should accept these constraints. 😊 |
Hey @octogonz, best to take another look before merging. See #3321 (comment) for details. |
common/changes/@microsoft/api-extractor/path-mapping_2022-04-06-02-38.json
Outdated
Show resolved
Hide resolved
common/changes/@microsoft/api-extractor/path-mapping_2022-04-06-02-38.json
Outdated
Show resolved
Hide resolved
…6-02-38.json Co-authored-by: Pete Gonzalez <[email protected]>
🚀 Released with |
In the tweet about this fix, you suggest it's to benefit Bazel projects, so naturally I'd like to understand how to use it. But the discussion here doesn't mention a Bazel use case and neither does the associated issue. Do you have any pointers to some context I could read? |
I can share some context here. I work at Google, and as you also used to work there I know you're familiar with Google's Bazel/TS setup. I've been working on pulling API Extractor into Google so that teams can take advantage of some of its features (e.g. API documentation generation, API golden report, etc). When you run API Extractor on some TypeScript project, it has logic that determines which code is part of the "current package" (that it should be generating full documentation for) and which code is part of external packages (that it should ignore). Before this PR, its logic was essentially: imports to relative paths (e.g. This doesn't work well for Google's Bazel/TS setup, as it ignores any path mappings specified in the tsconfig (e.g. a tsconfig generated by Bazel's This PR introduces an entirely new way of determining whether code is part of an external package that's compatible with mappings set up in a tsconfig. Now, I'm not sure if all Bazel/TS projects rely upon tsconfig path mappings like this, or just Google's. If the former, then I think it's reasonable to say that this PR is a step in the right direction for Bazel projects to be able to use API Extractor. But it's definitely not correct to say that this PR only benefits Bazel projects (i.e. there are plenty of TS projects that use path mappings without Bazel). This is only some high-level context, and I'd love to chat more @alexeagle if you have time. You can reach me at zell [at] google [dot] com or the API Extractor Zulip chat. |
wondering if there are any plans to fix this ? we recently bumped API-extractor which is causing issues for rolluping dts/ api.md generation microsoft/fluentui#28215 I reported this some time ago here #3443. It would be great if path aliasing worked as expected otherwise this makes this tool tightly coupled to npm/yarn workspaces. |
[api-extractor] API Extractor now properly runs on projects with tsconfig path mappings.
Fixes #3291
Background
API Extractor uses the
_isExternalModulePath
method inExportAnalyzer.ts
to determine whether or not a particular module specifier (i.e. from animport
orexport
statement) is part of the current package or an external package. A module specifier is determined to be part of the current package IFF (1) it is a relative path (i.e.ts.isExternalModuleNameRelative
returns true) or (2) it is part of a bundled package.However, the logic does not take into consideration any path mappings that may have been set up in the project's tsconfig. For example, consider the following project scenario:
paths
{ "foo": ["some/path/to/foo"] }
andbaseUrl
as"."
.import Foo from 'foo';
In this case, given
ts.isExternalModuleNameRelative
returns false for'foo'
and'foo'
isn't a bundled package, API Extractor considers this module specifier to be external and will not generate any API doc model nodes for it. However, the actual import after resolving any path mappings is'./some/path/to/foo'
, which is relative. If the import was originally written asimport Foo from './some/path/to/foo'
, we would have generated API doc model nodes for it. We should fix this inconsistency in behavior.Solution
Update
Please see #3321 (comment) for latest approach.
Previous approach
This PR adds additional logic to the
_isExternalModulePath
method that - in addition to the criteria above - determines a module specifier is relative if it matches any tsconfig path mapping. We consider it relative in this case because the fully-resolved path is guaranteed to be relative, as the mappings are always resolved relative to"baseUrl"
. The logic handles two types of path mappings (the only two that I know exist):some/path/to/import
some/path/to/*
If there are other kinds of syntaxes that are allowed in path mappings, this PR may not handle them correctly. I couldn't find an exhaustive reference for the allowed syntax in path mappings.
Testing
I tested this change by adding a new test scenario within
api-extractor-scenarios
calledpathMappings
. This test scenario references path mappings set up in the project tsconfig. I then ranrush rebuild
to validate that the correct.api.json
was generated. Other test scenarios'.api.json
files did not change.Alternatives considered
I think there may be an argument to be made that
ts.isExternalModuleNameRelative
should take into consideration path mappings and return true for any path mapping matches. I'm curious what the API Extractor maintainers think of this idea. If so, then the fix for this bug would be instead within the TypeScript project, and we wouldn't have to amend the_isExternalModulePath
method. Regardless, it may still make sense to check in this PR in the meantime if we think the discussion & change tots.isExternalModuleNameRelative
might take time.