Gerrit0 / typedoc-plugin-missing-exports

MIT License
44 stars 4 forks source link

Using glob entry point doesn't assign `<internal>` to individual entry points #22

Closed ixTec closed 8 months ago

ixTec commented 9 months ago

Given the following folder structure, for a library of components:

src
│  ComponentA
    ├── index.ts
    ├── componentA-file1.tsx
    └── componentA-file2.tsx
│  ComponentB
    ├── index.ts
    ├── componentB-file1.tsx
    └── componentB-file2.tsx
│ ComponentC
    └── ...
│ ComponentD

I have exported types in each index.ts, but these are complex types made up from internal types in the component*-file*.tsx files, which are not directly exported in those files. E.g:

Example index.ts file:

export type { ButtonProps } from './componentA-file1.tsx';

Example componentA-file1.tsx file:

type ButtonIconOnlyProps = {
  'aria-label': string;
  children?: never;
  icon: JSX.Element;
};

type ButtonNoIconProps = {
  'aria-label'?: string;
  children: React.ReactNode;
  icon?: never;
};

export type ButtonProps = ButtonIconOnlyProps | ButtonNoIconProps>;

When using the following in a typedoc.json config

"entryPoints": ["./src/**/index.ts"],
"entryPointStrategy": "expand",
"plugin": ["typedoc-plugin-missing-exports"]

All of the generated <internal> type info for all files is appended to the last child in the glob entry point (in the above example, this would be ComponentD). I would expect them to be appended to the individual entries instead (e.g. ComponentA, ComponentB etc). The type information generated from the index.ts files correctly links to the <internal> references, it's just not under the correct module.

Any help would be greatly appreciated here, as I may be missing something vital.

Gerrit0 commented 9 months ago

I'm not seeing this behavior... using this repo, I built the plugin, then ran:

npx typedoc --tsconfig test/packages/tsconfig.json 'test/packages/*/index.ts' --plugin ./index.js

This should be effectively the same, as the plugin only cares about what reflections with a Module kind are created for entry points... and indeed, I get an <internal> namespace in each entry point's module.

ixTec commented 9 months ago

After some further investigation, some of my types are a little more complex, as some are polymorphic, and it only seems to be an issue with those complex ones.

I'll share a better example next week, but may have to adjust my typings too

ixTec commented 9 months ago

I've confirmed through further testing that this issue is only applicable to our polymorphic components and our regular ones work fine.

Here's a more detailed example, for one of our polymorphic components, with a forwarded ref (still a button, as we have two versions of it):

types.tsx

Some types to help with polymorphic typings (shared with other components too)

export type PolymorphicRef<T extends React.ElementType> =
  React.ComponentPropsWithRef<T>['ref'];

export type PolymorphicComponentProps<
  T extends React.ElementType,
  P extends Record<string, unknown>,
> = Omit<React.ComponentPropsWithoutRef<T>, keyof P> &
  P & { as?: T; ref?: PolymorphicRef<T> };

button-internal.tsx

I've removed the content of the functional component here, as it's irrelevant.

import type { PolymorphicComponentProps, PolymorphicRef } from '../helpers';

type ButtonBaseProps = {
  size?: 'large' | 'medium' | 'small';
  variant?: 'destructive' | 'plain' | 'primary' | 'secondary' | 'upgrade';
};

type ButtonIconOnlyProps = ButtonBaseProps & {
  'aria-label': string;
  children?: never;
  icon: JSX.Element;
};

type ButtonNoIconProps = ButtonBaseProps & {
  'aria-label'?: string;
  children: React.ReactNode;
  icon?: never;
};

export type ButtonProps<T extends React.ElementType = 'button'> =
  PolymorphicComponentProps<T, ButtonIconOnlyProps | ButtonNoIconProps>;

function ButtonInternal<T extends React.ElementType = 'button'>(
 { ... }: ButtonProps<T>,
  ref?: PolymorphicRef<T>,
) {
 return ( ... );
}

export default ButtonInternal;

button.tsx

import { forwardRef } from 'react';

import type { ButtonProps } from './button-internal';
import ButtonInternal from './button-internal';

type ButtonComponent = <T extends React.ElementType = 'button'>(
  props: ButtonProps<T>,
) => JSX.Element | null;

const Button: ButtonComponent = forwardRef(ButtonInternal);

export default Button;

index.ts

export type { ButtonProps } from './button-internal';
export { default as Button } from './button';

In addition, when viewing the generated output, I can see that typedoc correctly identifies ButtonProps under Button and that the internal types are defined in src/button/button-internal.tsx, but ButtonBaseProps, ButtonIconOnlyProps and ButtonNoIconProps are shown in the tree under the last glob entry point, e.g. AnotherComponent/<internal>/ButtonIconOnlyProps

Here's the full typedoc.json:

{
  "$schema": "https://typedoc.org/schema.json",
  "entryPoints": ["./src/**/index.ts"],
  "out": "doc",
  "json": "typedoc-output.json",
  "includeVersion": true,
  "logLevel": "Verbose",
  "entryPointStrategy": "expand",
  "excludeReferences": true,
  "plugin": ["typedoc-plugin-missing-exports", "typedoc-plugin-mdn-links"]
}
Gerrit0 commented 8 months ago

It looks like that isn't enough to reproduce - can you add code for another component, or even better, set up a minimal repo I can clone?

ixTec commented 8 months ago

I've created a reproduction here https://github.com/ixTec/typedoc-reproduce - just run yarn and yarn build for the output.

The problematic types are assigned to Tag in the output (the last component/entry point) and this is my issue.

Gerrit0 commented 8 months ago

Thanks for the awesome repro! It still took me ~30 minutes to actually understand what was going on, but that made it infinitely easier.

The polymorphic thing turned out to be a red herring. What actually matters is whether the originally referencing reflection is re-exported or not. If it isn't re-exported, TypeDoc will convert it when first going through modules. If it's re-exported, on the other hand, TypeDoc will do it on a second pass through modules. TypeDoc does this so that if a symbol is exported from multiple entry points, its primary documentation will be present in the module where it is directly exported from, if such a module exists.

This causes a problem for this plugin because I completely forgot about this behavior when implementing the plugin, and assumed that all children of a module were added before the next module created. Thus, the last module is considered "active" for the second pass of all entry points. Pretty easy fix, just need to walk the reflection tree to determine the active module rather than saving it in a variable.

ixTec commented 8 months ago

Fantastic, thank you! Just tested and works perfectly now 😄