Closed alan-agius4 closed 3 years ago
export declare const ɵdefineDirective: import("./nested").CssSelector[];
Oh man... do people really do this? :-P Is there a practical reason why this shouldn't be replaced by a conventional named import?
API Extractor should add support for this in theory, but I've been trying to triage people's feature requests into 3 categories:
Inline import
statements seem like they would definitely be in category #<!---->3.
@pgonzal, the lazy import is not written in code, but emitted by the ts compiler.
Example having the following nested.ts
export enum SelectorFlags {
NOT = 0b0001
}
export type CssSelector = (string | SelectorFlags)[];
export function foo(): CssSelector {
return;
}
index.ts
import { foo } from "./nested";
export const x = foo();
index.d.ts
export declare const x: (string | import("./nested").SelectorFlags)[];
While this can be trivial to fix and not rely on type inference, I'd at least expect that the dts bundler to emit some sort of error to warn the user.
Ahh thanks for clarifying. This should be pretty easy to fix I think.
BTW the "easy" fix is to report an error when this is encountered. Properly rewiring the import
statements is not easy, since in general the referenced module may not be exported at all.
Hi! Author of rollup-plugin-dts here :-)
I just did a short comparison of some edge-cases between api-extractor and my plugin and noticed that your specific usecase might be supported by my plugin without a problem. Feel free to try it and give some feedback :-)
I believe we're also encountering that bug when trying to build https://github.com/async-library/react-async with pika, which internally uses api-extractor.
Typescript creates type files with import type references like state: AsyncState<T, import("./Async").AbstractState<T>>;
and api-extractor then fails to inline those, resulting in a final file like this: https://gist.github.com/phryneas/d101e7e0b8070f6dc8b44540825c87cd#file-index-d-ts-L220 (line 220)
Could you take another look at this please? (Original issue over at pika https://github.com/pikapkg/builders/issues/87 )
@phryneas Can you share a repro branch that I could use to investigate/debug this?
As far as the repro above, what's happening is that the type of x
is not explicitly declared:
index.ts
import { foo } from "./nested";
export const x = foo();
Newer versions of the TypeScript compiler (>= 3.2) have started emitting the inferred type into the output, like this:
index.d.ts
export declare const x: (string | import("./nested").SelectorFlags)[];
The ugly-looking import("./nested")
is generated I suppose because the compiler wants to avoid introducing a new local symbol in the file. (In some cases that might require renaming CssSelector
to avoid a naming conflict.) To me this seems like not the best design choice. The compiler should either have left x
as implicitly typed (which does not affect the accuracy of the type information at all), or else they should have gone all the way and generated a conventional import like this:
index.d.ts
import { CssSelector } from "./nested";
export declare const x: CssSelector;
In any case, our own projects would never encounter this issue, because we would never declare a public API without a type annotation. Our lint rules require the source code to look like this:
index.ts
import { foo, CssSelector } from "./nested";
export const x: CssSelector = foo(); // <--- properly typed variable
Compared to this, the original index.ts is arguably helping someone bang out code faster, at the expense of making the result more difficult for other people read and understand. Seems like great evidence in favor of this lint rule heheh.
API Extractor cannot emit import("./Async")
in its output, so if we want to fix this, we will need to finish the job that (in my view) was really the compiler's responsibility. We can do that, but first I'd like to determine whether the compiler is already planning to address this in an upcoming release. (@rbuckton @RyanCavanaugh ?)
@phryneas In the meantime, are you able to work around your issue by adding explicit type annotations for the relevant APIs? If not, then we'd definitely give this fix a higher priority.
You pointed me in the right direction to circumvent this bug - and pinpoint it in our case. In case you're interested, here's the commit that still encounters the bug, but I'll explain it below: https://github.com/phryneas/react-async/blob/0321c69f1bc4df61f89fa127045fbbb24add087f/packages/react-async/src/helpers.tsx
In this case, we have this method signature:
export const IfFulfilled = <T extends {}>({
children,
persist,
state = {} as any,
}: {
children?: FulfilledChildren<T>
persist?: boolean
state: AsyncState<T>
}) => (
<>{state.isFulfilled || (persist && state.data) ? renderFn(children, state.data, state) : null}</>
)
Now, AsyncState
takes an optional second parameter:
export declare type AsyncState<T, S extends AbstractState<T> = AbstractState<T>> = /* ... */
So the usage of AsyncState
above has an implicit reference to AbstractState
(which is not imported). And tsc
doesn't leave it at that implicit reference, but makes it explicit by import()
ing AbstractState, so something like this is generated AsyncState<T, import("./Async").AbstractState<T>>;
.
And that leads to the whole problem.
Now, the solution is almost stupid: I only import AbstractState
- I don't even need to use it. So, this diff for now solved all our problems: https://github.com/phryneas/react-async/commit/70393af94ddd030de56bfcadcd17f00e6762dfa9
But still, I guess that there are many more cases like this, where tsc
will emit import()
statements - I guess this issue will keep popping back up with the weirdest cases :/
For references, this blog article describes some more use cases and motivation for "import()
types" (which I think is the official name for this feature).
Things break on this PR: https://github.com/ScaleLeap/amazon-mws-api-sdk/pull/47
Things is, TS itself is generating this type. I have not written that manually.
export declare class Sellers {
private httpClient;
constructor(httpClient: HttpClient);
listMarketplaceParticipations(): Promise<[MarketplaceParticipations, RequestMeta]>;
listMarketplaceParticipationsByNextToken(nextToken: NextToken<'ListMarketplaceParticipations'>): Promise<[MarketplaceParticipations, RequestMeta]>;
getServiceStatus(): Promise<[{
Status: import("../parsing").ServiceStatus;
Timestamp: string;
}, RequestMeta]>;
}
@moltar just as a workaround, do an import {ServiceStatus} from '../parsing'
somewhere in the file that is coming from (you don't have to use it, it just has to be there) and it prevents the automatic import. Still not a good long-term solution though.
After implementing the error on import() type encounter, I thought about the next milestone: adding support for import() types.
The plan is to follow the same approach as rollup-plugin-dts.
// For import type nodes of the form
// `import("./foo").Bar`
// we create the following ESTree equivalent:
// 1. `import * as _ from "./foo";` on the toplevel
// 2. `_.Bar` in our declaration scope
convertImportTypeNode(node: ts.ImportTypeNode) {
...
}
First, _tryMatchImportDeclaration
should find the import() type and create an AstImport, which is easy. Second, the rollup method _modifySpan
should find again the import() type, lookup the previous AstImport and modify the Span with information derived from the AstImport (nameForEmit).
But I got stuck. The dtsRollup can only match Identifiers to AstImports, and import() types don't provide any Identifier. The core of this functionality is implemented at src/analyzer/AstSymbolTable.ts
, in the function _fetchEntityForIdentifierNode().
Do two import() type statements with the same module path share the same Symbol?
E.g. import('abc').foo
and import('abc').bar
.
In that case AstSymbolTable can be modified to match Symbols to AstEntities. this._exportAnalyzer.fetchReferencedAstEntity()
can already match Symbol -> AstEntities.
If this is implemented, identifiers could be matched to AstEntities using this scheme "Identifier -> Symbol -> AstEntity", or AstSymbolTable could have 2 tables: "Identifier -> AstEntity" and "Symbol -> AstEntity".
Do two import() type statements with the same module path share the same Symbol? E.g.
import('abc').foo
andimport('abc').bar
.
I would not rely on that assumption. For example in https://github.com/microsoft/rushstack/issues/1874#issuecomment-631802141 we recently found a violation of a reasonable assumption about ts.Declaration
relationships that had always been true in the past.
(However if you follow the import to the imported module, that ts.Symbol
is likely to be reliable for comparison. The machine that we normally use for this is TypeScriptInternals.getImmediateAliasedSymbol()
. You can put a breakpoint here to see how it works. It is very similar to pressing F12 in VS Code -- it hops to the next alias along the way to the original declaration, allowing you to stop when you find a meaningful stopping point. I haven't tried it, but it would probably get you there, if the import()
symbol itself doesn't already provide this info somehow. But see below.)
First,
_tryMatchImportDeclaration
should find the import() type and create an AstImport, which is easy.
Hmm... This is actually the part that needs an equivalence relation, to avoid creating duplicate AstImport
instances. But it looks like we already do that a simpler way by using the import path string:
...with:
I bet this same idea will work for import()
-type AstImport
cases.
Second, the rollup method
_modifySpan
should find again the import() type, lookup the previous AstImport and modify the Span with information derived from the AstImport (nameForEmit).
The generator's lookup here is very dumb actually. It is essentially saying _"Hey, remember when we already analyzed this thing in AstSymbolTable._analyzeChildTree()? Give me back the entity that we found then."_ So the mapping really should happen in _analyzeChildTree()
long before the generator stage.
If there is no ts.Identifier
, you could choose some other syntax element such as the import()
node and generalize tryGetEntityForIdentifierNode()
to accept either node type.
If this is implemented, identifiers could be matched to AstEntities using this scheme "Identifier -> Symbol -> AstEntity",
This might work, but it makes me uneasy. The idea behind _analyzeChildTree()
is to force all the analysis to happen during a specific stage. For example, when the Collector
goes and renames symbols, we wouldn't want new names suddenly getting analyzed into existence that we thought were available names. Relying on the syntax ts.Identifier
(rather than the semantic ts.Symbol
) is like an assertion that this specific thing was supposed to have been analyzed earlier. Using a semantic mapping might weaken that assertion and hide bugs.
Another issue is that the mappings can have tricky edge cases. For example, "displaced symbols" get filtered out during the mapping. We should be able to forget about all that nonsense after _analyzeChildTree()
finishes its thing.
Again it might work. But I'd have to think about it to convince myself that it is correct for all cases. I'm not good at keeping lots of stuff in my head, so these proofs can be time-consuming (which is why https://github.com/microsoft/rushstack/pull/1796 is taking so long).
That said, your overall idea sounds correct and rather straightforward to implement. I had assumed it would require some kind of architectural change. It would be so awesome to add support for import()
!
I'm also reachable in the API Extractor chat room if you have more questions.
Thanks for your explanation @octogonz, it has been of great help. Now it's working, take a look at my branch import_type_support_simplicity.
Summary of how import() types are converted to star imports
_analyzeChildTree
links ImportType nodes to AstEntitiesfetchReferencedAstEntityFromImportTypeNode
tryGetEntityForIdentifierNode
has been refactored into a generic tryGetEntityForNode
_modifySpan
replaces the whole ImportTypeNode with the AstSymbol.nameForEmit
or builds a replacement token from a AstImport.StarImport
I also thought about another strategy: leave external references untouched as import() nodes if possible. The idea is to improve accuracy with respect to the original source. This attempt sits in branch import_type_support_accuracy.
It works, but there are some challenges to overcome. It is built on top of a new AstImport.ImportType
and these are the challenges:
import('y').a.b
. Should it be exportName='a.b'
exportName=['a', 'b']
or something else?type foo = import('y').a.b;
- Import() types may specify a namespace, such as
import('y').a.b
. Should it beexportName='a.b'
exportName=['a', 'b']
or something else?
It would probably be best for the AstImport
should represent import('y')
. Normally the .a.b
part is considered part of the usage of the imported thing. Consider this example:
For example:
import * as foo from "foo";
declare export function bar(): foo.a.b;
API Extractor considers .a.b
to be a detail of the signature for bar()
, not an aspect of the import.
https://github.com/microsoft/rushstack/issues/1754 is possible a dupe of this issue.
Hey guys, +1 for this issue. Our codebase relies heavily on TypeScript's type inference (so that developers aren't met with a wall of text when they open a source file), and it's been issue after issue trying to get API Extractor to work on all of our Rush repo's packages (which, btw, API Extractor is an awesome tool when it does work!)
Any progress on this though?
Just hit this on a particularly complicated type (conditional and infer
generic types) so adding my +1 to this as well.
+1. I encountered this issue too. It would be great to see import()
support!
+1 is useless, but we also waiting for fix. 🙂
Another use case which I didn't see mentioned above: pure-JS projects using TypeScript-compatible docstrings.
The analogue of a type
or interface
is the @typedef
tag.
If we want to reference a typedef Foo
defined in b.js
from module a.js
, we write e.g., @type {import('./b').Foo}
.
Using an appropriately-configured tsconfig.json
, we can use tsc
to generate declarations from these pure-JS sources (which actually works pretty well). The generated .d.ts
files contain these import()
-style references.
There is no alternative syntax we could use that I'm aware of, since these symbols only live in comments and cannot be import
'ed or require
'd.
I am interested in exploring api-extractor
to help document such projects (e.g., IBM/report-toolkit)--an alternative to typedoc
would be great--but I'm blocking on this issue.
(It's possible that the "real problem" is tsc
should actually eliminate import()
syntax from the .d.ts
files and use alternatives as mentioned here, but I don't know enough to say either way.)
+1 here I'm planning on using this to generate api json files or .md files, and blocked here. hope this can be solved soon~~~~
+1 I am also encountering this, and it is blocking integration into some of our packages.
@octogonz is anyone currently working on a fix for this?
@lifeiscontent I believe that PR https://github.com/microsoft/rushstack/pull/1916 is a partial fix for this problem. Could you try that branch and see if it works for your scenario? We've started a new effort to get these old PRs finally merged, so if PR #1916 works for you, we could prioritize that one.
@octogonz is there a way I can npm install that PR?
Instructions for compiling and running the branch are here: https://api-extractor.com/pages/contributing/building/ https://api-extractor.com/pages/contributing/debugging/
If that doesn't work, tomorrow I can publish a PR build for you.
@lifeiscontent We've published the PR branch as @microsoft/api-extractor@7.13.2-pr1916.0
. Please give it a try.
@microsoft/api-extractor@7.13.2-pr1916.0
works for vue-router-next!
@microsoft/api-extractor@7.13.2-pr1916.0
Can confirm, works for me too. 🎉
@microsoft/api-extractor@7.13.2-pr1916.0
Not installable with yarn, because the shipped package.json
contains dependencies with workspace
scheme, which is non standard...
@jsamr Yeah I ran into the same issue, ended up cloning the repo locally, fixing the package.json, and installing as a local dependency.
@microsoft/api-extractor@7.13.2-pr1916.0
Not installable with yarn, because the shippedpackage.json
contains dependencies withworkspace
scheme, which is non standard...
installable with Yarn1/2/3:
"resolutions": {
"@microsoft/api-extractor-model": "*",
"@rushstack/node-core-library": "*",
"@rushstack/rig-package": "*",
"@rushstack/ts-command-line": "*",
"@rushstack/eslint-config": "*"
},
Fixed by PR https://github.com/microsoft/rushstack/pull/1916 from @javier-garcia-meteologica
🎈 Released with API Extractor 7.18.0
That did the trick, thanks so much! 🙏
Lazy types are not being rewired when using dtsRollup.
index.d.ts
nested.d.ts
output
I'd expect that the lazy type is resolved and appended to the file and the import is removed.
expected output
version: 7.0.11