diff --git a/src/DocoffReactProps/_helpers/parseProps/_helpers/parseDefinitions/_helpers/adjustTypesPlugin/_helpers/adjustTypes/adjustTypes.js b/src/DocoffReactProps/_helpers/parseProps/_helpers/parseDefinitions/_helpers/adjustTypesPlugin/_helpers/adjustTypes/adjustTypes.js index 0faf499..795ba4f 100644 --- a/src/DocoffReactProps/_helpers/parseProps/_helpers/parseDefinitions/_helpers/adjustTypesPlugin/_helpers/adjustTypes/adjustTypes.js +++ b/src/DocoffReactProps/_helpers/parseProps/_helpers/parseDefinitions/_helpers/adjustTypesPlugin/_helpers/adjustTypes/adjustTypes.js @@ -11,11 +11,12 @@ import { wrapGenericTypeAlias } from './_helpers/wrapGenericTypeAlias'; * @param {Object|Object[]} node The syntax tree node, or a list of nodes * @param {Function} parseOverrideType The parser of the type written in the override tag * @param {string} code The source code + * @param {Map} declaredValueTypes The types of the values declared without initial value by the names of the values * @returns {Object|Object[]} The adjusted node, or the list of adjusted nodes */ -export const adjustTypes = (node, parseOverrideType, code) => { +export const adjustTypes = (node, parseOverrideType, code, declaredValueTypes = new Map()) => { if (Array.isArray(node)) { - return node.map((item) => adjustTypes(item, parseOverrideType, code)); + return node.map((item) => adjustTypes(item, parseOverrideType, code, declaredValueTypes)); } if (typeof node?.type !== 'string') { @@ -25,12 +26,23 @@ export const adjustTypes = (node, parseOverrideType, code) => { // Parenthesized type, e.g. `(string | number)[]`, and read-only type, e.g. `readonly string[]`, are reported // as `unknown`. The parentheses and the operator are removed, which keeps the meaning of the type. if (node.type === 'TSParenthesizedType' || isTypeOperator(node, 'readonly')) { - return adjustTypes(node.typeAnnotation, parseOverrideType, code); + return adjustTypes(node.typeAnnotation, parseOverrideType, code, declaredValueTypes); + } + + // `typeof` of a value declared without initial value, e.g. `declare const sizes: { small: number }` in type + // declarations, is the type of the value, as `react-docgen` reads the type of a value from its initial value only + if (node.type === 'TSTypeQuery' && declaredValueTypes.has(node.exprName.name)) { + return adjustTypes( + structuredClone(declaredValueTypes.get(node.exprName.name)), + parseOverrideType, + code, + declaredValueTypes, + ); } // `keyof` of a value, e.g. `keyof typeof sizes`, and of an object written in place is resolved by `react-docgen` if (isTypeOperator(node, 'keyof')) { - const typeNode = adjustTypes(node.typeAnnotation, parseOverrideType, code); + const typeNode = adjustTypes(node.typeAnnotation, parseOverrideType, code, declaredValueTypes); return ['TSTypeLiteral', 'TSTypeQuery'].includes(typeNode.type) ? { @@ -44,8 +56,8 @@ export const adjustTypes = (node, parseOverrideType, code) => { // written in place if (node.type === 'TSIndexedAccessType') { return createTypeReferenceNode(node, 'indexedAccess', [ - adjustTypes(node.objectType, parseOverrideType, code), - adjustTypes(node.indexType, parseOverrideType, code), + adjustTypes(node.objectType, parseOverrideType, code, declaredValueTypes), + adjustTypes(node.indexType, parseOverrideType, code, declaredValueTypes), ]); } @@ -61,7 +73,7 @@ export const adjustTypes = (node, parseOverrideType, code) => { Object.keys(node).forEach((key) => { // eslint-disable-next-line no-param-reassign - node[key] = adjustTypes(node[key], parseOverrideType, code); + node[key] = adjustTypes(node[key], parseOverrideType, code, declaredValueTypes); }); if (node.type === 'TSTypeAliasDeclaration') { diff --git a/src/DocoffReactProps/_helpers/parseProps/_helpers/parseDefinitions/_helpers/adjustTypesPlugin/_helpers/getDeclaredValueTypes.js b/src/DocoffReactProps/_helpers/parseProps/_helpers/parseDefinitions/_helpers/adjustTypesPlugin/_helpers/getDeclaredValueTypes.js new file mode 100644 index 0000000..4bacac9 --- /dev/null +++ b/src/DocoffReactProps/_helpers/parseProps/_helpers/parseDefinitions/_helpers/adjustTypesPlugin/_helpers/getDeclaredValueTypes.js @@ -0,0 +1,13 @@ +/** + * Values in type declarations have no initial value to read their type from, only their type, e.g. + * `declare const sizes: { small: number }`. + * + * @param {Object} file The parsed file + * @returns {Map} The types of the values declared without initial value by the names of the values + */ +export const getDeclaredValueTypes = (file) => new Map(file.program.body + .map((statement) => (statement.type === 'ExportNamedDeclaration' ? statement.declaration : statement)) + .filter((statement) => statement?.type === 'VariableDeclaration') + .flatMap((statement) => statement.declarations) + .filter((declarator) => declarator.init === null && declarator.id.type === 'Identifier' && declarator.id.typeAnnotation) + .map((declarator) => [declarator.id.name, structuredClone(declarator.id.typeAnnotation.typeAnnotation)])); diff --git a/src/DocoffReactProps/_helpers/parseProps/_helpers/parseDefinitions/_helpers/adjustTypesPlugin/adjustTypesPlugin.js b/src/DocoffReactProps/_helpers/parseProps/_helpers/parseDefinitions/_helpers/adjustTypesPlugin/adjustTypesPlugin.js index 4e255a6..4a345f1 100644 --- a/src/DocoffReactProps/_helpers/parseProps/_helpers/parseDefinitions/_helpers/adjustTypesPlugin/adjustTypesPlugin.js +++ b/src/DocoffReactProps/_helpers/parseProps/_helpers/parseDefinitions/_helpers/adjustTypesPlugin/adjustTypesPlugin.js @@ -1,14 +1,15 @@ import { adjustTypes } from './_helpers/adjustTypes'; import { createOverrideTypeParser } from './_helpers/createOverrideTypeParser'; +import { getDeclaredValueTypes } from './_helpers/getDeclaredValueTypes'; /** * Babel plugin that adjusts the types in the parsed source code. `react-docgen` does not recognize some of the type * syntax and reports it as `unknown` or incorrectly, so the syntax tree is adjusted before it gets processed. */ export const adjustTypesPlugin = { - parserOverride: (code, parserOpts, parse) => adjustTypes( - parse(code, parserOpts), - createOverrideTypeParser(parse, parserOpts), - code, - ), + parserOverride: (code, parserOpts, parse) => { + const file = parse(code, parserOpts); + + return adjustTypes(file, createOverrideTypeParser(parse, parserOpts), code, getDeclaredValueTypes(file)); + }, }; diff --git a/src/DocoffReactProps/_helpers/parseProps/parseProps.test.js b/src/DocoffReactProps/_helpers/parseProps/parseProps.test.js index da85dd1..9b4ed9e 100644 --- a/src/DocoffReactProps/_helpers/parseProps/parseProps.test.js +++ b/src/DocoffReactProps/_helpers/parseProps/parseProps.test.js @@ -292,6 +292,18 @@ const files = { labelPosition?: ToggleLabelPosition; }; + declare const avatarSizes: { + small: number; + large: number; + }; + export type AvatarSize = keyof typeof avatarSizes; + export type AvatarProps = { + /** + * Size of the avatar. + */ + size?: AvatarSize; + }; + export {}; `, 'https://cdn.test/library/src/components/Toggle/Toggle.types.ts': ` @@ -504,6 +516,17 @@ describe('functionality', () => { expect(global.fetch).not.toHaveBeenCalled(); }); + it('evaluates `keyof typeof` of value declared without initial value in type declarations', async () => { + expect(await getProps('https://cdn.test/library.d.ts', 'AvatarProps')).toEqual({ + size: { + defaultValue: undefined, + description: 'Size of the avatar.', + required: false, + type: '\'small\' | \'large\'', + }, + }); + }); + it('looks up relative imports of the component in the given file instead of downloading them', async () => { expect(await getProps( 'http://localhost/components/Switch/Switch.tsx',