Skip to content
Open
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
5 changes: 5 additions & 0 deletions .changeset/tidy-beans-smile.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'react-docgen': patch
---

Allow parsing with Babel 8 when it is supplied via an override, while preserving Babel 7's default parser behavior.
3 changes: 3 additions & 0 deletions packages/react-docgen/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,5 +53,8 @@
"doctrine": "^3.0.0",
"resolve": "^1.22.1",
"strip-indent": "^4.0.0"
},
"devDependencies": {
"babel-core-8": "npm:@babel/core@8.0.1"
}
}
75 changes: 75 additions & 0 deletions packages/react-docgen/src/__tests__/babel8-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { parse } from '../main.js';
import { expect, test, vi } from 'vitest';

vi.mock('@babel/core', () => import('babel-core-8'));

test('parses components with Babel 8', () => {
const result = parse('export function Button() { return <button />; }');

expect(result).toHaveLength(1);
});

test('parses TypeScript function props with Babel 8', () => {
const result = parse(
`export type MenuProps = {
onOpenChange?: (open: boolean) => void;
};

export function Menu(_props: MenuProps) {
return <div />;
}`,
{ filename: 'index.tsx' },
);

expect(result[0]?.props?.onOpenChange?.tsType).toEqual({
name: 'signature',
type: 'function',
raw: '(open: boolean) => void',
signature: {
arguments: [
{
name: 'open',
type: { name: 'boolean' },
},
],
return: { name: 'void' },
},
});
});

test('parses generic arrow functions in TypeScript files with Babel 8', () => {
const result = parse(
`import React from 'react';

export const mockDomain = <Entity>(
entities: Record<string, Entity> = {},
) => ({ entities });

export function Button() {
return React.createElement('button');
}`,
{ filename: 'store.ts' },
);

expect(result).toHaveLength(1);
});

test('parses mapped TypeScript props with Babel 8', () => {
const result = parse(
`export type StatusFiltersProps<K extends string = string> = {
statuses?: { readonly [Key in K]: number };
};

export function StatusFilters<K extends string = string>(
_props: StatusFiltersProps<K>,
) {
return <div />;
}`,
{ filename: 'index.tsx' },
);

expect(result[0]?.props?.statuses?.tsType).toMatchObject({
name: 'signature',
type: 'object',
});
});
56 changes: 40 additions & 16 deletions packages/react-docgen/src/babelParser.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,58 @@
import type { ParserOptions, TransformOptions } from '@babel/core';
import { loadPartialConfig, parseSync } from '@babel/core';
import * as babel from '@babel/core';
import type { File } from '@babel/types';
import { extname } from 'path';

const TYPESCRIPT_EXTS = new Set(['.cts', '.mts', '.ts', '.tsx']);
const { parseSync, version } = babel;
// Babel 7 exports this at runtime, but its DefinitelyTyped definitions omit it.
const loadPartialConfigSync = (
babel as typeof babel & {
loadPartialConfigSync: typeof babel.loadPartialConfig;
}
).loadPartialConfigSync;
const IS_BABEL_8 = Number.parseInt(version, 10) >= 8;

function getDefaultPlugins(
options: TransformOptions,
): NonNullable<ParserOptions['plugins']> {
return [
'jsx',
options.filename && TYPESCRIPT_EXTS.has(extname(options.filename))
? 'typescript'
: 'flow',
const extension = options.filename ? extname(options.filename) : '';
const isTypeScript = TYPESCRIPT_EXTS.has(extension);
const plugins: NonNullable<ParserOptions['plugins']> = [
...(isTypeScript && extension !== '.tsx' ? [] : ['jsx' as const]),
isTypeScript ? 'typescript' : 'flow',
'asyncDoExpressions',
'decimal',
];

if (!IS_BABEL_8) {
plugins.push('decimal');
}

plugins.push(
['decorators', { decoratorsBeforeExport: false }],
'decoratorAutoAccessors',
'destructuringPrivate',
'doExpressions',
'exportDefaultFrom',
'functionBind',
'importAssertions',
'moduleBlocks',
'partialApplication',
['pipelineOperator', { proposal: 'minimal' }],
['recordAndTuple', { syntaxType: 'bar' }],
'regexpUnicodeSets',
'throwExpressions',
];
);

if (!IS_BABEL_8) {
plugins.push('importAssertions');
}

plugins.push('moduleBlocks', 'partialApplication', [
'pipelineOperator',
{ proposal: IS_BABEL_8 ? 'fsharp' : 'minimal' },
]);

if (!IS_BABEL_8) {
plugins.push(['recordAndTuple', { syntaxType: 'bar' }]);
}

plugins.push('regexpUnicodeSets', 'throwExpressions');

return plugins;
}

function buildPluginList(
Expand All @@ -42,7 +66,7 @@ function buildPluginList(

// Let's check if babel finds a config file for this source file
// If babel does find a config file we do not apply our defaults
const partialConfig = loadPartialConfig(options);
const partialConfig = loadPartialConfigSync(options);

if (
plugins.length === 0 &&
Expand Down
21 changes: 15 additions & 6 deletions packages/react-docgen/src/utils/getTSType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -285,10 +285,12 @@ function handleTSMappedType(
path: NodePath<TSMappedType>,
typeParams: TypeParameters | null,
): ObjectSignatureType<TSFunctionSignatureType> {
const key = getTSTypeWithResolvedTypes(
path.get('typeParameter').get('constraint') as NodePath<TSType>,
typeParams,
);
const constraint = (
'constraint' in path.node
? path.get('constraint')
: path.get('typeParameter').get('constraint')
) as NodePath<TSType>;
const key = getTSTypeWithResolvedTypes(constraint, typeParams);

key.required = !path.node.optional;

Expand Down Expand Up @@ -321,8 +323,11 @@ function handleTSFunctionType(
typeParams: TypeParameters | null,
): TSFunctionSignatureType {
let returnType: TypeDescriptor<TSFunctionSignatureType> | undefined;
const usesBabel8Fields = 'params' in path.node;

const annotation = path.get('typeAnnotation');
const annotation = path.get(
usesBabel8Fields ? 'returnType' : 'typeAnnotation',
) as NodePath<TSTypeAnnotation | null | undefined>;

if (annotation.hasNode()) {
returnType = getTSTypeWithResolvedTypes(annotation, typeParams);
Expand All @@ -338,7 +343,11 @@ function handleTSFunctionType(
},
};

path.get('parameters').forEach((param) => {
const parameters = path.get(
usesBabel8Fields ? 'params' : 'parameters',
) as Array<NodePath<TSFunctionType['parameters'][number]>>;

parameters.forEach((param) => {
const typeAnnotation = getTypeAnnotation<TSType>(param);

const arg: FunctionArgumentType<TSFunctionSignatureType> = {
Expand Down
Loading