Skip to content
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

[docs-infra] Simplify API sections typing #43128

Merged
merged 20 commits into from
Aug 8, 2024

Conversation

alexfauquette
Copy link
Member

@alexfauquette alexfauquette commented Jul 31, 2024

Follow up of #43149

Previously

In the previous PR, The section props (PropertiesSection, ClassesSection, and SlotsSection) have been typed base on the @mui-internal/api-docs-builder types to enforce consistency.

In This PR

Section types

The sections now define the items they will render independently from the api-docs-builder with interfaces ClassDefinition, SlotDefinition, and PropertyDefinition. They come with processor that map slot, classes, props, and hooks params and return values to the appropriate definition

 <PropertiesSection
-    properties={parameters}
-    propertiesDescriptions={parametersDescriptions}
-    componentName={hookName}
-    hooksParameters
+    properties={hookApiProcessor({ // Or propsApiProcessor, or interfaceApiProcessor depending on the section to display.
+      kind: 'parameters',
+      hookName,
+      properties: parameters,
+      translations: parametersDescriptions,
+    })}
    level="h3"
    title="api-docs.parameters"
    titleHash={`${hookNameKebabCase}-parameters`}
/>

This migration is optional. See for example the typing of ClassesSectionProps. If the props are not the new ones, the processor will be applied by the ClassesSection. One other repository (I think only X is impacted) we will be able to remove those old options.

export type ClassesSectionProps = (
  | {
      classes: ClassDefinition[];
      componentClasses?: undefined;
      classDescriptions?: undefined;
      componentName?: undefined;
    }
  | {
      classes: undefined;
      componentClasses: ComponentClassDefinition[];
      classDescriptions: PropsTranslations['classDescriptions'];
      componentName: string;
    }
) & { ... }

Table of contents

The generation of the ToC can be done based in those ClassDefinition, SlotDefinition, and PropertyDefinition.

But because the MarkdownV2 which defines its ToC before rendering the Sections, I kept the old one and marked them as deprecated

Table of content entries are now typed

@alexfauquette alexfauquette added typescript scope: docs-infra Specific to the docs-infra product labels Jul 31, 2024
@mui-bot
Copy link

mui-bot commented Jul 31, 2024

Netlify deploy preview

https://deploy-preview-43128--material-ui.netlify.app/

Bundle size report

No bundle size changes (Toolpad)
No bundle size changes

Generated by 🚫 dangerJS against 2138bda


const [displayOption, setDisplayOption] = useApiPageOption(layoutStorageKey, defaultLayout);
const formatedProperties = Object.entries(properties)
.filter(([, propData]) => propData.description !== '@ignore')
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you take a close look, you will notice this line is removed in the tsx files. That's because the description property does not exist in the typing.

I will need to double-check with all components using this. But for example, base uses the @ignore on some props, and they never reach that place.

@@ -96,7 +107,9 @@ export interface TranslateOptions {
ignoreWarning?: boolean;
}

export function useTranslate() {
export type Translator = (key: string, options?: TranslateOptions) => string;
Copy link
Member Author

@alexfauquette alexfauquette Jul 31, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added to simplify the typing when using translator in helper functions

@alexfauquette alexfauquette changed the title [docs-infra] Move API pages to TS [docs-infra][PoC] Move API pages to TS Aug 1, 2024
@Janpot Janpot self-requested a review August 1, 2024 12:21
@Janpot
Copy link
Member

Janpot commented Aug 1, 2024

Having a typing contract

top-notch. I think the strategy makes a lot of sense. add types => refactor => extract. Similar approach can be done between the pages and the output of the markdown generator. Or anywhere else we bridge between build time and runtime.

Make API Sections more independent from scripts results

I'd love for those {{...}} interpolations to some day be moved to MDX. In that sense, it would be helpful if we can already make them look more like rendering a component. Not necessarily in terms of JSX syntax, but in terms of how properties are defined

{{"component": "./SomeComponent", "prop1": "foo", "prop2": 123}}

Is spiritually equivalent to

import SomeComponent from "./SomeComponent"

// ...

<SomeComponent prop1='foo' prop2={123} />

@alexfauquette
Copy link
Member Author

alexfauquette commented Aug 1, 2024

I'd love for those {{...}} interpolations to some day be moved to MDX. In that sense, it would be helpful if we can already make them look more like rendering a component. Not necessarily in terms of JSX syntax, but in terms of how properties are defined

@Janpot Not sure to understand your point on this one.

The API pages are not markdown rendered

The page file fetch the JSON and render the ApiPage component with the JSON data.

@Janpot
Copy link
Member

Janpot commented Aug 2, 2024

The API pages are not markdown rendered

Ah yes ofcourse.

Long term we could perhaps think about making the constituents composable in markdown pages? e.g. due to complexity in the types we're at the moment writing some tables by hand in Toolpad that look a lot like these property tables.

@github-actions github-actions bot added the PR: out-of-date The pull request has merge conflicts and can't be merged label Aug 2, 2024
@github-actions github-actions bot removed the PR: out-of-date The pull request has merge conflicts and can't be merged label Aug 2, 2024
Comment on lines 173 to 172
t,
componentName: pageContent.name,
componentProps,
inheritance,
themeDefaultProps: pageContent.themeDefaultProps,
}),
createTocEntry('props'),
componentSlots?.length > 0 && createTocEntry('slots'),
...getClassesToC({
t,
componentName: pageContent.name,
componentClasses,
}),
createTocEntry('classes'),
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to bring that feature back. I initially removed it because I remembered that with the table layout it was useless, but did not manage to find back this conclusion on github

properties={componentProps}
propertiesDescriptions={propDescriptions}
componentName={pageContent.name}
properties={propsApiProcessor({
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the new way of defining API pages, such that if someone need to pass other kind of data, they can do it

The previous one should still work the time of the migration

{classes.map((classDefinition) => {
const { className, key, description, isGlobal, isDeprecated, deprecationInfo } =
const { hash, className, key, description, isGlobal, isDeprecated, deprecationInfo } =
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generating the hash is now the responsibility of the processors.

Comment on lines 101 to 105
// interface InterfaceApiProcessorParams {}

// export function InterfaceApiProcessor(params: InterfaceApiProcessorParams): PropertyDefinition[] {
// return [];
// }
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO on X side

@alexfauquette alexfauquette changed the title [docs-infra][PoC] Move API pages to TS [docs-infra] Simplify API sections typing Aug 6, 2024
@alexfauquette alexfauquette marked this pull request as ready for review August 6, 2024 09:58
Copy link
Member

@Janpot Janpot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two minor comments about naming and type-safety. The latter wasn't introduced in this PR, just something I noticed. They're just suggestions, no blockers.

componentName: string;
}

export function classesApiProcessor(params: ClassesApiProcessorParams): ClassDefinition[] {
Copy link
Member

@Janpot Janpot Aug 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a blocker at all, just some reflections from reviewing this but I find the naming confusing. a "processor" is a noun, for a function I usually expect a verb. The meaning of the word "processor" is quite broad, it can mean anything.
For functions that transform one data type to another, I'd prefer to choose a name that includes both types. e.g. mapXtoY(x), xToY(x), yFromX(x). I like the latter as it mimics javascript API design (Object.fromEntries(), Array.from(), String.fromCharCode(), String.fromCodePoint()). I realize those aren't verbs neither, but the from/to clearly indicates a transformation.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Effectively I was not super happy with those namings

About mapXtoY naming, my struggle is that the X is not easy to name since it's a mix of descriptions and translations. So I went with a getter naming get[Yyy]ApiDefinitions

...classDefinition,
description:
classDescriptions[classDefinition.key]?.description
?.replace(/{{conditions}}/, classDescriptions[classDefinition.key].conditions!)
Copy link
Member

@Janpot Janpot Aug 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any way to get rid of these non-null assertion operators? It removes type safety and an upstream bug can be introduced that essentially turns this replacement into 'undefined' without a warning.
If it's hard to guarantee type-wise and have this as a compile time error, we could add an invariant() call to turn this into a build time error instead of quietly starting to replace strings with 'undefined' at run time.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went with the following solution

    if (!conditions && description.search(/{{conditions}}/) !== -1) {
      throw Error(
        `In ${componentName} the class "${classDefinition.className}" description with "{{conditions}}" but without \`conditions\` to replace it.`,
      );
    }

It adds the runtime error, but does not fix the typing, because the fact that conditions is defined depends on the presence of {{conditions}} in the description, which might be hard to define with types

Copy link
Member

@Janpot Janpot Aug 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The way I would write this is to replace only within the check of whether the description contains {{conditions}}

// (use .includes())
if (!conditions && description.includes(/{{conditions}}/)) {
  throw new Error('...')
}

// This is suboptimal, a type checker can't relate the previous condition with the following replacement

return {
  // we're replacing, even when we know there is nothing to replace
  description: description.replace(/{{conditions}}/, conditions!),
}

The solution requires a refactor

// make description a mutable variable

if (description.includes(/{{conditions}}/)) {
  // Use an invariant to assert the condition, this would be a programming error
  // (don't use invariant when an error comes from user input)
  invariant(conditions, 'conditions should be defined if description includes {{conditions}}')
  // only replace when it makes sense
  descripton = description.replace(/{{conditions}}/, conditions)
}

return {
  description
}

It's cleaner and more defensive imo. non-null assertion is always a code smell. I have never seen a use-case that isn't better solved with a refactor or an invariant. But again, this is all polish. Doesn't have to be in scope for this PR.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the detailed explanation 👍

);
}

if (!conditions && description.search(/{{nodeName}}/) !== -1) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (!conditions && description.search(/{{nodeName}}/) !== -1) {
if (!nodeName && description.search(/{{nodeName}}/) !== -1) {

Copy link
Member

@Janpot Janpot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clean

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
scope: docs-infra Specific to the docs-infra product typescript
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants