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

[code-infra] Refactor eslint import/no-cycle rule #42705

Merged
merged 8 commits into from
Jun 26, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -436,7 +436,10 @@ module.exports = {
],
},
],
'import/no-cycle': ['error', { ignoreExternal: true }],
// TODO: Consider setting back to `ignoreExternal: true` when the expected behavior is fixed:
Copy link
Member

Choose a reason for hiding this comment

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

But do we want it to be true? Or was it only at true because we thought back then it would improve performance?

Copy link
Member Author

Choose a reason for hiding this comment

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

Well, as you mentioned, and they specify in the readme:

Its value is false by default, but can be set to true for reducing total project lint time, if needed.

// https://github.com/import-js/eslint-plugin-import/issues/2348#issuecomment-1587320057
LukasTy marked this conversation as resolved.
Show resolved Hide resolved
// Reevaluate when https://github.com/import-js/eslint-plugin-import/pull/2998 is merged.
LukasTy marked this conversation as resolved.
Show resolved Hide resolved
'import/no-cycle': ['error', { ignoreExternal: false }],
Copy link
Member

@Janpot Janpot Jun 26, 2024

Choose a reason for hiding this comment

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

nit: If the default is false, maybe it makes sense to just remove the options altogether?

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've added a todo comment regarding this: 5e32a4c
Ideally, it should be true and hopefully it will be fixed at some point. 🤔
WDYT?

Copy link
Member

Choose a reason for hiding this comment

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

I think it depends on the impact of import-js/eslint-plugin-import#2998
If after this change the difference between true and false is marginal, it could stay on false.

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 added a mention of this PR in the comment for more context.

},
},
{
Expand Down
3 changes: 1 addition & 2 deletions packages/api-docs-builder-core/baseUi/generateApiLinks.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import kebabCase from 'lodash/kebabCase';
import { ReactApi as ComponentReactApi } from '@mui-internal/api-docs-builder/ApiBuilders/ComponentApiBuilder';
import { ReactApi as HookReactApi } from '@mui-internal/api-docs-builder/ApiBuilders/HookApiBuilder';
import { ComponentReactApi, HookReactApi } from '@mui-internal/api-docs-builder';

/**
* Generates the api links, in a format that would point to the appropriate API tab
Expand Down
113 changes: 18 additions & 95 deletions packages/api-docs-builder/ApiBuilders/ComponentApiBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,11 @@ import kebabCase from 'lodash/kebabCase';
import remark from 'remark';
import remarkVisit from 'unist-util-visit';
import type { Link } from 'mdast';
import { defaultHandlers, parse as docgenParse, ReactDocgenApi } from 'react-docgen';
import { defaultHandlers, parse as docgenParse } from 'react-docgen';
import { renderMarkdown } from '@mui/internal-markdown';
import { ComponentClassDefinition } from '@mui/internal-docs-utils';
import { parse as parseDoctrine, Annotation } from 'doctrine';
import { ProjectSettings, SortingStrategiesType } from '../ProjectSettings';
import { ComponentInfo, toGitHubPath, writePrettifiedFile } from '../buildApiUtils';
import { toGitHubPath, writePrettifiedFile } from '../buildApiUtils';
import muiDefaultPropsHandler from '../utils/defaultPropsHandler';
import parseTest from '../utils/parseTest';
import generatePropTypeDescription, { getChained } from '../utils/generatePropTypeDescription';
Expand All @@ -23,90 +22,11 @@ import createDescribeableProp, {
} from '../utils/createDescribeableProp';
import generatePropDescription from '../utils/generatePropDescription';
import { TypeScriptProject } from '../utils/createTypeScriptProject';
import parseSlotsAndClasses, { Slot } from '../utils/parseSlotsAndClasses';
import parseSlotsAndClasses from '../utils/parseSlotsAndClasses';
import generateApiTranslations from '../utils/generateApiTranslation';
import { sortAlphabetical } from '../utils/sortObjects';

export type AdditionalPropsInfo = {
cssApi?: boolean;
sx?: boolean;
slotsApi?: boolean;
'joy-size'?: boolean;
'joy-color'?: boolean;
'joy-variant'?: boolean;
};

export type SeeMore = { description: string; link: { text: string; url: string } };

export interface ReactApi extends ReactDocgenApi {
demos: ReturnType<ComponentInfo['getDemos']>;
EOL: string;
filename: string;
apiPathname: string;
forwardsRefTo: string | undefined;
inheritance: ReturnType<ComponentInfo['getInheritance']>;
/**
* react component name
* @example 'Accordion'
*/
name: string;
muiName: string;
description: string;
spread: boolean | undefined;
/**
* If `true`, the component supports theme default props customization.
* If `null`, we couldn't infer this information.
* If `undefined`, it's not applicable in this context, for example Base UI components.
*/
themeDefaultProps: boolean | undefined | null;
/**
* result of path.readFileSync from the `filename` in utf-8
*/
src: string;
classes: ComponentClassDefinition[];
slots: Slot[];
propsTable: _.Dictionary<{
default: string | undefined;
required: boolean | undefined;
type: { name: string | undefined; description: string | undefined };
deprecated: true | undefined;
deprecationInfo: string | undefined;
signature: undefined | { type: string; describedArgs?: string[]; returned?: string };
additionalInfo?: AdditionalPropsInfo;
seeMoreLink?: SeeMore['link'];
}>;
/**
* Different ways to import components
*/
imports: string[];
translations: {
componentDescription: string;
deprecationInfo: string | undefined;
propDescriptions: {
[key: string]: {
description: string;
requiresRef?: boolean;
deprecated?: string;
typeDescriptions?: { [t: string]: string };
seeMoreText?: string;
};
};
classDescriptions: {
[key: string]: {
description: string;
conditions?: string;
nodeName?: string;
deprecationInfo?: string;
};
};
slotDescriptions?: { [key: string]: string };
};
/**
* The folder used to store the API translation.
*/
apiDocsTranslationFolder?: string;
deprecated: true | undefined;
}
import { AdditionalPropsInfo, ComponentReactApi } from '../types/ApiBuilder.types';
import { Slot, ComponentInfo } from '../types/utils.types';

const cssComponents = ['Box', 'Grid', 'Typography', 'Stack'];

Expand All @@ -118,7 +38,7 @@ const cssComponents = ['Box', 'Grid', 'Typography', 'Stack'];
* this method.
*/
export async function computeApiDescription(
api: { description: ReactApi['description'] },
api: { description: ComponentReactApi['description'] },
options: { host: string },
): Promise<string> {
const { host } = options;
Expand Down Expand Up @@ -151,7 +71,7 @@ export async function computeApiDescription(
* * - [Icon API](https://mui.com/api/icon/)
*/
async function annotateComponentDefinition(
api: ReactApi,
api: ComponentReactApi,
componentJsdoc: Annotation,
projectSettings: ProjectSettings,
) {
Expand Down Expand Up @@ -372,7 +292,7 @@ function extractClassCondition(description: string) {
const generateApiPage = async (
apiPagesDirectory: string,
importTranslationPagesDirectory: string,
reactApi: ReactApi,
reactApi: ComponentReactApi,
sortingStrategies?: SortingStrategiesType,
onlyJsonFile: boolean = false,
layoutConfigPath: string = '',
Expand Down Expand Up @@ -471,11 +391,11 @@ const generateApiPage = async (
};

const attachTranslations = (
reactApi: ReactApi,
reactApi: ComponentReactApi,
deprecationInfo: string | undefined,
settings?: CreateDescribeablePropSettings,
) => {
const translations: ReactApi['translations'] = {
const translations: ComponentReactApi['translations'] = {
componentDescription: reactApi.description,
deprecationInfo: deprecationInfo ? renderMarkdown(deprecationInfo) : undefined,
propDescriptions: {},
Expand Down Expand Up @@ -539,10 +459,13 @@ const attachTranslations = (
reactApi.translations = translations;
};

const attachPropsTable = (reactApi: ReactApi, settings?: CreateDescribeablePropSettings) => {
const attachPropsTable = (
reactApi: ComponentReactApi,
settings?: CreateDescribeablePropSettings,
) => {
const propErrors: Array<[propName: string, error: Error]> = [];
type Pair = [string, ReactApi['propsTable'][string]];
const componentProps: ReactApi['propsTable'] = _.fromPairs(
type Pair = [string, ComponentReactApi['propsTable'][string]];
const componentProps: ComponentReactApi['propsTable'] = _.fromPairs(
Object.entries(reactApi.props!).map(([propName, propDescriptor]): Pair => {
let prop: DescribeablePropDescriptor | null;
try {
Expand Down Expand Up @@ -599,7 +522,7 @@ const attachPropsTable = (reactApi: ReactApi, settings?: CreateDescribeablePropS
}
}

let signature: ReactApi['propsTable'][string]['signature'];
let signature: ComponentReactApi['propsTable'][string]['signature'];
if (signatureType !== undefined) {
signature = {
type: signatureType,
Expand Down Expand Up @@ -699,7 +622,7 @@ export default async function generateComponentApi(
}

const filename = componentInfo.filename;
let reactApi: ReactApi;
let reactApi: ComponentReactApi;

if (componentInfo.isSystemComponent) {
try {
Expand Down
93 changes: 14 additions & 79 deletions packages/api-docs-builder/ApiBuilders/HookApiBuilder.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { readFileSync, writeFileSync } from 'fs';
import path from 'path';
import * as ts from 'typescript';
import { Symbol, isPropertySignature } from 'typescript';
import * as astTypes from 'ast-types';
import * as _ from 'lodash';
import * as babel from '@babel/core';
import traverse from '@babel/traverse';
import { defaultHandlers, parse as docgenParse, ReactDocgenApi } from 'react-docgen';
import { defaultHandlers, parse as docgenParse } from 'react-docgen';
import kebabCase from 'lodash/kebabCase';
import upperFirst from 'lodash/upperFirst';
import { parse as parseDoctrine, Annotation } from 'doctrine';
Expand All @@ -15,91 +15,26 @@ import { computeApiDescription } from './ComponentApiBuilder';
import {
getSymbolDescription,
getSymbolJSDocTags,
HookInfo,
stringifySymbol,
toGitHubPath,
writePrettifiedFile,
} from '../buildApiUtils';
import { TypeScriptProject } from '../utils/createTypeScriptProject';
import generateApiTranslations from '../utils/generateApiTranslation';

interface ParsedProperty {
name: string;
description: string;
tags: { [tagName: string]: ts.JSDocTagInfo };
required: boolean;
typeStr: string;
}
import { HookReactApi, ParsedProperty } from '../types/ApiBuilder.types';
import { HookInfo } from '../types/utils.types';

const parseProperty = async (
propertySymbol: ts.Symbol,
propertySymbol: Symbol,
project: TypeScriptProject,
): Promise<ParsedProperty> => ({
name: propertySymbol.name,
description: getSymbolDescription(propertySymbol, project),
tags: getSymbolJSDocTags(propertySymbol),
required: !propertySymbol.declarations?.find(ts.isPropertySignature)?.questionToken,
required: !propertySymbol.declarations?.find(isPropertySignature)?.questionToken,
typeStr: await stringifySymbol(propertySymbol, project),
});

export interface ReactApi extends ReactDocgenApi {
demos: ReturnType<HookInfo['getDemos']>;
EOL: string;
filename: string;
apiPathname: string;
parameters?: ParsedProperty[];
returnValue?: ParsedProperty[];
/**
* hook name
* @example 'useButton'
*/
name: string;
description: string;
/**
* Different ways to import components
*/
imports: string[];
/**
* result of path.readFileSync from the `filename` in utf-8
*/
src: string;
parametersTable: _.Dictionary<{
default: string | undefined;
required: boolean | undefined;
type: { name: string | undefined; description: string | undefined };
deprecated: true | undefined;
deprecationInfo: string | undefined;
}>;
returnValueTable: _.Dictionary<{
default: string | undefined;
required: boolean | undefined;
type: { name: string | undefined; description: string | undefined };
deprecated: true | undefined;
deprecationInfo: string | undefined;
}>;
translations: {
hookDescription: string;
deprecationInfo: string | undefined;
parametersDescriptions: {
[key: string]: {
description: string;
deprecated?: string;
};
};
returnValueDescriptions: {
[key: string]: {
description: string;
deprecated?: string;
};
};
};
/**
* The folder used to store the API translation.
*/
apiDocsTranslationFolder?: string;
deprecated: true | undefined;
}

/**
* Add demos & API comment block to type definitions, e.g.:
* /**
Expand All @@ -112,7 +47,7 @@ export interface ReactApi extends ReactDocgenApi {
* * - [useButton API](https://mui.com/base-ui/api/use-button/)
*/
async function annotateHookDefinition(
api: ReactApi,
api: HookReactApi,
hookJsdoc: Annotation,
projectSettings: ProjectSettings,
) {
Expand Down Expand Up @@ -308,12 +243,12 @@ async function annotateHookDefinition(
}

const attachTable = (
reactApi: ReactApi,
reactApi: HookReactApi,
params: ParsedProperty[],
tableName: 'parametersTable' | 'returnValueTable',
) => {
const propErrors: Array<[propName: string, error: Error]> = [];
const parameters: ReactApi[typeof tableName] = params
const parameters: HookReactApi[typeof tableName] = params
.map((p) => {
const { name: propName, ...propDescriptor } = p;
let prop: Omit<ParsedProperty, 'name'> | null;
Expand Down Expand Up @@ -355,7 +290,7 @@ const attachTable = (
},
};
})
.reduce((acc, curr) => ({ ...acc, ...curr }), {}) as unknown as ReactApi['parametersTable'];
.reduce((acc, curr) => ({ ...acc, ...curr }), {}) as unknown as HookReactApi['parametersTable'];
if (propErrors.length > 0) {
throw new Error(
`There were errors creating prop descriptions:\n${propErrors
Expand All @@ -376,8 +311,8 @@ const generateTranslationDescription = (description: string) => {
return renderMarkdown(description.replace(/\n@default.*$/, ''));
};

const attachTranslations = (reactApi: ReactApi, deprecationInfo: string | undefined) => {
const translations: ReactApi['translations'] = {
const attachTranslations = (reactApi: HookReactApi, deprecationInfo: string | undefined) => {
const translations: HookReactApi['translations'] = {
hookDescription: reactApi.description,
deprecationInfo: deprecationInfo ? renderMarkdown(deprecationInfo).trim() : undefined,
parametersDescriptions: {},
Expand Down Expand Up @@ -413,7 +348,7 @@ const attachTranslations = (reactApi: ReactApi, deprecationInfo: string | undefi
reactApi.translations = translations;
};

const generateApiJson = async (outputDirectory: string, reactApi: ReactApi) => {
const generateApiJson = async (outputDirectory: string, reactApi: HookReactApi) => {
/**
* Gather the metadata needed for the component's API page.
*/
Expand Down Expand Up @@ -543,7 +478,7 @@ export default async function generateHookApi(
return null;
}

const reactApi: ReactApi = docgenParse(
const reactApi: HookReactApi = docgenParse(
src,
(ast) => {
let node;
Expand Down
6 changes: 2 additions & 4 deletions packages/api-docs-builder/ProjectSettings.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import { ComponentClassDefinition } from '@mui/internal-docs-utils';
import { ComponentInfo, HookInfo } from './buildApiUtils';
import { CreateTypeScriptProjectOptions } from './utils/createTypeScriptProject';
import { CreateDescribeablePropSettings } from './utils/createDescribeableProp';
import { ReactApi as ComponentReactApi } from './ApiBuilders/ComponentApiBuilder';
import { ReactApi as HookReactApi } from './ApiBuilders/HookApiBuilder';
import { Slot } from './utils/parseSlotsAndClasses';
import { ComponentReactApi, HookReactApi } from './types/ApiBuilder.types';
import { Slot, ComponentInfo, HookInfo } from './types/utils.types';

export type SortingStrategiesType = {
/**
Expand Down
Loading