Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand All @@ -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)
? {
Expand All @@ -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),
]);
}

Expand All @@ -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') {
Expand Down
Original file line number Diff line number Diff line change
@@ -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)]));
Original file line number Diff line number Diff line change
@@ -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));
},
};
23 changes: 23 additions & 0 deletions src/DocoffReactProps/_helpers/parseProps/parseProps.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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': `
Expand Down Expand Up @@ -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',
Expand Down
Loading