-
Notifications
You must be signed in to change notification settings - Fork 607
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] just "don't export" @internal types instead of stripping them #1664
Comments
What you described is the intended behavior. Maybe you found a bug? Could you share a repro? |
I've created a branch with the build artefacts of https://github.com/phryneas/redux-toolkit/tree/api-extractor-repo Here is the entry point and this is the rollup For example, |
This seems like a bug. I was able to reduce your repro down to this: /** @public */
export declare function f(x: X): void;
/** @internal */
export declare class X { } ...which produces a warning:
...but the resulting .d.ts rollup produces a compiler error: /**
* @public
*/
export declare function f(x: X): void; // <-- TS2304: Cannot find name "X"
/* Excluded from this release type: X */ I thought we had logic to handle this case, but apparently not. We would be unlikely to encounter this issue in our own projects, since we wouldn't allow |
I had feared as much - I seem to have a talent of uncovering api-extractor bugs ;) I know this is a bit inappropriate to ask, but seeing that I don't have a smidgen of the understanding necessary to file a PR on this myself (heck, I don't even get it to build right now on my system): Can you give me a general ETA on when a fix for this could be landing in api-extractor? |
Fixing this compiler error makes sense. The TypeScript compiler today is much more strict about reporting errors in .d.ts files. Avoiding compile errors should be a higher priority than honoring trimming requests. We could solve it like this: Before trimming a given
@phryneas Here you are maybe asking for something extra: It sounds like you want API Extractor to change an exported declaration to be a local symbol, like this: /** @public */
export declare function f(x: X): void;
/** @internal */
/* export */ declare class X { } This is a more complicated request. It's almost like a different feature:
The original intended idea behind It can also be used to hide file-level exports (e.g. APIs that are used by the internal implementation within a project). We've been considering to introduce a separate But both scenarios could be handled by trimming (deleting declarations), not by undoing exports. So coming back to your request: If you don't intend for others to consume the types, why export them from your library entry point? Is it simply a matter of convenience, that you want to write In other words, is it an okay workaround to simply fix up your |
Yeah, I guess this really is a convenience thing for us - so if this is really that complicated to implement, fixing up our It was just something that I had assumed |
Oh, and generally: thanks for your super-fast reaction and looking into this. I really appreciate that 🤗 |
If I get some more time over the holiday, I'll think about this algorithm some more. It may turn out to be relatively easy to implement, at least for the cases people are most likely to encounter.
It was an interesting question. :-) |
This algorithm leads to a graph theory problem:
Example: /** @internal=1 */
interface X { // declared: 1, transitive: 4
z: Z;
}
/** @alpha=2 */
interface Y { // declared: 2, transitive: 2
x: X;
}
/** @beta=3 */
interface Z { // declared: 3, transitive: 4
x: X;
}
/** @public=4 */
function f(): Z; // declared: 4, transitive: 4 Challenge: Given a graph and the declared visibility for each node, find an efficient algorithm to calculate the transitive visibility of all nodes in a graph. |
Hm. I'm not getting better than
This should be a "visit every node once, extending to each of their successors every time" algorithm, |
Cool!
How do you handle cycles?
Is that true? For example, what if the graph was 100 nodes organized into a linked list. I posted this question on Twitter. Seems like there should be an |
Oh, I wasn't 100% clear on that: I only look at direct successors and never traverse down further.
Then we would look at every node once as a predecessor and once as a successor (except the first one of your linked list). 100 nodes, 99 edges, 199 "node visits".
Assuming that |
Just for fun, an implementation that probably illustrates it better than I can describe it. With your example of 100 nodes in a linked-list-like graph ;) enum Level {
internal,
alpha,
beta,
public
}
interface GraphNode {
declaredVisibility: Level
transitiveVisibility: Level
children: GraphNode[]
}
const buckets = [
new Set<GraphNode>(), // internal
new Set<GraphNode>(), // alpha
new Set<GraphNode>(), // beta
new Set<GraphNode>() // public
] as const
const nodes: Array<GraphNode> = []
for (let i = 0; i < 100; i++) {
nodes.push({
declaredVisibility: i % 4,
transitiveVisibility: i % 4,
children: []
})
}
let lastNode: GraphNode | undefined
for (const node of nodes) {
if (lastNode) lastNode.children.push(node)
lastNode = node
}
// now nodes is a linked list of internal->alpha->beta->public->[repeat until 100 elements]
// let's begin the algorithm
// sort nodes into the four buckets
for (const node of nodes) {
buckets[node.transitiveVisibility].add(
Object.assign(node, { transitiveVisibility: node.declaredVisibility })
)
}
let steps = 0
// walking through these outer two for-loops will visit each node exactly once since each node will exist in only one bucket at any given time.
for (let level = Level.public; level >= Level.internal; level--) {
for (const predecessor of buckets[level].values()) { // values is an iterator, nodes added while in the loop will still be visited
// now we visit the edges - as each node is only visited once, each directional edge of the graph is only visited once
for (const successor of predecessor.children) {
steps++
if (successor.transitiveVisibility < level) {
// add to the current bucket
buckets[level].add(successor)
// remove from the original bucket (which has not been visited yet)
buckets[successor.transitiveVisibility].delete(successor)
successor.transitiveVisibility = level
}
}
}
}
console.log(
buckets.map((set, level) => `${Level[level]}: ${set.size}`),
`${steps} successor nodes visited`
)
// [ 'internal: 1', 'alpha: 1', 'beta: 1', 'public: 97' ] '99 successor nodes visited' |
Ahh okay, from your original description I didn't understand that the algorithm should stop iterating when it reaches a successor with // now we visit the edges - as each node is only visited once, each directional edge of the graph is only visited once
for (const successor of predecessor.children) {
steps++
if (successor.transitiveVisibility < level) { // <=== 👍 This early exit, combined with the bucket sort, I think makes your algorithm actually |
because of this I have to export all of my internal interfaces/classes and mark them as public. Is there any workaround ? |
Yeah, we got pretty lost in algorithmic fun there - @octogonz any chance that you can take a look at this? |
@ayZagen If you don't do an |
@phryneas Thank you very much, I have removed exports and I had to change {
"messages": {
"extractorMessageReporting": {
"ae-forgotten-export": {
"logLevel": "none"
}
}
} Exporting everything separately means lots of lines and would pollute the files in large project |
I very much would want to work on this. It would be fun to code. But other priorities have come along. For API Extractor, the priorities would be:
(If someone from the community wants to work on #1664, I would be happy to provide guidance.) |
Is there any progress here? I've got another case: after trimming, some referenced unexported entity still exists in the dts rollup result. e.g. declare interface A {}
export declare class B {
/**
* @internal
*/
prop: A;
} Which will produce: declare interface A {}
export declare class B {
/* Excluded from this release type */
}
export {} The declaration "A" is meaningless. And I think finding visibility will solve this case, too. |
After trying some solutions, I'm thinking that this may not be the responsibility of The purpose of keeping @internal types is: if we finally export a type (e.g. @public), we want to ensure that the type is valid with everything it refers existed. The attempt of keeping @internal types just fixes one case. There're other cases, e.g. referring to inner property of a type. Take an example: /** @internal */
export declare interface A {
/** @internal */
propA: number;
}
export declare class B {
/**
* @internal
*/
prop: A['propA'];
} To ensure type We might consider the first case as a normal one and support it, or we treat them the same: @internal/@beta trimming won't care who refers to it, its user's responsibility to mark @internal correctly to make the dts result valid. |
export declare class B {
/**
* @internal
*/
prop: A['propA'];
} This case feels fairly contrived. Although the TypeScript language provides some advanced syntaxes, arguably many of them are not good contracts for a public API signature. declare interface A {} // <--- implicitly @public
export declare class B {
/**
* @internal
*/
prop: A;
} Your above example seems different and should be its own GitHub issue: Because In summary, we have now discussed 3 distinct problems:
1 and 2 require the new reachability algorithm proposed above. 3 could be solved by modifying the existing As far as priority, I cannot say any of these would ever arise for the projects that I work on, given the way that we design APIs. But implementing a graph crawler sounds like a lot of fun! |
Is this a feature or a bug?
Please describe the actual behavior.
Currently,
api-extractor
strips all@internal
types from a@public
export.What is the expected behavior?
I would like
@internal
types to still be present in the dts-rollup, but not exported - so other@public
exported types can use them internally.Reason: we have helper types that are exported because they are used throughout the project, but we don't want those to be exposed to our end users so we can change these in the future without being afraid of breaking things for library consumers. Having a
ae-forgotten-export
warning for those is fine, but currently the resulting types just reference types that aren't there - which makes them completely useless.(And yes, those are in our entry point, because our entry point just does
export *
- after all, we monitor if we accidentally exported something we did not want to - thanks to api-extractor 😄. So we are just using@public
and@internal
to mark "what should be accessible for a library consumer". )The text was updated successfully, but these errors were encountered: