Add comprehensive development roadmap via GitHub Issues

Created 10 detailed GitHub issues covering:
- Project activation and management UI (#1-2)
- Worker node coordination and visualization (#3-4)
- Automated GitHub repository scanning (#5)
- Intelligent model-to-issue matching (#6)
- Multi-model task execution system (#7)
- N8N workflow integration (#8)
- Hive-Bzzz P2P bridge (#9)
- Peer assistance protocol (#10)

Each issue includes detailed specifications, acceptance criteria,
technical implementation notes, and dependency mapping.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
anthonyrawlins
2025-07-12 19:41:01 +10:00
parent 9a6a06da89
commit e89f2f4b7b
4980 changed files with 1501266 additions and 57 deletions

View File

@@ -0,0 +1,112 @@
export interface Documentation {
childContext?: Record<string, PropDescriptor>;
composes?: string[];
context?: Record<string, PropDescriptor>;
description?: string;
displayName?: string;
methods?: MethodDescriptor[];
props?: Record<string, PropDescriptor>;
}
export interface MethodParameter {
name: string;
description?: string;
optional: boolean;
type?: TypeDescriptor<FunctionSignatureType> | null;
}
export interface MethodReturn {
description?: string;
type: TypeDescriptor<FunctionSignatureType> | undefined;
}
export type MethodModifier = 'async' | 'generator' | 'get' | 'set' | 'static';
export interface MethodDescriptor {
name: string;
description?: string | null;
docblock: string | null;
modifiers: MethodModifier[];
params: MethodParameter[];
returns: MethodReturn | null;
}
export interface PropTypeDescriptor {
name: 'any' | 'array' | 'arrayOf' | 'bool' | 'custom' | 'element' | 'elementType' | 'enum' | 'exact' | 'func' | 'instanceOf' | 'node' | 'number' | 'object' | 'objectOf' | 'shape' | 'string' | 'symbol' | 'union';
value?: unknown;
raw?: string;
computed?: boolean;
description?: string;
required?: boolean;
}
export interface DefaultValueDescriptor {
value: unknown;
computed: boolean;
}
export interface BaseType {
required?: boolean;
nullable?: boolean;
alias?: string;
}
export interface SimpleType extends BaseType {
name: string;
raw?: string;
}
export interface LiteralType extends BaseType {
name: 'literal';
value: string;
}
export interface ElementsType<T = FunctionSignatureType> extends BaseType {
name: string;
raw: string;
elements: Array<TypeDescriptor<T>>;
}
export interface FunctionArgumentType<T> {
name: string;
type?: TypeDescriptor<T>;
rest?: boolean;
}
export interface FunctionSignatureType extends BaseType {
name: 'signature';
type: 'function';
raw: string;
signature: {
arguments: Array<FunctionArgumentType<FunctionSignatureType>>;
return?: TypeDescriptor<FunctionSignatureType>;
};
}
export interface TSFunctionSignatureType extends FunctionSignatureType {
signature: {
arguments: Array<FunctionArgumentType<TSFunctionSignatureType>>;
return?: TypeDescriptor<TSFunctionSignatureType>;
this?: TypeDescriptor<TSFunctionSignatureType>;
};
}
export interface ObjectSignatureType<T = FunctionSignatureType> extends BaseType {
name: 'signature';
type: 'object';
raw: string;
signature: {
properties: Array<{
key: TypeDescriptor<T> | string;
value: TypeDescriptor<T>;
description?: string;
}>;
constructor?: TypeDescriptor<T>;
};
}
export type TypeDescriptor<T = FunctionSignatureType> = ElementsType<T> | LiteralType | ObjectSignatureType<T> | SimpleType | T;
export interface PropDescriptor {
type?: PropTypeDescriptor;
flowType?: TypeDescriptor<FunctionSignatureType>;
tsType?: TypeDescriptor<TSFunctionSignatureType>;
required?: boolean;
defaultValue?: DefaultValueDescriptor;
description?: string;
}
export default class DocumentationBuilder {
#private;
constructor();
addComposes(moduleName: string): void;
set(key: string, value: unknown): void;
get<T>(key: string): T | null;
getPropDescriptor(propName: string): PropDescriptor;
getContextDescriptor(propName: string): PropDescriptor;
getChildContextDescriptor(propName: string): PropDescriptor;
build(): Documentation;
}

View File

@@ -0,0 +1,91 @@
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
if (kind === "m") throw new TypeError("Private method is not writable");
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
};
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
};
var _DocumentationBuilder_props, _DocumentationBuilder_context, _DocumentationBuilder_childContext, _DocumentationBuilder_composes, _DocumentationBuilder_data;
class DocumentationBuilder {
constructor() {
_DocumentationBuilder_props.set(this, void 0);
_DocumentationBuilder_context.set(this, void 0);
_DocumentationBuilder_childContext.set(this, void 0);
_DocumentationBuilder_composes.set(this, void 0);
_DocumentationBuilder_data.set(this, void 0);
__classPrivateFieldSet(this, _DocumentationBuilder_props, new Map(), "f");
__classPrivateFieldSet(this, _DocumentationBuilder_context, new Map(), "f");
__classPrivateFieldSet(this, _DocumentationBuilder_childContext, new Map(), "f");
__classPrivateFieldSet(this, _DocumentationBuilder_composes, new Set(), "f");
__classPrivateFieldSet(this, _DocumentationBuilder_data, new Map(), "f");
}
addComposes(moduleName) {
__classPrivateFieldGet(this, _DocumentationBuilder_composes, "f").add(moduleName);
}
set(key, value) {
__classPrivateFieldGet(this, _DocumentationBuilder_data, "f").set(key, value);
}
get(key) {
return __classPrivateFieldGet(this, _DocumentationBuilder_data, "f").get(key);
}
getPropDescriptor(propName) {
let propDescriptor = __classPrivateFieldGet(this, _DocumentationBuilder_props, "f").get(propName);
if (!propDescriptor) {
__classPrivateFieldGet(this, _DocumentationBuilder_props, "f").set(propName, (propDescriptor = {}));
}
return propDescriptor;
}
getContextDescriptor(propName) {
let propDescriptor = __classPrivateFieldGet(this, _DocumentationBuilder_context, "f").get(propName);
if (!propDescriptor) {
__classPrivateFieldGet(this, _DocumentationBuilder_context, "f").set(propName, (propDescriptor = {}));
}
return propDescriptor;
}
getChildContextDescriptor(propName) {
let propDescriptor = __classPrivateFieldGet(this, _DocumentationBuilder_childContext, "f").get(propName);
if (!propDescriptor) {
__classPrivateFieldGet(this, _DocumentationBuilder_childContext, "f").set(propName, (propDescriptor = {}));
}
return propDescriptor;
}
build() {
const obj = {};
for (const [key, value] of __classPrivateFieldGet(this, _DocumentationBuilder_data, "f")) {
// @ts-expect-error custom handlers can add any properties to Documentation
obj[key] = value;
}
if (__classPrivateFieldGet(this, _DocumentationBuilder_props, "f").size > 0) {
obj.props = {};
for (const [propName, propDescriptor] of __classPrivateFieldGet(this, _DocumentationBuilder_props, "f")) {
if (Object.keys(propDescriptor).length > 0) {
obj.props[propName] = propDescriptor;
}
}
}
if (__classPrivateFieldGet(this, _DocumentationBuilder_context, "f").size > 0) {
obj.context = {};
for (const [contextName, contextDescriptor] of __classPrivateFieldGet(this, _DocumentationBuilder_context, "f")) {
if (Object.keys(contextDescriptor).length > 0) {
obj.context[contextName] = contextDescriptor;
}
}
}
if (__classPrivateFieldGet(this, _DocumentationBuilder_childContext, "f").size > 0) {
obj.childContext = {};
for (const [childContextName, childContextDescriptor] of __classPrivateFieldGet(this, _DocumentationBuilder_childContext, "f")) {
obj.childContext[childContextName] = childContextDescriptor;
}
}
if (__classPrivateFieldGet(this, _DocumentationBuilder_composes, "f").size > 0) {
obj.composes = Array.from(__classPrivateFieldGet(this, _DocumentationBuilder_composes, "f"));
}
return obj;
}
}
_DocumentationBuilder_props = new WeakMap(), _DocumentationBuilder_context = new WeakMap(), _DocumentationBuilder_childContext = new WeakMap(), _DocumentationBuilder_composes = new WeakMap(), _DocumentationBuilder_data = new WeakMap();
export default DocumentationBuilder;

31
frontend/node_modules/react-docgen/dist/FileState.d.ts generated vendored Normal file
View File

@@ -0,0 +1,31 @@
import type { HubInterface, Scope, Visitor } from '@babel/traverse';
import { NodePath } from '@babel/traverse';
import type { File, Program } from '@babel/types';
import type { Importer, ImportPath } from './importer/index.js';
import type { TransformOptions } from '@babel/core';
export default class FileState {
#private;
opts: TransformOptions;
path: NodePath<Program>;
ast: File;
scope: Scope;
code: string;
hub: HubInterface;
constructor(options: TransformOptions, { code, ast, importer }: {
code: string;
ast: File;
importer: Importer;
});
/**
* Try to resolve and import the ImportPath with the `name`
*/
import(path: ImportPath, name: string): NodePath | null;
/**
* Parse the content of a new file
* The `filename` is required so that potential imports inside the content can be correctly resolved and
* the correct babel config file could be loaded. `filename` needs to be an absolute path.
*/
parse(code: string, filename: string): FileState;
traverse<S>(visitors: Visitor<S>, state?: S): void;
traverse(visitors: Visitor): void;
}

77
frontend/node_modules/react-docgen/dist/FileState.js generated vendored Normal file
View File

@@ -0,0 +1,77 @@
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
if (kind === "m") throw new TypeError("Private method is not writable");
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
};
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
};
var _FileState_importer;
import babelTraverse, { NodePath } from '@babel/traverse';
import babelParse from './babelParser.js';
// Workaround while babel is not a proper ES module
const traverse = babelTraverse.default ?? babelTraverse;
class FileState {
constructor(options, { code, ast, importer }) {
_FileState_importer.set(this, void 0);
this.hub = {
// keep it for the usage in babel-core, ex: path.hub.file.opts.filename
file: this,
parse: this.parse.bind(this),
import: this.import.bind(this),
getCode: () => this.code,
getScope: () => this.scope,
addHelper: () => undefined,
buildError: (node, msg, Error) => {
const err = new Error(msg);
err.node = node;
return err;
},
};
this.opts = options;
this.code = code;
this.ast = ast;
__classPrivateFieldSet(this, _FileState_importer, importer, "f");
this.path = NodePath.get({
hub: this.hub,
parentPath: null,
parent: this.ast,
container: this.ast,
key: 'program',
}).setContext();
this.scope = this.path.scope;
}
/**
* Try to resolve and import the ImportPath with the `name`
*/
import(path, name) {
return __classPrivateFieldGet(this, _FileState_importer, "f").call(this, path, name, this);
}
/**
* Parse the content of a new file
* The `filename` is required so that potential imports inside the content can be correctly resolved and
* the correct babel config file could be loaded. `filename` needs to be an absolute path.
*/
parse(code, filename) {
const newOptions = { ...this.opts, filename };
// We need to build a new parser, because there might be a new
// babel config file in effect, so we need to load it
const ast = babelParse(code, newOptions);
return new FileState(newOptions, {
ast,
code,
importer: __classPrivateFieldGet(this, _FileState_importer, "f"),
});
}
/**
* Traverse the current file
*/
traverse(visitors, state) {
traverse(this.ast, visitors, this.scope, state);
}
}
_FileState_importer = new WeakMap();
export default FileState;

View File

@@ -0,0 +1,3 @@
import type { TransformOptions } from '@babel/core';
import type { File } from '@babel/types';
export default function babelParser(src: string, options?: TransformOptions): File;

64
frontend/node_modules/react-docgen/dist/babelParser.js generated vendored Normal file
View File

@@ -0,0 +1,64 @@
import { loadPartialConfig, parseSync } from '@babel/core';
import { extname } from 'path';
const TYPESCRIPT_EXTS = new Set(['.cts', '.mts', '.ts', '.tsx']);
function getDefaultPlugins(options) {
return [
'jsx',
options.filename && TYPESCRIPT_EXTS.has(extname(options.filename))
? 'typescript'
: 'flow',
'asyncDoExpressions',
'decimal',
['decorators', { decoratorsBeforeExport: false }],
'decoratorAutoAccessors',
'destructuringPrivate',
'doExpressions',
'exportDefaultFrom',
'functionBind',
'importAssertions',
'moduleBlocks',
'partialApplication',
['pipelineOperator', { proposal: 'minimal' }],
['recordAndTuple', { syntaxType: 'bar' }],
'regexpUnicodeSets',
'throwExpressions',
];
}
function buildPluginList(options) {
let plugins = [];
if (options.parserOpts?.plugins) {
plugins = [...options.parserOpts.plugins];
}
// 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);
if (plugins.length === 0 &&
partialConfig &&
!partialConfig.hasFilesystemConfig()) {
plugins = getDefaultPlugins(options);
}
// Ensure that the estree plugin is never active
// TODO add test
return plugins.filter((plugin) => plugin !== 'estree');
}
function buildParserOptions(options) {
const plugins = buildPluginList(options);
return {
sourceType: 'unambiguous',
...(options.parserOpts || {}),
plugins,
tokens: false,
};
}
export default function babelParser(src, options = {}) {
const parserOpts = buildParserOptions(options);
const opts = {
...options,
parserOpts,
};
const ast = parseSync(src, opts);
if (!ast) {
throw new Error('Unable to parse source code.');
}
return ast;
}

19
frontend/node_modules/react-docgen/dist/config.d.ts generated vendored Normal file
View File

@@ -0,0 +1,19 @@
import type { TransformOptions } from '@babel/core';
import type { Handler } from './handlers/index.js';
import type { Importer } from './importer/index.js';
import type { Resolver } from './resolver/index.js';
export interface Config {
handlers?: Handler[];
importer?: Importer;
resolver?: Resolver;
/**
* shortcut for `babelOptions.filename`
* Set to an absolute path (recommended) to the file currently being parsed or
* to an relative path that is relative to the `babelOptions.cwd`.
*/
filename?: string;
babelOptions?: TransformOptions;
}
export type InternalConfig = Omit<Required<Config>, 'filename'>;
export declare const defaultHandlers: Handler[];
export declare function createConfig(inputConfig: Config): InternalConfig;

39
frontend/node_modules/react-docgen/dist/config.js generated vendored Normal file
View File

@@ -0,0 +1,39 @@
import { childContextTypeHandler, codeTypeHandler, componentDocblockHandler, componentMethodsHandler, componentMethodsJsDocHandler, contextTypeHandler, defaultPropsHandler, displayNameHandler, propDocblockHandler, propTypeCompositionHandler, propTypeHandler, } from './handlers/index.js';
import { fsImporter } from './importer/index.js';
import { ChainResolver, FindAnnotatedDefinitionsResolver, FindExportedDefinitionsResolver, } from './resolver/index.js';
const defaultResolvers = [
new FindExportedDefinitionsResolver({
limit: 1,
}),
new FindAnnotatedDefinitionsResolver(),
];
const defaultResolver = new ChainResolver(defaultResolvers, {
chainingLogic: ChainResolver.Logic.ALL,
});
const defaultImporter = fsImporter;
export const defaultHandlers = [
propTypeHandler,
contextTypeHandler,
childContextTypeHandler,
propTypeCompositionHandler,
propDocblockHandler,
codeTypeHandler,
defaultPropsHandler,
componentDocblockHandler,
displayNameHandler,
componentMethodsHandler,
componentMethodsJsDocHandler,
];
export function createConfig(inputConfig) {
const { babelOptions, filename, handlers, importer, resolver } = inputConfig;
const config = {
babelOptions: { ...babelOptions },
handlers: handlers ?? defaultHandlers,
importer: importer ?? defaultImporter,
resolver: resolver ?? defaultResolver,
};
if (filename) {
config.babelOptions.filename = filename;
}
return config;
}

8
frontend/node_modules/react-docgen/dist/error.d.ts generated vendored Normal file
View File

@@ -0,0 +1,8 @@
export declare enum ERROR_CODES {
MISSING_DEFINITION = "ERR_REACTDOCGEN_MISSING_DEFINITION",
MULTIPLE_DEFINITIONS = "ERR_REACTDOCGEN_MULTIPLE_DEFINITIONS"
}
export declare class ReactDocgenError extends Error {
code: string | undefined;
constructor(code: ERROR_CODES);
}

18
frontend/node_modules/react-docgen/dist/error.js generated vendored Normal file
View File

@@ -0,0 +1,18 @@
export var ERROR_CODES;
(function (ERROR_CODES) {
ERROR_CODES["MISSING_DEFINITION"] = "ERR_REACTDOCGEN_MISSING_DEFINITION";
ERROR_CODES["MULTIPLE_DEFINITIONS"] = "ERR_REACTDOCGEN_MULTIPLE_DEFINITIONS";
})(ERROR_CODES || (ERROR_CODES = {}));
const messages = new Map([
[ERROR_CODES.MISSING_DEFINITION, 'No suitable component definition found.'],
[
ERROR_CODES.MULTIPLE_DEFINITIONS,
'Multiple exported component definitions found.',
],
]);
export class ReactDocgenError extends Error {
constructor(code) {
super(messages.get(code));
this.code = code;
}
}

View File

@@ -0,0 +1,8 @@
import type { Handler } from './index.js';
/**
* This handler tries to find flow and TS Type annotated react components and extract
* its types to the documentation. It also extracts docblock comments which are
* inlined in the type definition.
*/
declare const codeTypeHandler: Handler;
export default codeTypeHandler;

View File

@@ -0,0 +1,79 @@
import { unwrapUtilityType } from '../utils/flowUtilityTypes.js';
import getFlowType from '../utils/getFlowType.js';
import getTypeFromReactComponent, { applyToTypeProperties, } from '../utils/getTypeFromReactComponent.js';
import getPropertyName from '../utils/getPropertyName.js';
import getTSType from '../utils/getTSType.js';
import resolveToValue from '../utils/resolveToValue.js';
import setPropDescription from '../utils/setPropDescription.js';
function setPropDescriptor(documentation, path, typeParams) {
if (path.isObjectTypeSpreadProperty()) {
const argument = unwrapUtilityType(path.get('argument'));
if (argument.isObjectTypeAnnotation()) {
applyToTypeProperties(documentation, argument, (propertyPath, innerTypeParams) => {
setPropDescriptor(documentation, propertyPath, innerTypeParams);
}, typeParams);
return;
}
const id = argument.get('id');
if (!id.hasNode() || !id.isIdentifier()) {
return;
}
const resolvedPath = resolveToValue(id);
if (resolvedPath.isTypeAlias()) {
const right = resolvedPath.get('right');
applyToTypeProperties(documentation, right, (propertyPath, innerTypeParams) => {
setPropDescriptor(documentation, propertyPath, innerTypeParams);
}, typeParams);
}
else if (!argument.has('typeParameters')) {
documentation.addComposes(id.node.name);
}
}
else if (path.isObjectTypeProperty()) {
const type = getFlowType(path.get('value'), typeParams);
const propName = getPropertyName(path);
if (!propName)
return;
const propDescriptor = documentation.getPropDescriptor(propName);
propDescriptor.required = !path.node.optional;
propDescriptor.flowType = type;
// We are doing this here instead of in a different handler
// to not need to duplicate the logic for checking for
// imported types that are spread in to props.
setPropDescription(documentation, path);
}
else if (path.isTSPropertySignature()) {
const typeAnnotation = path.get('typeAnnotation');
if (!typeAnnotation.hasNode()) {
return;
}
const type = getTSType(typeAnnotation, typeParams);
const propName = getPropertyName(path);
if (!propName)
return;
const propDescriptor = documentation.getPropDescriptor(propName);
propDescriptor.required = !path.node.optional;
propDescriptor.tsType = type;
// We are doing this here instead of in a different handler
// to not need to duplicate the logic for checking for
// imported types that are spread in to props.
setPropDescription(documentation, path);
}
}
/**
* This handler tries to find flow and TS Type annotated react components and extract
* its types to the documentation. It also extracts docblock comments which are
* inlined in the type definition.
*/
const codeTypeHandler = function (documentation, componentDefinition) {
const typePaths = getTypeFromReactComponent(componentDefinition);
if (typePaths.length === 0) {
return;
}
for (const typePath of typePaths) {
applyToTypeProperties(documentation, typePath, (propertyPath, typeParams) => {
setPropDescriptor(documentation, propertyPath, typeParams);
}, null);
}
};
export default codeTypeHandler;

View File

@@ -0,0 +1,6 @@
import type { Handler } from './index.js';
/**
* Finds the nearest block comment before the component definition.
*/
declare const componentDocblockHandler: Handler;
export default componentDocblockHandler;

View File

@@ -0,0 +1,46 @@
import { getDocblock } from '../utils/docblock.js';
import isReactForwardRefCall from '../utils/isReactForwardRefCall.js';
import resolveToValue from '../utils/resolveToValue.js';
function getDocblockFromComponent(path) {
let description = null;
if (path.isClassDeclaration() || path.isClassExpression()) {
const decorators = path.get('decorators');
// If we have a class declaration or expression, then the comment might be
// attached to the last decorator instead as trailing comment.
if (decorators && decorators.length > 0) {
description = getDocblock(decorators[decorators.length - 1], true);
}
}
if (description == null) {
// Find parent statement (e.g. var Component = React.createClass(<path>);)
let searchPath = path;
while (searchPath && !searchPath.isStatement()) {
searchPath = searchPath.parentPath;
}
if (searchPath) {
// If the parent is an export statement, we have to traverse one more up
if (searchPath.parentPath.isExportNamedDeclaration() ||
searchPath.parentPath.isExportDefaultDeclaration()) {
searchPath = searchPath.parentPath;
}
description = getDocblock(searchPath);
}
}
if (!description) {
const searchPath = isReactForwardRefCall(path)
? path.get('arguments')[0]
: path;
const inner = resolveToValue(searchPath);
if (inner.node !== path.node) {
return getDocblockFromComponent(inner);
}
}
return description;
}
/**
* Finds the nearest block comment before the component definition.
*/
const componentDocblockHandler = function (documentation, componentDefinition) {
documentation.set('description', getDocblockFromComponent(componentDefinition) || '');
};
export default componentDocblockHandler;

View File

@@ -0,0 +1,7 @@
import type { Handler } from './index.js';
/**
* Extract all flow types for the methods of a react component. Doesn't
* return any react specific lifecycle methods.
*/
declare const componentMethodsHandler: Handler;
export default componentMethodsHandler;

View File

@@ -0,0 +1,168 @@
import getMemberValuePath from '../utils/getMemberValuePath.js';
import getMethodDocumentation from '../utils/getMethodDocumentation.js';
import isReactComponentClass from '../utils/isReactComponentClass.js';
import isReactComponentMethod from '../utils/isReactComponentMethod.js';
import { shallowIgnoreVisitors } from '../utils/traverse.js';
import resolveToValue from '../utils/resolveToValue.js';
import { visitors } from '@babel/traverse';
import { isReactBuiltinCall, isReactForwardRefCall, isStatelessComponent, findFunctionReturn, } from '../utils/index.js';
/**
* The following values/constructs are considered methods:
*
* - Method declarations in classes (except "constructor" and React lifecycle
* methods
* - Public class fields in classes whose value are a functions
* - Object properties whose values are functions
*/
function isMethod(path) {
let isProbablyMethod = (path.isClassMethod() && path.node.kind !== 'constructor') ||
path.isObjectMethod();
if (!isProbablyMethod &&
(path.isClassProperty() || path.isObjectProperty())) {
const value = resolveToValue(path.get('value'));
isProbablyMethod = value.isFunction();
}
return isProbablyMethod && !isReactComponentMethod(path);
}
const explodedVisitors = visitors.explode({
...shallowIgnoreVisitors,
AssignmentExpression: {
enter: function (assignmentPath, state) {
const { name, scope } = state;
const left = assignmentPath.get('left');
const binding = assignmentPath.scope.getBinding(name);
if (binding &&
left.isMemberExpression() &&
left.get('object').isIdentifier({ name }) &&
binding.scope === scope &&
resolveToValue(assignmentPath.get('right')).isFunction()) {
state.methods.push(assignmentPath);
}
assignmentPath.skip();
},
},
});
function isObjectExpression(path) {
return path.isObjectExpression();
}
const explodedImperativeHandleVisitors = visitors.explode({
...shallowIgnoreVisitors,
CallExpression: {
enter: function (path, state) {
if (!isReactBuiltinCall(path, 'useImperativeHandle')) {
return path.skip();
}
// useImperativeHandle(ref, () => ({ name: () => {}, ...}))
const arg = path.get('arguments')[1];
if (!arg || !arg.isFunction()) {
return path.skip();
}
const body = resolveToValue(arg.get('body'));
let definition;
if (body.isObjectExpression()) {
definition = body;
}
else {
definition = findFunctionReturn(arg, isObjectExpression);
}
// We found the object body, now add all of the properties as methods.
definition?.get('properties').forEach((p) => {
if (isMethod(p)) {
state.results.push(p);
}
});
path.skip();
},
},
});
function findStatelessComponentBody(componentDefinition) {
if (isStatelessComponent(componentDefinition)) {
const body = componentDefinition.get('body');
if (body.isBlockStatement()) {
return body;
}
}
else if (isReactForwardRefCall(componentDefinition)) {
const inner = resolveToValue(componentDefinition.get('arguments')[0]);
return findStatelessComponentBody(inner);
}
return undefined;
}
function findImperativeHandleMethods(componentDefinition) {
const body = findStatelessComponentBody(componentDefinition);
if (!body) {
return [];
}
const state = { results: [] };
body.traverse(explodedImperativeHandleVisitors, state);
return state.results.map((p) => ({ path: p }));
}
function findAssignedMethods(path, idPath) {
if (!idPath.hasNode() || !idPath.isIdentifier()) {
return [];
}
const name = idPath.node.name;
const binding = idPath.scope.getBinding(name);
if (!binding) {
return [];
}
const scope = binding.scope;
const state = {
scope,
name,
methods: [],
};
path.traverse(explodedVisitors, state);
return state.methods.map((p) => ({ path: p }));
}
/**
* Extract all flow types for the methods of a react component. Doesn't
* return any react specific lifecycle methods.
*/
const componentMethodsHandler = function (documentation, componentDefinition) {
// Extract all methods from the class or object.
let methodPaths = [];
const parent = componentDefinition.parentPath;
if (isReactComponentClass(componentDefinition)) {
methodPaths = componentDefinition
.get('body')
.get('body')
.filter(isMethod).map((p) => ({ path: p }));
}
else if (componentDefinition.isObjectExpression()) {
methodPaths = componentDefinition.get('properties').filter(isMethod).map((p) => ({ path: p }));
// Add the statics object properties.
const statics = getMemberValuePath(componentDefinition, 'statics');
if (statics && statics.isObjectExpression()) {
statics.get('properties').forEach((property) => {
if (isMethod(property)) {
methodPaths.push({
path: property,
isStatic: true,
});
}
});
}
}
else if (parent.isVariableDeclarator() &&
parent.node.init === componentDefinition.node &&
parent.get('id').isIdentifier()) {
methodPaths = findAssignedMethods(parent.scope.path, parent.get('id'));
}
else if (parent.isAssignmentExpression() &&
parent.node.right === componentDefinition.node &&
parent.get('left').isIdentifier()) {
methodPaths = findAssignedMethods(parent.scope.path, parent.get('left'));
}
else if (componentDefinition.isFunctionDeclaration()) {
methodPaths = findAssignedMethods(parent.scope.path, componentDefinition.get('id'));
}
const imperativeHandles = findImperativeHandleMethods(componentDefinition);
if (imperativeHandles) {
methodPaths = [...methodPaths, ...imperativeHandles];
}
documentation.set('methods', methodPaths
.map(({ path: p, isStatic }) => getMethodDocumentation(p, { isStatic }))
.filter(Boolean));
};
export default componentMethodsHandler;

View File

@@ -0,0 +1,7 @@
import type { Handler } from './index.js';
/**
* Extract info from the methods jsdoc blocks. Must be run after
* componentMethodsHandler.
*/
declare const componentMethodsJsDocHandler: Handler;
export default componentMethodsJsDocHandler;

View File

@@ -0,0 +1,43 @@
import parseJsDoc from '../utils/parseJsDoc.js';
function removeEmpty(obj) {
return Object.fromEntries(Object.entries(obj).filter(([, v]) => v != null));
}
function merge(obj1, obj2) {
if (obj1 == null && obj2 == null) {
return null;
}
const merged = {
...removeEmpty(obj1 ?? {}),
...removeEmpty(obj2 ?? {}),
};
return merged;
}
/**
* Extract info from the methods jsdoc blocks. Must be run after
* componentMethodsHandler.
*/
const componentMethodsJsDocHandler = function (documentation) {
let methods = documentation.get('methods');
if (!methods) {
return;
}
methods = methods.map((method) => {
if (!method.docblock) {
return method;
}
const jsDoc = parseJsDoc(method.docblock);
const returns = merge(jsDoc.returns, method.returns);
const params = method.params.map((param) => {
const jsDocParam = jsDoc.params.find((p) => p.name === param.name);
return merge(jsDocParam, param);
});
return {
...method,
description: jsDoc.description || null,
returns,
params,
};
});
documentation.set('methods', methods);
};
export default componentMethodsJsDocHandler;

View File

@@ -0,0 +1,3 @@
import type { Handler } from './index.js';
declare const defaultPropsHandler: Handler;
export default defaultPropsHandler;

View File

@@ -0,0 +1,126 @@
import getPropertyName from '../utils/getPropertyName.js';
import getMemberValuePath from '../utils/getMemberValuePath.js';
import printValue from '../utils/printValue.js';
import resolveToValue from '../utils/resolveToValue.js';
import resolveFunctionDefinitionToReturnValue from '../utils/resolveFunctionDefinitionToReturnValue.js';
import isReactComponentClass from '../utils/isReactComponentClass.js';
import isReactForwardRefCall from '../utils/isReactForwardRefCall.js';
function getDefaultValue(path) {
let defaultValue;
let resolvedPath = path;
let valuePath = path;
if (path.isBooleanLiteral()) {
defaultValue = `${path.node.value}`;
}
else if (path.isNullLiteral()) {
defaultValue = 'null';
}
else if (path.isLiteral()) {
defaultValue = path.node.extra?.raw;
}
else {
if (path.isAssignmentPattern()) {
resolvedPath = resolveToValue(path.get('right'));
}
else {
resolvedPath = resolveToValue(path);
}
if (resolvedPath.parentPath?.isImportDeclaration() && path.isIdentifier()) {
defaultValue = path.node.name;
}
else {
valuePath = resolvedPath;
defaultValue = printValue(resolvedPath);
}
}
if (typeof defaultValue !== 'undefined') {
return {
value: defaultValue,
computed: valuePath.isCallExpression() ||
valuePath.isMemberExpression() ||
valuePath.isIdentifier(),
};
}
return null;
}
function getStatelessPropsPath(componentDefinition) {
let value = componentDefinition;
if (isReactForwardRefCall(componentDefinition)) {
value = resolveToValue(componentDefinition.get('arguments')[0]);
}
if (!value.isFunction()) {
return;
}
return value.get('params')[0];
}
function getDefaultPropsPath(componentDefinition) {
let defaultPropsPath = getMemberValuePath(componentDefinition, 'defaultProps');
if (!defaultPropsPath) {
return null;
}
defaultPropsPath = resolveToValue(defaultPropsPath);
if (!defaultPropsPath) {
return null;
}
if (defaultPropsPath.isFunctionExpression() ||
defaultPropsPath.isFunctionDeclaration() ||
defaultPropsPath.isClassMethod() ||
defaultPropsPath.isObjectMethod()) {
// Find the value that is returned from the function and process it if it is
// an object literal.
const returnValue = resolveFunctionDefinitionToReturnValue(defaultPropsPath);
if (returnValue && returnValue.isObjectExpression()) {
defaultPropsPath = returnValue;
}
}
return defaultPropsPath;
}
function getDefaultValuesFromProps(properties, documentation, isStateless) {
properties.forEach((propertyPath) => {
if (propertyPath.isObjectProperty()) {
const propName = getPropertyName(propertyPath);
if (!propName)
return;
let valuePath = propertyPath.get('value');
if (isStateless) {
if (valuePath.isAssignmentPattern()) {
valuePath = valuePath.get('right');
}
else {
// Don't evaluate property if component is functional and the node is not an AssignmentPattern
return;
}
}
// Initialize the prop descriptor here after the early return from above
const propDescriptor = documentation.getPropDescriptor(propName);
const defaultValue = getDefaultValue(valuePath);
if (defaultValue) {
propDescriptor.defaultValue = defaultValue;
}
}
else if (propertyPath.isSpreadElement()) {
const resolvedValuePath = resolveToValue(propertyPath.get('argument'));
if (resolvedValuePath.isObjectExpression()) {
getDefaultValuesFromProps(resolvedValuePath.get('properties'), documentation, isStateless);
}
}
});
}
const defaultPropsHandler = function (documentation, componentDefinition) {
let statelessProps;
const defaultPropsPath = getDefaultPropsPath(componentDefinition);
/**
* function, lazy, memo, forwardRef etc components can resolve default props as well
*/
if (!isReactComponentClass(componentDefinition)) {
statelessProps = getStatelessPropsPath(componentDefinition);
}
// Do both statelessProps and defaultProps if both are available so defaultProps can override
if (statelessProps && statelessProps.isObjectPattern()) {
getDefaultValuesFromProps(statelessProps.get('properties'), documentation, true);
}
if (defaultPropsPath && defaultPropsPath.isObjectExpression()) {
getDefaultValuesFromProps(defaultPropsPath.get('properties'), documentation, false);
}
};
export default defaultPropsHandler;

View File

@@ -0,0 +1,3 @@
import type { Handler } from './index.js';
declare const displayNameHandler: Handler;
export default displayNameHandler;

View File

@@ -0,0 +1,49 @@
import getMemberValuePath from '../utils/getMemberValuePath.js';
import getNameOrValue from '../utils/getNameOrValue.js';
import isReactForwardRefCall from '../utils/isReactForwardRefCall.js';
import resolveToValue from '../utils/resolveToValue.js';
import resolveFunctionDefinitionToReturnValue from '../utils/resolveFunctionDefinitionToReturnValue.js';
const displayNameHandler = function (documentation, componentDefinition) {
let displayNamePath = getMemberValuePath(componentDefinition, 'displayName');
if (!displayNamePath) {
// Function and class declarations need special treatment. The name of the
// function / class is the displayName
if ((componentDefinition.isClassDeclaration() ||
componentDefinition.isFunctionDeclaration()) &&
componentDefinition.has('id')) {
documentation.set('displayName', getNameOrValue(componentDefinition.get('id')));
}
else if (componentDefinition.isArrowFunctionExpression() ||
componentDefinition.isFunctionExpression() ||
isReactForwardRefCall(componentDefinition)) {
let currentPath = componentDefinition;
while (currentPath.parentPath) {
if (currentPath.parentPath.isVariableDeclarator()) {
documentation.set('displayName', getNameOrValue(currentPath.parentPath.get('id')));
return;
}
else if (currentPath.parentPath.isAssignmentExpression()) {
const leftPath = currentPath.parentPath.get('left');
if (leftPath.isIdentifier() || leftPath.isLiteral()) {
documentation.set('displayName', getNameOrValue(leftPath));
return;
}
}
currentPath = currentPath.parentPath;
}
}
return;
}
displayNamePath = resolveToValue(displayNamePath);
// If display name is defined as function somehow (getter, property with function)
// we resolve the return value of the function
if (displayNamePath.isFunction()) {
displayNamePath = resolveFunctionDefinitionToReturnValue(displayNamePath);
}
if (!displayNamePath ||
(!displayNamePath.isStringLiteral() && !displayNamePath.isNumericLiteral())) {
return;
}
documentation.set('displayName', displayNamePath.node.value);
};
export default displayNameHandler;

View File

@@ -0,0 +1,13 @@
import type { NodePath } from '@babel/traverse';
import type Documentation from '../Documentation.js';
import type { ComponentNode } from '../resolver/index.js';
export { default as componentDocblockHandler } from './componentDocblockHandler.js';
export { default as componentMethodsHandler } from './componentMethodsHandler.js';
export { default as componentMethodsJsDocHandler } from './componentMethodsJsDocHandler.js';
export { default as defaultPropsHandler } from './defaultPropsHandler.js';
export { default as displayNameHandler } from './displayNameHandler.js';
export { default as codeTypeHandler } from './codeTypeHandler.js';
export { default as propDocblockHandler } from './propDocblockHandler.js';
export { default as propTypeCompositionHandler } from './propTypeCompositionHandler.js';
export { propTypeHandler, contextTypeHandler, childContextTypeHandler, } from './propTypeHandler.js';
export type Handler = (documentation: Documentation, componentDefinition: NodePath<ComponentNode>) => void;

View File

@@ -0,0 +1,9 @@
export { default as componentDocblockHandler } from './componentDocblockHandler.js';
export { default as componentMethodsHandler } from './componentMethodsHandler.js';
export { default as componentMethodsJsDocHandler } from './componentMethodsJsDocHandler.js';
export { default as defaultPropsHandler } from './defaultPropsHandler.js';
export { default as displayNameHandler } from './displayNameHandler.js';
export { default as codeTypeHandler } from './codeTypeHandler.js';
export { default as propDocblockHandler } from './propDocblockHandler.js';
export { default as propTypeCompositionHandler } from './propTypeCompositionHandler.js';
export { propTypeHandler, contextTypeHandler, childContextTypeHandler, } from './propTypeHandler.js';

View File

@@ -0,0 +1,3 @@
import type { Handler } from './index.js';
declare const propDocblockHandler: Handler;
export default propDocblockHandler;

View File

@@ -0,0 +1,30 @@
import getMemberValuePath from '../utils/getMemberValuePath.js';
import resolveToValue from '../utils/resolveToValue.js';
import setPropDescription from '../utils/setPropDescription.js';
function resolveDocumentation(documentation, path) {
if (!path.isObjectExpression()) {
return;
}
path.get('properties').forEach((propertyPath) => {
if (propertyPath.isSpreadElement()) {
const resolvedValuePath = resolveToValue(propertyPath.get('argument'));
resolveDocumentation(documentation, resolvedValuePath);
}
else if (propertyPath.isObjectProperty() ||
propertyPath.isObjectMethod()) {
setPropDescription(documentation, propertyPath);
}
});
}
const propDocblockHandler = function (documentation, componentDefinition) {
let propTypesPath = getMemberValuePath(componentDefinition, 'propTypes');
if (!propTypesPath) {
return;
}
propTypesPath = resolveToValue(propTypesPath);
if (!propTypesPath) {
return;
}
resolveDocumentation(documentation, propTypesPath);
};
export default propDocblockHandler;

View File

@@ -0,0 +1,3 @@
import type { Handler } from './index.js';
declare const propTypeCompositionHandler: Handler;
export default propTypeCompositionHandler;

View File

@@ -0,0 +1,36 @@
import getMemberValuePath from '../utils/getMemberValuePath.js';
import resolveToModule from '../utils/resolveToModule.js';
import resolveToValue from '../utils/resolveToValue.js';
/**
* It resolves the path to its module name and adds it to the "composes" entry
* in the documentation.
*/
function amendComposes(documentation, path) {
const moduleName = resolveToModule(path);
if (moduleName) {
documentation.addComposes(moduleName);
}
}
function processObjectExpression(documentation, path) {
path.get('properties').forEach((propertyPath) => {
if (propertyPath.isSpreadElement()) {
amendComposes(documentation, resolveToValue(propertyPath.get('argument')));
}
});
}
const propTypeCompositionHandler = function (documentation, componentDefinition) {
let propTypesPath = getMemberValuePath(componentDefinition, 'propTypes');
if (!propTypesPath) {
return;
}
propTypesPath = resolveToValue(propTypesPath);
if (!propTypesPath) {
return;
}
if (propTypesPath.isObjectExpression()) {
processObjectExpression(documentation, propTypesPath);
return;
}
amendComposes(documentation, propTypesPath);
};
export default propTypeCompositionHandler;

View File

@@ -0,0 +1,4 @@
import type { Handler } from './index.js';
export declare const propTypeHandler: Handler;
export declare const contextTypeHandler: Handler;
export declare const childContextTypeHandler: Handler;

View File

@@ -0,0 +1,71 @@
import getPropType from '../utils/getPropType.js';
import getPropertyName from '../utils/getPropertyName.js';
import getMemberValuePath from '../utils/getMemberValuePath.js';
import isReactModuleName from '../utils/isReactModuleName.js';
import isRequiredPropType from '../utils/isRequiredPropType.js';
import printValue from '../utils/printValue.js';
import resolveToModule from '../utils/resolveToModule.js';
import resolveToValue from '../utils/resolveToValue.js';
function isPropTypesExpression(path) {
const moduleName = resolveToModule(path);
if (moduleName) {
return isReactModuleName(moduleName) || moduleName === 'ReactPropTypes';
}
return false;
}
function amendPropTypes(getDescriptor, path) {
if (!path.isObjectExpression()) {
return;
}
path.get('properties').forEach((propertyPath) => {
if (propertyPath.isObjectProperty()) {
const propName = getPropertyName(propertyPath);
if (!propName)
return;
const propDescriptor = getDescriptor(propName);
const valuePath = resolveToValue(propertyPath.get('value'));
const type = isPropTypesExpression(valuePath)
? getPropType(valuePath)
: { name: 'custom', raw: printValue(valuePath) };
if (type) {
propDescriptor.type = type;
propDescriptor.required =
type.name !== 'custom' && isRequiredPropType(valuePath);
}
}
if (propertyPath.isSpreadElement()) {
const resolvedValuePath = resolveToValue(propertyPath.get('argument'));
if (resolvedValuePath.isObjectExpression()) {
// normal object literal
amendPropTypes(getDescriptor, resolvedValuePath);
}
}
});
}
function getPropTypeHandler(propName) {
return function (documentation, componentDefinition) {
let propTypesPath = getMemberValuePath(componentDefinition, propName);
if (!propTypesPath) {
return;
}
propTypesPath = resolveToValue(propTypesPath);
if (!propTypesPath) {
return;
}
let getDescriptor;
switch (propName) {
case 'childContextTypes':
getDescriptor = documentation.getChildContextDescriptor;
break;
case 'contextTypes':
getDescriptor = documentation.getContextDescriptor;
break;
default:
getDescriptor = documentation.getPropDescriptor;
}
amendPropTypes(getDescriptor.bind(documentation), propTypesPath);
};
}
export const propTypeHandler = getPropTypeHandler('propTypes');
export const contextTypeHandler = getPropTypeHandler('contextTypes');
export const childContextTypeHandler = getPropTypeHandler('childContextTypes');

View File

@@ -0,0 +1,2 @@
declare const defaultFsImporter: import("./index.js").Importer;
export default defaultFsImporter;

View File

@@ -0,0 +1,3 @@
import makeFsImporter from './makeFsImporter.js';
const defaultFsImporter = makeFsImporter();
export default defaultFsImporter;

View File

@@ -0,0 +1,2 @@
declare const ignoreImporter: import("./index.js").Importer;
export default ignoreImporter;

View File

@@ -0,0 +1,3 @@
import makeIgnoreImporter from './makeIgnoreImporter.js';
const ignoreImporter = makeIgnoreImporter();
export default ignoreImporter;

View File

@@ -0,0 +1,9 @@
import type { NodePath } from '@babel/traverse';
import type { ExportAllDeclaration, ExportNamedDeclaration, ImportDeclaration } from '@babel/types';
import type FileState from '../FileState.js';
import ignoreImporter from './ignoreImporter.js';
import fsImporter from './fsImporter.js';
import makeFsImporter from './makeFsImporter.js';
export type ImportPath = NodePath<ExportAllDeclaration | ExportNamedDeclaration | ImportDeclaration>;
export type Importer = (path: ImportPath, name: string, file: FileState) => NodePath | null;
export { fsImporter, ignoreImporter, makeFsImporter };

View File

@@ -0,0 +1,4 @@
import ignoreImporter from './ignoreImporter.js';
import fsImporter from './fsImporter.js';
import makeFsImporter from './makeFsImporter.js';
export { fsImporter, ignoreImporter, makeFsImporter };

View File

@@ -0,0 +1,8 @@
import type { Importer } from './index.js';
import type FileState from '../FileState.js';
interface FsImporterCache {
parseCache: Map<string, FileState>;
resolveCache: Map<string, string | null>;
}
export default function makeFsImporter(lookupModule?: (filename: string, basedir: string) => string, { parseCache, resolveCache }?: FsImporterCache): Importer;
export {};

View File

@@ -0,0 +1,208 @@
import { shallowIgnoreVisitors } from '../utils/traverse.js';
import resolve from 'resolve';
import { dirname, extname } from 'path';
import fs from 'fs';
import { visitors } from '@babel/traverse';
import { resolveObjectPatternPropertyToValue } from '../utils/index.js';
// These extensions are sorted by priority
// resolve() will check for files in the order these extensions are sorted
const RESOLVE_EXTENSIONS = [
'.js',
'.ts',
'.tsx',
'.mjs',
'.cjs',
'.mts',
'.cts',
'.jsx',
];
function defaultLookupModule(filename, basedir) {
const resolveOptions = {
basedir,
extensions: RESOLVE_EXTENSIONS,
// we do not need to check core modules as we cannot import them anyway
includeCoreModules: false,
};
try {
return resolve.sync(filename, resolveOptions);
}
catch (error) {
const ext = extname(filename);
let newFilename;
// if we try to import a JavaScript file it might be that we are actually pointing to
// a TypeScript file. This can happen in ES modules as TypeScript requires to import other
// TypeScript files with .js extensions
// https://www.typescriptlang.org/docs/handbook/esm-node.html#type-in-packagejson-and-new-extensions
switch (ext) {
case '.js':
case '.mjs':
case '.cjs':
newFilename = `${filename.slice(0, -2)}ts`;
break;
case '.jsx':
newFilename = `${filename.slice(0, -3)}tsx`;
break;
default:
throw error;
}
return resolve.sync(newFilename, {
...resolveOptions,
// we already know that there is an extension at this point, so no need to check other extensions
extensions: [],
});
}
}
// Factory for the resolveImports importer
// If this resolver is used in an environment where the source files change (e.g. watch)
// then the cache needs to be cleared on file changes.
export default function makeFsImporter(lookupModule = defaultLookupModule, { parseCache, resolveCache } = {
parseCache: new Map(),
resolveCache: new Map(),
}) {
function resolveImportedValue(path, name, file, seen = new Set()) {
// Bail if no filename was provided for the current source file.
// Also never traverse into react itself.
const source = path.node.source?.value;
const { filename } = file.opts;
if (!source || !filename || source === 'react') {
return null;
}
// Resolve the imported module using the Node resolver
const basedir = dirname(filename);
const resolveCacheKey = `${basedir}|${source}`;
let resolvedSource = resolveCache.get(resolveCacheKey);
// We haven't found it before, so no need to look again
if (resolvedSource === null) {
return null;
}
// First time we try to resolve this file
if (resolvedSource === undefined) {
try {
resolvedSource = lookupModule(source, basedir);
}
catch (error) {
const { code } = error;
if (code === 'MODULE_NOT_FOUND' || code === 'INVALID_PACKAGE_MAIN') {
resolveCache.set(resolveCacheKey, null);
return null;
}
throw error;
}
resolveCache.set(resolveCacheKey, resolvedSource);
}
// Prevent recursive imports
if (seen.has(resolvedSource)) {
return null;
}
seen.add(resolvedSource);
let nextFile = parseCache.get(resolvedSource);
if (!nextFile) {
// Read and parse the code
const src = fs.readFileSync(resolvedSource, 'utf8');
nextFile = file.parse(src, resolvedSource);
parseCache.set(resolvedSource, nextFile);
}
return findExportedValue(nextFile, name, seen);
}
const explodedVisitors = visitors.explode({
...shallowIgnoreVisitors,
ExportNamedDeclaration: {
enter: function (path, state) {
const { file, name, seen } = state;
const declaration = path.get('declaration');
// export const/var ...
if (declaration.hasNode() && declaration.isVariableDeclaration()) {
for (const declPath of declaration.get('declarations')) {
const id = declPath.get('id');
const init = declPath.get('init');
if (id.isIdentifier({ name }) && init.hasNode()) {
// export const/var a = <init>
state.resultPath = init;
break;
}
else if (id.isObjectPattern()) {
// export const/var { a } = <init>
state.resultPath = id.get('properties').find((prop) => {
if (prop.isObjectProperty()) {
const value = prop.get('value');
return value.isIdentifier({ name });
}
// We don't handle RestElement here yet as complicated
return false;
});
if (state.resultPath) {
state.resultPath = resolveObjectPatternPropertyToValue(state.resultPath);
break;
}
}
// ArrayPattern not handled yet
}
}
else if (declaration.hasNode() &&
declaration.has('id') &&
declaration.get('id').isIdentifier({ name })) {
// export function/class/type/interface/enum ...
state.resultPath = declaration;
}
else if (path.has('specifiers')) {
// export { ... } or export x from ... or export * as x from ...
for (const specifierPath of path.get('specifiers')) {
if (specifierPath.isExportNamespaceSpecifier()) {
continue;
}
const exported = specifierPath.get('exported');
if (exported.isIdentifier({ name })) {
// export ... from ''
if (path.has('source')) {
const local = specifierPath.isExportSpecifier()
? specifierPath.node.local.name
: 'default';
state.resultPath = resolveImportedValue(path, local, file, seen);
if (state.resultPath) {
break;
}
}
else {
state.resultPath = specifierPath.get('local');
break;
}
}
}
}
return state.resultPath ? path.stop() : path.skip();
},
},
ExportDefaultDeclaration: {
enter: function (path, state) {
const { name } = state;
if (name === 'default') {
state.resultPath = path.get('declaration');
return path.stop();
}
path.skip();
},
},
ExportAllDeclaration: {
enter: function (path, state) {
const { name, file, seen } = state;
const resolvedPath = resolveImportedValue(path, name, file, seen);
if (resolvedPath) {
state.resultPath = resolvedPath;
return path.stop();
}
path.skip();
},
},
});
// Traverses the program looking for an export that matches the requested name
function findExportedValue(file, name, seen) {
const state = {
file,
name,
seen,
};
file.traverse(explodedVisitors, state);
return state.resultPath || null;
}
return resolveImportedValue;
}

View File

@@ -0,0 +1,2 @@
import type { Importer } from './index.js';
export default function makeIgnoreImporter(): Importer;

View File

@@ -0,0 +1,3 @@
export default function makeIgnoreImporter() {
return () => null;
}

46
frontend/node_modules/react-docgen/dist/main.d.ts generated vendored Normal file
View File

@@ -0,0 +1,46 @@
import type DocumentationBuilder from './Documentation.js';
import type { BaseType, Documentation, ElementsType, FunctionArgumentType, FunctionSignatureType, LiteralType, MethodParameter, MethodReturn, ObjectSignatureType, PropDescriptor, PropTypeDescriptor, SimpleType, TSFunctionSignatureType, TypeDescriptor } from './Documentation.js';
import type FileState from './FileState.js';
import type { Config } from './config.js';
import { defaultHandlers } from './config.js';
import { ERROR_CODES } from './error.js';
import type { Handler } from './handlers/index.js';
import * as builtinHandlers from './handlers/index.js';
import type { Importer } from './importer/index.js';
import { makeFsImporter } from './importer/index.js';
import type { Resolver, ResolverClass, ResolverFunction } from './resolver/index.js';
import * as builtinResolvers from './resolver/index.js';
import * as utils from './utils/index.js';
declare const builtinImporters: {
fsImporter: Importer;
ignoreImporter: Importer;
};
declare module '@babel/traverse' {
interface HubInterface {
file: FileState;
parse: typeof FileState.prototype.parse;
import: typeof FileState.prototype.import;
}
interface Hub {
file: FileState;
parse: typeof FileState.prototype.parse;
import: typeof FileState.prototype.import;
}
}
/**
* Parse the *src* and scan for react components based on the config
* that gets supplied.
*
* The default resolvers look for *exported* react components.
*
* By default all handlers are applied, so that all possible
* different use cases are covered.
*
* The default importer is the fs-importer that tries to resolve
* files based on the nodejs resolve algorithm.
*/
declare function defaultParse(src: Buffer | string, config?: Config): Documentation[];
export type { NodePath } from '@babel/traverse';
export type * as babelTypes from '@babel/types';
export { builtinHandlers, builtinImporters, builtinResolvers, defaultHandlers, ERROR_CODES, makeFsImporter, defaultParse as parse, utils, };
export type { BaseType, Config, Documentation, DocumentationBuilder, ElementsType, FileState, FunctionArgumentType, FunctionSignatureType, Handler, Importer, LiteralType, MethodParameter, MethodReturn, ObjectSignatureType, PropDescriptor, PropTypeDescriptor, Resolver, ResolverClass, ResolverFunction, SimpleType, TSFunctionSignatureType, TypeDescriptor, };

28
frontend/node_modules/react-docgen/dist/main.js generated vendored Normal file
View File

@@ -0,0 +1,28 @@
import { createConfig, defaultHandlers } from './config.js';
import { ERROR_CODES } from './error.js';
import * as builtinHandlers from './handlers/index.js';
import { fsImporter, ignoreImporter, makeFsImporter, } from './importer/index.js';
import parse from './parse.js';
import * as builtinResolvers from './resolver/index.js';
import * as utils from './utils/index.js';
const builtinImporters = {
fsImporter,
ignoreImporter,
};
/**
* Parse the *src* and scan for react components based on the config
* that gets supplied.
*
* The default resolvers look for *exported* react components.
*
* By default all handlers are applied, so that all possible
* different use cases are covered.
*
* The default importer is the fs-importer that tries to resolve
* files based on the nodejs resolve algorithm.
*/
function defaultParse(src, config = {}) {
const defaultConfig = createConfig(config);
return parse(String(src), defaultConfig);
}
export { builtinHandlers, builtinImporters, builtinResolvers, defaultHandlers, ERROR_CODES, makeFsImporter, defaultParse as parse, utils, };

20
frontend/node_modules/react-docgen/dist/parse.d.ts generated vendored Normal file
View File

@@ -0,0 +1,20 @@
import type { Documentation } from './Documentation.js';
import type { InternalConfig } from './config.js';
/**
* Takes JavaScript source code and returns an object with the information
* extract from it.
*
* `resolver` is a strategy to find the AST node(s) of the component
* definition(s) inside `src`.
* It is a function that gets passed the program AST node of
* the source as first argument, and a reference to the parser as second argument.
*
* This allows you define your own strategy for finding component definitions.
*
* `handlers` is an array of functions which are passed a reference to the
* component definitions (extracted by `resolver`) so that they can extract
* information from it. They get also passed a reference to a `Documentation`
* object to attach the information to. A reference to the parser is parsed as the
* last argument.
*/
export default function parse(code: string, config: InternalConfig): Documentation[];

44
frontend/node_modules/react-docgen/dist/parse.js generated vendored Normal file
View File

@@ -0,0 +1,44 @@
import DocumentationBuilder from './Documentation.js';
import postProcessDocumentation from './utils/postProcessDocumentation.js';
import babelParse from './babelParser.js';
import FileState from './FileState.js';
import { ERROR_CODES, ReactDocgenError } from './error.js';
import runResolver from './resolver/utils/runResolver.js';
function executeHandlers(handlers, componentDefinitions) {
return componentDefinitions.map((componentDefinition) => {
const documentation = new DocumentationBuilder();
handlers.forEach((handler) => handler(documentation, componentDefinition));
return postProcessDocumentation(documentation.build());
});
}
/**
* Takes JavaScript source code and returns an object with the information
* extract from it.
*
* `resolver` is a strategy to find the AST node(s) of the component
* definition(s) inside `src`.
* It is a function that gets passed the program AST node of
* the source as first argument, and a reference to the parser as second argument.
*
* This allows you define your own strategy for finding component definitions.
*
* `handlers` is an array of functions which are passed a reference to the
* component definitions (extracted by `resolver`) so that they can extract
* information from it. They get also passed a reference to a `Documentation`
* object to attach the information to. A reference to the parser is parsed as the
* last argument.
*/
export default function parse(code, config) {
const { babelOptions, handlers, importer, resolver } = config;
const ast = babelParse(code, babelOptions);
const fileState = new FileState(babelOptions, {
ast,
code,
importer,
});
const componentDefinitions = runResolver(resolver, fileState);
if (componentDefinitions.length === 0) {
throw new ReactDocgenError(ERROR_CODES.MISSING_DEFINITION);
}
return executeHandlers(handlers, componentDefinitions);
}

View File

@@ -0,0 +1,19 @@
import type FileState from '../FileState.js';
import type { ComponentNodePath, Resolver, ResolverClass } from './index.js';
declare enum ChainingLogic {
ALL = 0,
FIRST_FOUND = 1
}
interface ChainResolverOptions {
chainingLogic?: ChainingLogic;
}
export default class ChainResolver implements ResolverClass {
resolvers: Resolver[];
options: ChainResolverOptions;
static Logic: typeof ChainingLogic;
constructor(resolvers: Resolver[], options: ChainResolverOptions);
private resolveFirstOnly;
private resolveAll;
resolve(file: FileState): ComponentNodePath[];
}
export {};

View File

@@ -0,0 +1,39 @@
import runResolver from './utils/runResolver.js';
var ChainingLogic;
(function (ChainingLogic) {
ChainingLogic[ChainingLogic["ALL"] = 0] = "ALL";
ChainingLogic[ChainingLogic["FIRST_FOUND"] = 1] = "FIRST_FOUND";
})(ChainingLogic || (ChainingLogic = {}));
class ChainResolver {
constructor(resolvers, options) {
this.resolvers = resolvers;
this.options = options;
}
resolveFirstOnly(file) {
for (const resolver of this.resolvers) {
const components = runResolver(resolver, file);
if (components.length > 0) {
return components;
}
}
return [];
}
resolveAll(file) {
const allComponents = new Set();
for (const resolver of this.resolvers) {
const components = runResolver(resolver, file);
components.forEach((component) => {
allComponents.add(component);
});
}
return Array.from(allComponents);
}
resolve(file) {
if (this.options.chainingLogic === ChainingLogic.FIRST_FOUND) {
return this.resolveFirstOnly(file);
}
return this.resolveAll(file);
}
}
ChainResolver.Logic = ChainingLogic;
export default ChainResolver;

View File

@@ -0,0 +1,9 @@
import type FileState from '../FileState.js';
import type { ComponentNodePath, ResolverClass } from './index.js';
/**
* Given an AST, this function tries to find all object expressions that are
* passed to `React.createClass` calls, by resolving all references properly.
*/
export default class FindAllDefinitionsResolver implements ResolverClass {
resolve(file: FileState): ComponentNodePath[];
}

View File

@@ -0,0 +1,66 @@
import isReactComponentClass from '../utils/isReactComponentClass.js';
import isReactCreateClassCall from '../utils/isReactCreateClassCall.js';
import isReactForwardRefCall from '../utils/isReactForwardRefCall.js';
import isStatelessComponent from '../utils/isStatelessComponent.js';
import normalizeClassDefinition from '../utils/normalizeClassDefinition.js';
import resolveToValue from '../utils/resolveToValue.js';
import { visitors } from '@babel/traverse';
function classVisitor(path, state) {
if (isReactComponentClass(path)) {
normalizeClassDefinition(path);
state.foundDefinitions.add(path);
}
path.skip();
}
function statelessVisitor(path, state) {
if (isStatelessComponent(path)) {
state.foundDefinitions.add(path);
}
path.skip();
}
const explodedVisitors = visitors.explode({
FunctionDeclaration: { enter: statelessVisitor },
FunctionExpression: { enter: statelessVisitor },
ObjectMethod: { enter: statelessVisitor },
ArrowFunctionExpression: { enter: statelessVisitor },
ClassExpression: { enter: classVisitor },
ClassDeclaration: { enter: classVisitor },
CallExpression: {
enter: function (path, state) {
const argument = path.get('arguments')[0];
if (!argument) {
return;
}
if (isReactForwardRefCall(path)) {
// If the the inner function was previously identified as a component
// replace it with the parent node
const inner = resolveToValue(argument);
state.foundDefinitions.delete(inner);
state.foundDefinitions.add(path);
// Do not traverse into arguments
return path.skip();
}
else if (isReactCreateClassCall(path)) {
const resolvedPath = resolveToValue(argument);
if (resolvedPath.isObjectExpression()) {
state.foundDefinitions.add(resolvedPath);
}
// Do not traverse into arguments
return path.skip();
}
},
},
});
/**
* Given an AST, this function tries to find all object expressions that are
* passed to `React.createClass` calls, by resolving all references properly.
*/
export default class FindAllDefinitionsResolver {
resolve(file) {
const state = {
foundDefinitions: new Set(),
};
file.traverse(explodedVisitors, state);
return Array.from(state.foundDefinitions);
}
}

View File

@@ -0,0 +1,15 @@
import type FileState from '../FileState.js';
import type { ComponentNodePath, ResolverClass } from './index.js';
interface FindAnnotatedDefinitionsResolverOptions {
annotation?: string;
}
/**
* Given an AST, this function tries to find all react components which
* are annotated with an annotation
*/
export default class FindAnnotatedDefinitionsResolver implements ResolverClass {
annotation: string;
constructor({ annotation, }?: FindAnnotatedDefinitionsResolverOptions);
resolve(file: FileState): ComponentNodePath[];
}
export {};

View File

@@ -0,0 +1,62 @@
import normalizeClassDefinition from '../utils/normalizeClassDefinition.js';
import { visitors } from '@babel/traverse';
function isAnnotated(path, annotation) {
let inspectPath = path;
do {
const leadingComments = inspectPath.node.leadingComments;
// If an export doesn't have leading comments, we can simply continue
if (leadingComments && leadingComments.length > 0) {
// Search for the annotation in any comment.
const hasAnnotation = leadingComments.some(({ value }) => value.includes(annotation));
// if we found an annotation return true
if (hasAnnotation) {
return true;
}
}
// return false if the container of the current path is an array
// as we do not want to traverse up through this kind of nodes, like ArrayExpressions for example
// The only exception is variable declarations
if (Array.isArray(inspectPath.container) &&
!inspectPath.isVariableDeclarator() &&
!inspectPath.parentPath?.isCallExpression()) {
return false;
}
} while ((inspectPath = inspectPath.parentPath));
return false;
}
function classVisitor(path, state) {
if (isAnnotated(path, state.annotation)) {
normalizeClassDefinition(path);
state.foundDefinitions.add(path);
}
}
function statelessVisitor(path, state) {
if (isAnnotated(path, state.annotation)) {
state.foundDefinitions.add(path);
}
}
const explodedVisitors = visitors.explode({
ArrowFunctionExpression: { enter: statelessVisitor },
FunctionDeclaration: { enter: statelessVisitor },
FunctionExpression: { enter: statelessVisitor },
ObjectMethod: { enter: statelessVisitor },
ClassDeclaration: { enter: classVisitor },
ClassExpression: { enter: classVisitor },
});
/**
* Given an AST, this function tries to find all react components which
* are annotated with an annotation
*/
export default class FindAnnotatedDefinitionsResolver {
constructor({ annotation = '@component', } = {}) {
this.annotation = annotation;
}
resolve(file) {
const state = {
foundDefinitions: new Set(),
annotation: this.annotation,
};
file.traverse(explodedVisitors, state);
return Array.from(state.foundDefinitions);
}
}

View File

@@ -0,0 +1,28 @@
import type FileState from '../FileState.js';
import type { ComponentNodePath, ResolverClass } from './index.js';
interface FindExportedDefinitionsResolverOptions {
limit?: number;
}
/**
* Given an AST, this function tries to find the exported component definitions.
*
* The component definitions are either the ObjectExpression passed to
* `React.createClass` or a `class` definition extending `React.Component` or
* having a `render()` method.
*
* If a definition is part of the following statements, it is considered to be
* exported:
*
* modules.exports = Definition;
* exports.foo = Definition;
* export default Definition;
* export var Definition = ...;
*
* limit can be used to limit the components to be found. When the limit is reached an error will be thrown
*/
export default class FindExportedDefinitionsResolver implements ResolverClass {
limit: number;
constructor({ limit }?: FindExportedDefinitionsResolverOptions);
resolve(file: FileState): ComponentNodePath[];
}
export {};

View File

@@ -0,0 +1,75 @@
import isExportsOrModuleAssignment from '../utils/isExportsOrModuleAssignment.js';
import resolveExportDeclaration from '../utils/resolveExportDeclaration.js';
import resolveToValue from '../utils/resolveToValue.js';
import { visitors } from '@babel/traverse';
import { shallowIgnoreVisitors } from '../utils/traverse.js';
import findComponentDefinition from '../utils/findComponentDefinition.js';
import { ERROR_CODES, ReactDocgenError } from '../error.js';
function exportDeclaration(path, state) {
resolveExportDeclaration(path).forEach((exportedPath) => {
const definition = findComponentDefinition(exportedPath);
if (definition) {
if (state.limit > 0 && state.foundDefinitions.size > 0) {
// If a file exports multiple components, ... complain!
throw new ReactDocgenError(ERROR_CODES.MULTIPLE_DEFINITIONS);
}
state.foundDefinitions.add(definition);
}
});
return path.skip();
}
function assignmentExpressionVisitor(path, state) {
// Ignore anything that is not `exports.X = ...;` or
// `module.exports = ...;`
if (!isExportsOrModuleAssignment(path)) {
return path.skip();
}
// Resolve the value of the right hand side. It should resolve to a call
// expression, something like React.createClass
const resolvedPath = resolveToValue(path.get('right'));
const definition = findComponentDefinition(resolvedPath);
if (definition) {
if (state.limit > 0 && state.foundDefinitions.size > 0) {
// If a file exports multiple components, ... complain!
throw new ReactDocgenError(ERROR_CODES.MULTIPLE_DEFINITIONS);
}
state.foundDefinitions.add(definition);
}
return path.skip();
}
const explodedVisitors = visitors.explode({
...shallowIgnoreVisitors,
ExportNamedDeclaration: { enter: exportDeclaration },
ExportDefaultDeclaration: { enter: exportDeclaration },
AssignmentExpression: { enter: assignmentExpressionVisitor },
});
/**
* Given an AST, this function tries to find the exported component definitions.
*
* The component definitions are either the ObjectExpression passed to
* `React.createClass` or a `class` definition extending `React.Component` or
* having a `render()` method.
*
* If a definition is part of the following statements, it is considered to be
* exported:
*
* modules.exports = Definition;
* exports.foo = Definition;
* export default Definition;
* export var Definition = ...;
*
* limit can be used to limit the components to be found. When the limit is reached an error will be thrown
*/
export default class FindExportedDefinitionsResolver {
constructor({ limit = 0 } = {}) {
this.limit = limit;
}
resolve(file) {
const state = {
foundDefinitions: new Set(),
limit: this.limit,
};
file.traverse(explodedVisitors, state);
return Array.from(state.foundDefinitions);
}
}

View File

@@ -0,0 +1,16 @@
import ChainResolver from './ChainResolver.js';
import FindAllDefinitionsResolver from './FindAllDefinitionsResolver.js';
import FindAnnotatedDefinitionsResolver from './FindAnnotatedDefinitionsResolver.js';
import FindExportedDefinitionsResolver from './FindExportedDefinitionsResolver.js';
import type { NodePath } from '@babel/traverse';
import type FileState from '../FileState.js';
import type { ArrowFunctionExpression, CallExpression, ClassDeclaration, ClassExpression, FunctionDeclaration, FunctionExpression, ObjectExpression, ObjectMethod } from '@babel/types';
export type StatelessComponentNode = ArrowFunctionExpression | FunctionDeclaration | FunctionExpression | ObjectMethod;
export type ComponentNode = CallExpression | ClassDeclaration | ClassExpression | ObjectExpression | StatelessComponentNode;
export type ComponentNodePath = NodePath<ComponentNode>;
export type ResolverFunction = (file: FileState) => ComponentNodePath[];
export interface ResolverClass {
resolve: ResolverFunction;
}
export type Resolver = ResolverClass | ResolverFunction;
export { FindAllDefinitionsResolver, FindAnnotatedDefinitionsResolver, FindExportedDefinitionsResolver, ChainResolver, };

View File

@@ -0,0 +1,5 @@
import ChainResolver from './ChainResolver.js';
import FindAllDefinitionsResolver from './FindAllDefinitionsResolver.js';
import FindAnnotatedDefinitionsResolver from './FindAnnotatedDefinitionsResolver.js';
import FindExportedDefinitionsResolver from './FindExportedDefinitionsResolver.js';
export { FindAllDefinitionsResolver, FindAnnotatedDefinitionsResolver, FindExportedDefinitionsResolver, ChainResolver, };

View File

@@ -0,0 +1,3 @@
import type FileState from '../../FileState.js';
import type { Resolver, ResolverFunction } from '../index.js';
export default function runResolver(resolver: Resolver, file: FileState): ReturnType<ResolverFunction>;

View File

@@ -0,0 +1,6 @@
function isResolverClass(resolver) {
return typeof resolver === 'object';
}
export default function runResolver(resolver, file) {
return isResolverClass(resolver) ? resolver.resolve(file) : resolver(file);
}

View File

@@ -0,0 +1,14 @@
/**
* Helper functions to work with docblock comments.
*/
import type { NodePath } from '@babel/traverse';
/**
* Given a path, this function returns the closest preceding docblock if it
* exists.
*/
export declare function getDocblock(path: NodePath, trailing?: boolean): string | null;
/**
* Given a string, this functions returns an object with doclet names as keys
* and their "content" as values.
*/
export declare function getDoclets(str: string): Record<string, string>;

View File

@@ -0,0 +1,39 @@
/**
* Helper functions to work with docblock comments.
*/
const DOCLET_PATTERN = /^@(\w+)(?:$|\s((?:[^](?!^@\w))*))/gim;
function parseDocblock(str) {
// Does not use \s in the regex as this would match also \n and conflicts
// with windows line endings.
return str.replace(/^[ \t]*\*[ \t]?/gm, '').trim();
}
const DOCBLOCK_HEADER = /^\*\s/;
/**
* Given a path, this function returns the closest preceding docblock if it
* exists.
*/
export function getDocblock(path, trailing = false) {
let comments = [];
if (trailing && path.node.trailingComments) {
comments = path.node.trailingComments.filter((comment) => comment.type === 'CommentBlock' && DOCBLOCK_HEADER.test(comment.value));
}
else if (path.node.leadingComments) {
comments = path.node.leadingComments.filter((comment) => comment.type === 'CommentBlock' && DOCBLOCK_HEADER.test(comment.value));
}
if (comments.length > 0) {
return parseDocblock(comments[comments.length - 1].value);
}
return null;
}
/**
* Given a string, this functions returns an object with doclet names as keys
* and their "content" as values.
*/
export function getDoclets(str) {
const doclets = Object.create(null);
let match;
while ((match = DOCLET_PATTERN.exec(str))) {
doclets[match[1]] = match[2] || true;
}
return doclets;
}

View File

@@ -0,0 +1,12 @@
import type { NodePath } from '@babel/traverse';
import type { Node } from '@babel/types';
/**
* Splits a MemberExpression or CallExpression into parts.
* E.g. foo.bar.baz becomes ['foo', 'bar', 'baz']
*/
declare function toArray(path: NodePath<Node | null>): string[];
/**
* Creates a string representation of a member expression.
*/
declare function toString(path: NodePath<Node | null>): string;
export { toString as String, toArray as Array };

View File

@@ -0,0 +1,96 @@
/*eslint no-loop-func: 0, no-use-before-define: 0*/
import resolveToValue from './resolveToValue.js';
/**
* Splits a MemberExpression or CallExpression into parts.
* E.g. foo.bar.baz becomes ['foo', 'bar', 'baz']
*/
function toArray(path) {
const parts = [path];
let result = [];
while (parts.length > 0) {
path = parts.shift();
if (path.isCallExpression()) {
parts.push(path.get('callee'));
continue;
}
else if (path.isMemberExpression()) {
parts.push(path.get('object'));
const property = path.get('property');
if (path.node.computed) {
const resolvedPath = resolveToValue(property);
if (resolvedPath !== undefined) {
result = result.concat(toArray(resolvedPath));
}
else {
result.push('<computed>');
}
}
else if (property.isIdentifier()) {
result.push(property.node.name);
}
else if (property.isPrivateName()) {
// new test
result.push(`#${property.get('id').node.name}`);
}
continue;
}
else if (path.isIdentifier()) {
result.push(path.node.name);
continue;
}
else if (path.isTSAsExpression()) {
const expression = path.get('expression');
if (expression.isIdentifier()) {
result.push(expression.node.name);
}
continue;
}
else if (path.isLiteral() && path.node.extra?.raw) {
result.push(path.node.extra.raw);
continue;
}
else if (path.isThisExpression()) {
result.push('this');
continue;
}
else if (path.isObjectExpression()) {
const properties = path.get('properties').map(function (property) {
if (property.isSpreadElement()) {
return `...${toString(property.get('argument'))}`;
}
else if (property.isObjectProperty()) {
return (toString(property.get('key')) +
': ' +
toString(property.get('value')));
}
else if (property.isObjectMethod()) {
return toString(property.get('key')) + ': <function>';
}
else {
throw new Error('Unrecognized object property type');
}
});
result.push('{' + properties.join(', ') + '}');
continue;
}
else if (path.isArrayExpression()) {
result.push('[' +
path
.get('elements')
.map(function (el) {
return toString(el);
})
.join(', ') +
']');
continue;
}
}
return result.reverse();
}
/**
* Creates a string representation of a member expression.
*/
function toString(path) {
return toArray(path).join('.');
}
export { toString as String, toArray as Array };

View File

@@ -0,0 +1,3 @@
import type { NodePath } from '@babel/traverse';
import type { ComponentNode } from '../resolver/index.js';
export default function findComponentDefinition(path: NodePath): NodePath<ComponentNode> | null;

View File

@@ -0,0 +1,41 @@
import isReactComponentClass from './isReactComponentClass.js';
import isReactCreateClassCall from './isReactCreateClassCall.js';
import isReactForwardRefCall from './isReactForwardRefCall.js';
import isStatelessComponent from './isStatelessComponent.js';
import normalizeClassDefinition from './normalizeClassDefinition.js';
import resolveHOC from './resolveHOC.js';
import resolveToValue from './resolveToValue.js';
function isComponentDefinition(path) {
return (isReactCreateClassCall(path) ||
isReactComponentClass(path) ||
isStatelessComponent(path) ||
isReactForwardRefCall(path));
}
function resolveComponentDefinition(definition) {
if (isReactCreateClassCall(definition)) {
// return argument
const resolvedPath = resolveToValue(definition.get('arguments')[0]);
if (resolvedPath.isObjectExpression()) {
return resolvedPath;
}
}
else if (isReactComponentClass(definition)) {
normalizeClassDefinition(definition);
return definition;
}
else if (isStatelessComponent(definition) ||
isReactForwardRefCall(definition)) {
return definition;
}
return null;
}
export default function findComponentDefinition(path) {
let resolvedPath = path;
if (!isComponentDefinition(resolvedPath)) {
resolvedPath = resolveToValue(resolveHOC(resolvedPath));
if (!isComponentDefinition(resolvedPath)) {
return null;
}
}
return resolveComponentDefinition(resolvedPath);
}

View File

@@ -0,0 +1,11 @@
import type { NodePath } from '@babel/traverse';
type Predicate<T extends NodePath> = (path: NodePath) => path is T;
/**
* This can be used in two ways
* 1. Find the first return path that passes the predicate function
* (for example to check if a function is returning something)
* 2. Find all occurrences of return values
* For this the predicate acts more like a collector and always needs to return false
*/
export default function findFunctionReturn<T extends NodePath = NodePath>(path: NodePath, predicate: Predicate<T>): T | undefined;
export {};

View File

@@ -0,0 +1,102 @@
import { visitors } from '@babel/traverse';
import resolveToValue from './resolveToValue.js';
import { ignore } from './traverse.js';
const explodedVisitors = visitors.explode({
Function: { enter: ignore },
Class: { enter: ignore },
ObjectExpression: { enter: ignore },
ReturnStatement: {
enter: function (path, state) {
const argument = path.get('argument');
if (argument.hasNode()) {
const resolvedPath = resolvesToFinalValue(argument, state.predicate, state.seen);
if (resolvedPath) {
state.resolvedReturnPath = resolvedPath;
path.stop();
}
}
},
},
});
function resolvesToFinalValue(path, predicate, seen) {
// avoid returns with recursive function calls
if (seen.has(path)) {
return;
}
seen.add(path);
// Is the path already passes then return it.
if (predicate(path)) {
return path;
}
const resolvedPath = resolveToValue(path);
// If the resolved path is already passing then no need to further check
// Only do this if the resolvedPath actually resolved something as otherwise we did this check already
if (resolvedPath.node !== path.node && predicate(resolvedPath)) {
return resolvedPath;
}
// If the path points to a conditional expression, then we need to look only at
// the two possible paths
if (resolvedPath.isConditionalExpression()) {
return (resolvesToFinalValue(resolvedPath.get('consequent'), predicate, seen) ||
resolvesToFinalValue(resolvedPath.get('alternate'), predicate, seen));
}
// If the path points to a logical expression (AND, OR, ...), then we need to look only at
// the two possible paths
if (resolvedPath.isLogicalExpression()) {
return (resolvesToFinalValue(resolvedPath.get('left'), predicate, seen) ||
resolvesToFinalValue(resolvedPath.get('right'), predicate, seen));
}
// If we have a call expression, lets try to follow it
if (resolvedPath.isCallExpression()) {
const returnValue = findFunctionReturnWithCache(resolveToValue(resolvedPath.get('callee')), predicate, seen);
if (returnValue) {
return returnValue;
}
}
return;
}
/**
* This can be used in two ways
* 1. Find the first return path that passes the predicate function
* (for example to check if a function is returning something)
* 2. Find all occurrences of return values
* For this the predicate acts more like a collector and always needs to return false
*/
function findFunctionReturnWithCache(path, predicate, seen) {
let functionPath = path;
if (functionPath.isObjectProperty()) {
functionPath = functionPath.get('value');
}
else if (functionPath.isClassProperty()) {
const classPropertyValue = functionPath.get('value');
if (classPropertyValue.hasNode()) {
functionPath = classPropertyValue;
}
}
if (!functionPath.isFunction()) {
return;
}
// skip traversing for ArrowFunctionExpressions with no block
if (path.isArrowFunctionExpression()) {
const body = path.get('body');
if (!body.isBlockStatement()) {
return resolvesToFinalValue(body, predicate, seen);
}
}
const state = {
predicate,
seen,
};
path.traverse(explodedVisitors, state);
return state.resolvedReturnPath;
}
/**
* This can be used in two ways
* 1. Find the first return path that passes the predicate function
* (for example to check if a function is returning something)
* 2. Find all occurrences of return values
* For this the predicate acts more like a collector and always needs to return false
*/
export default function findFunctionReturn(path, predicate) {
return findFunctionReturnWithCache(path, predicate, new WeakSet());
}

View File

@@ -0,0 +1,13 @@
import type { NodePath } from '@babel/traverse';
import type { GenericTypeAnnotation } from '@babel/types';
/**
* See `supportedUtilityTypes` for which types are supported and
* https://flow.org/en/docs/types/utilities/ for which types are available.
*/
export declare function isSupportedUtilityType(path: NodePath): path is NodePath<GenericTypeAnnotation>;
/**
* Unwraps well known utility types. For example:
*
* $ReadOnly<T> => T
*/
export declare function unwrapUtilityType(path: NodePath): NodePath;

View File

@@ -0,0 +1,32 @@
/**
* See `supportedUtilityTypes` for which types are supported and
* https://flow.org/en/docs/types/utilities/ for which types are available.
*/
export function isSupportedUtilityType(path) {
if (path.isGenericTypeAnnotation()) {
const idPath = path.get('id');
if (idPath.isIdentifier()) {
const name = idPath.node.name;
return name === '$Exact' || name === '$ReadOnly';
}
}
return false;
}
/**
* Unwraps well known utility types. For example:
*
* $ReadOnly<T> => T
*/
export function unwrapUtilityType(path) {
let resultPath = path;
while (isSupportedUtilityType(resultPath)) {
const typeParameters = resultPath.get('typeParameters');
if (!typeParameters.hasNode())
break;
const firstParam = typeParameters.get('params')[0];
if (!firstParam)
break;
resultPath = firstParam;
}
return resultPath;
}

View File

@@ -0,0 +1,3 @@
import type { NodePath } from '@babel/traverse';
import type { ClassDeclaration, ClassExpression, ClassMethod, Expression } from '@babel/types';
export default function getClassMemberValuePath(classDefinition: NodePath<ClassDeclaration | ClassExpression>, memberName: string): NodePath<ClassMethod | Expression> | null;

View File

@@ -0,0 +1,23 @@
import getNameOrValue from './getNameOrValue.js';
export default function getClassMemberValuePath(classDefinition, memberName) {
const classMember = classDefinition
.get('body')
.get('body')
.find((memberPath) => {
if ((memberPath.isClassMethod() && memberPath.node.kind !== 'set') ||
memberPath.isClassProperty()) {
const key = memberPath.get('key');
return ((!memberPath.node.computed || key.isLiteral()) &&
getNameOrValue(key) === memberName);
}
return false;
});
if (classMember) {
// For ClassProperty we return the value and for ClassMethod
// we return itself
return classMember.isClassMethod()
? classMember
: classMember.get('value');
}
return null;
}

View File

@@ -0,0 +1,12 @@
import type { TypeParameters } from '../utils/getTypeParameters.js';
import type { TypeDescriptor } from '../Documentation.js';
import type { NodePath } from '@babel/traverse';
import type { FlowType } from '@babel/types';
/**
* Tries to identify the flow type by inspecting the path for known
* flow type names. This method doesn't check whether the found type is actually
* existing. It simply assumes that a match is always valid.
*
* If there is no match, "unknown" is returned.
*/
export default function getFlowType(path: NodePath<FlowType>, typeParams?: TypeParameters | null): TypeDescriptor;

View File

@@ -0,0 +1,343 @@
import getPropertyName from './getPropertyName.js';
import printValue from './printValue.js';
import getTypeAnnotation from '../utils/getTypeAnnotation.js';
import resolveToValue from '../utils/resolveToValue.js';
import { resolveObjectToNameArray } from '../utils/resolveObjectKeysToArray.js';
import getTypeParameters from '../utils/getTypeParameters.js';
import { getDocblock } from './docblock.js';
const flowTypes = {
AnyTypeAnnotation: 'any',
BooleanTypeAnnotation: 'boolean',
MixedTypeAnnotation: 'mixed',
NullLiteralTypeAnnotation: 'null',
NumberTypeAnnotation: 'number',
StringTypeAnnotation: 'string',
VoidTypeAnnotation: 'void',
EmptyTypeAnnotation: 'empty',
};
const flowLiteralTypes = {
BooleanLiteralTypeAnnotation: 1,
NumberLiteralTypeAnnotation: 1,
StringLiteralTypeAnnotation: 1,
};
const namedTypes = {
ArrayTypeAnnotation: handleArrayTypeAnnotation,
GenericTypeAnnotation: handleGenericTypeAnnotation,
ObjectTypeAnnotation: handleObjectTypeAnnotation,
InterfaceDeclaration: handleInterfaceDeclaration,
UnionTypeAnnotation: handleUnionTypeAnnotation,
NullableTypeAnnotation: handleNullableTypeAnnotation,
FunctionTypeAnnotation: handleFunctionTypeAnnotation,
IntersectionTypeAnnotation: handleIntersectionTypeAnnotation,
TupleTypeAnnotation: handleTupleTypeAnnotation,
TypeofTypeAnnotation: handleTypeofTypeAnnotation,
IndexedAccessType: handleIndexedAccessType,
};
function getFlowTypeWithRequirements(path, typeParams) {
const type = getFlowTypeWithResolvedTypes(path, typeParams);
type.required =
'optional' in path.parentPath.node ? !path.parentPath.node.optional : true;
return type;
}
function handleKeysHelper(path) {
const typeParams = path.get('typeParameters');
if (!typeParams.hasNode()) {
return null;
}
let value = typeParams.get('params')[0];
if (!value) {
return null;
}
if (value.isTypeofTypeAnnotation()) {
value = value.get('argument').get('id');
}
else if (!value.isObjectTypeAnnotation()) {
value = value.get('id');
}
const resolvedPath = value.hasNode() ? resolveToValue(value) : value;
if (resolvedPath.isObjectExpression() ||
resolvedPath.isObjectTypeAnnotation()) {
const keys = resolveObjectToNameArray(resolvedPath, true);
if (keys) {
return {
name: 'union',
raw: printValue(path),
elements: keys.map((key) => ({ name: 'literal', value: key })),
};
}
}
return null;
}
function handleArrayTypeAnnotation(path, typeParams) {
return {
name: 'Array',
elements: [
getFlowTypeWithResolvedTypes(path.get('elementType'), typeParams),
],
raw: printValue(path),
};
}
function handleGenericTypeAnnotation(path, typeParams) {
const id = path.get('id');
const typeParameters = path.get('typeParameters');
if (id.isIdentifier({ name: '$Keys' }) && typeParameters.hasNode()) {
return handleKeysHelper(path);
}
let type;
if (id.isQualifiedTypeIdentifier()) {
const qualification = id.get('qualification');
if (qualification.isIdentifier({ name: 'React' })) {
type = {
name: `${qualification.node.name}${id.node.id.name}`,
raw: printValue(id),
};
}
else {
type = { name: printValue(id).replace(/<.*>$/, '') };
}
}
else {
type = { name: id.node.name };
}
const resolvedPath = (typeParams && typeParams[type.name]) || resolveToValue(path.get('id'));
if (typeParameters.hasNode() && resolvedPath.has('typeParameters')) {
typeParams = getTypeParameters(resolvedPath.get('typeParameters'), typeParameters, typeParams);
}
if (typeParams &&
typeParams[type.name] &&
typeParams[type.name].isGenericTypeAnnotation()) {
return type;
}
if (typeParams && typeParams[type.name]) {
type = getFlowTypeWithResolvedTypes(resolvedPath, typeParams);
}
if (resolvedPath && resolvedPath.has('right')) {
type = getFlowTypeWithResolvedTypes(resolvedPath.get('right'), typeParams);
}
else if (typeParameters.hasNode()) {
const params = typeParameters.get('params');
type = {
...type,
elements: params.map((param) => getFlowTypeWithResolvedTypes(param, typeParams)),
raw: printValue(path),
};
}
return type;
}
function handleObjectTypeAnnotation(path, typeParams) {
const type = {
name: 'signature',
type: 'object',
raw: printValue(path),
signature: { properties: [] },
};
const callProperties = path.get('callProperties');
if (Array.isArray(callProperties)) {
callProperties.forEach((param) => {
type.signature.constructor = getFlowTypeWithResolvedTypes(param.get('value'), typeParams);
});
}
const indexers = path.get('indexers');
if (Array.isArray(indexers)) {
indexers.forEach((param) => {
const typeDescriptor = {
key: getFlowTypeWithResolvedTypes(param.get('key'), typeParams),
value: getFlowTypeWithRequirements(param.get('value'), typeParams),
};
const docblock = getDocblock(param);
if (docblock) {
typeDescriptor.description = docblock;
}
type.signature.properties.push(typeDescriptor);
});
}
path.get('properties').forEach((param) => {
if (param.isObjectTypeProperty()) {
const typeDescriptor = {
// For ObjectTypeProperties `getPropertyName` always returns string
key: getPropertyName(param),
value: getFlowTypeWithRequirements(param.get('value'), typeParams),
};
const docblock = getDocblock(param);
if (docblock) {
typeDescriptor.description = docblock;
}
type.signature.properties.push(typeDescriptor);
}
else if (param.isObjectTypeSpreadProperty()) {
let spreadObject = resolveToValue(param.get('argument'));
if (spreadObject.isGenericTypeAnnotation()) {
const typeAlias = resolveToValue(spreadObject.get('id'));
if (typeAlias.isTypeAlias() &&
typeAlias.get('right').isObjectTypeAnnotation()) {
spreadObject = resolveToValue(typeAlias.get('right'));
}
}
if (spreadObject.isObjectTypeAnnotation()) {
const props = handleObjectTypeAnnotation(spreadObject, typeParams);
type.signature.properties.push(...props.signature.properties);
}
}
});
return type;
}
function handleInterfaceDeclaration(path) {
// Interfaces are handled like references which would be documented separately,
// rather than inlined like type aliases.
return {
name: path.node.id.name,
};
}
function handleUnionTypeAnnotation(path, typeParams) {
return {
name: 'union',
raw: printValue(path),
elements: path
.get('types')
.map((subType) => getFlowTypeWithResolvedTypes(subType, typeParams)),
};
}
function handleIntersectionTypeAnnotation(path, typeParams) {
return {
name: 'intersection',
raw: printValue(path),
elements: path
.get('types')
.map((subType) => getFlowTypeWithResolvedTypes(subType, typeParams)),
};
}
function handleNullableTypeAnnotation(path, typeParams) {
const typeAnnotation = getTypeAnnotation(path);
if (!typeAnnotation)
return null;
const type = getFlowTypeWithResolvedTypes(typeAnnotation, typeParams);
type.nullable = true;
return type;
}
function handleFunctionTypeAnnotation(path, typeParams) {
const type = {
name: 'signature',
type: 'function',
raw: printValue(path),
signature: {
arguments: [],
return: getFlowTypeWithResolvedTypes(path.get('returnType'), typeParams),
},
};
path.get('params').forEach((param) => {
const typeAnnotation = getTypeAnnotation(param);
type.signature.arguments.push({
name: param.node.name ? param.node.name.name : '',
type: typeAnnotation
? getFlowTypeWithResolvedTypes(typeAnnotation, typeParams)
: undefined,
});
});
const rest = path.get('rest');
if (rest.hasNode()) {
const typeAnnotation = getTypeAnnotation(rest);
type.signature.arguments.push({
name: rest.node.name ? rest.node.name.name : '',
type: typeAnnotation
? getFlowTypeWithResolvedTypes(typeAnnotation, typeParams)
: undefined,
rest: true,
});
}
return type;
}
function handleTupleTypeAnnotation(path, typeParams) {
const type = {
name: 'tuple',
raw: printValue(path),
elements: [],
};
path.get('types').forEach((param) => {
type.elements.push(getFlowTypeWithResolvedTypes(param, typeParams));
});
return type;
}
function handleTypeofTypeAnnotation(path, typeParams) {
return getFlowTypeWithResolvedTypes(path.get('argument'), typeParams);
}
function handleIndexedAccessType(path, typeParams) {
const objectType = getFlowTypeWithResolvedTypes(path.get('objectType'), typeParams);
const indexType = getFlowTypeWithResolvedTypes(path.get('indexType'), typeParams);
// We only get the signature if the objectType is a type (vs interface)
if (!objectType.signature) {
return {
name: `${objectType.name}[${indexType.value ? indexType.value.toString() : indexType.name}]`,
raw: printValue(path),
};
}
const resolvedType = objectType.signature.properties.find((p) => {
// indexType.value = "'foo'"
return indexType.value && p.key === indexType.value.replace(/['"]+/g, '');
});
if (!resolvedType) {
return { name: 'unknown' };
}
return {
name: resolvedType.value.name,
raw: printValue(path),
};
}
let visitedTypes = {};
function getFlowTypeWithResolvedTypes(path, typeParams) {
let type = null;
const parent = path.parentPath;
const isTypeAlias = parent.isTypeAlias();
// When we see a TypeAlias mark it as visited so that the next
// call of this function does not run into an endless loop
if (isTypeAlias) {
const visitedType = visitedTypes[parent.node.id.name];
if (visitedType === true) {
// if we are currently visiting this node then just return the name
// as we are starting to endless loop
return { name: parent.node.id.name };
}
else if (typeof visitedType === 'object') {
// if we already resolved the type simple return it
return visitedType;
}
// mark the type as visited
visitedTypes[parent.node.id.name] = true;
}
if (path.node.type in flowTypes) {
type = { name: flowTypes[path.node.type] };
}
else if (path.node.type in flowLiteralTypes) {
type = {
name: 'literal',
value: path.node.extra?.raw ||
`${path.node.value}`,
};
}
else if (path.node.type in namedTypes) {
type = namedTypes[path.node.type](path, typeParams);
}
if (!type) {
type = { name: 'unknown' };
}
if (isTypeAlias) {
// mark the type as unvisited so that further calls can resolve the type again
visitedTypes[parent.node.id.name] = type;
}
return type;
}
/**
* Tries to identify the flow type by inspecting the path for known
* flow type names. This method doesn't check whether the found type is actually
* existing. It simply assumes that a match is always valid.
*
* If there is no match, "unknown" is returned.
*/
export default function getFlowType(path, typeParams = null) {
// Empty visited types before an after run
// Before: in case the detection threw and we rerun again
// After: cleanup memory after we are done here
visitedTypes = {};
const type = getFlowTypeWithResolvedTypes(path, typeParams);
visitedTypes = {};
return type;
}

View File

@@ -0,0 +1,11 @@
import type { NodePath } from '@babel/traverse';
import type { Expression, MemberExpression } from '@babel/types';
/**
* Returns the path to the first part of the MemberExpression. I.e. given a
* path representing
*
* foo.bar.baz
*
* it returns the path of/to `foo`.
*/
export default function getMemberExpressionRoot(memberExpressionPath: NodePath<MemberExpression>): NodePath<Expression>;

View File

@@ -0,0 +1,15 @@
/**
* Returns the path to the first part of the MemberExpression. I.e. given a
* path representing
*
* foo.bar.baz
*
* it returns the path of/to `foo`.
*/
export default function getMemberExpressionRoot(memberExpressionPath) {
let path = memberExpressionPath;
while (path.isMemberExpression()) {
path = path.get('object');
}
return path;
}

View File

@@ -0,0 +1,3 @@
import type { NodePath } from '@babel/traverse';
import type { Expression } from '@babel/types';
export default function getMemberExpressionValuePath(variableDefinition: NodePath, memberName: string): NodePath<Expression> | null;

View File

@@ -0,0 +1,83 @@
import { visitors } from '@babel/traverse';
import getNameOrValue from './getNameOrValue.js';
import { String as toString } from './expressionTo.js';
import isReactForwardRefCall from './isReactForwardRefCall.js';
function resolveName(path) {
if (path.isVariableDeclaration()) {
const declarations = path.get('declarations');
if (declarations.length > 1) {
throw new TypeError('Got unsupported VariableDeclaration. VariableDeclaration must only ' +
'have a single VariableDeclarator. Got ' +
declarations.length +
' declarations.');
}
// VariableDeclarator always has at least one declaration, hence the non-null-assertion
const id = declarations[0].get('id');
if (id.isIdentifier()) {
return id.node.name;
}
return;
}
if (path.isFunctionDeclaration()) {
const id = path.get('id');
if (id.isIdentifier()) {
return id.node.name;
}
return;
}
if (path.isFunctionExpression() ||
path.isArrowFunctionExpression() ||
path.isTaggedTemplateExpression() ||
path.isCallExpression() ||
isReactForwardRefCall(path)) {
let currentPath = path;
while (currentPath.parentPath) {
if (currentPath.parentPath.isVariableDeclarator()) {
const id = currentPath.parentPath.get('id');
if (id.isIdentifier()) {
return id.node.name;
}
return;
}
currentPath = currentPath.parentPath;
}
return;
}
throw new TypeError('Attempted to resolveName for an unsupported path. resolveName does not accept ' +
path.node.type +
'".');
}
const explodedVisitors = visitors.explode({
AssignmentExpression: {
enter: function (path, state) {
const memberPath = path.get('left');
if (!memberPath.isMemberExpression()) {
return;
}
const property = memberPath.get('property');
if (((!memberPath.node.computed && property.isIdentifier()) ||
property.isStringLiteral() ||
property.isNumericLiteral()) &&
getNameOrValue(property) === state.memberName &&
toString(memberPath.get('object')) === state.localName) {
state.result = path.get('right');
path.stop();
}
},
},
});
export default function getMemberExpressionValuePath(variableDefinition, memberName) {
const localName = resolveName(variableDefinition);
if (!localName) {
// likely an immediately exported and therefore nameless/anonymous node
// passed in
return null;
}
const state = {
localName,
memberName,
result: null,
};
variableDefinition.hub.file.traverse(explodedVisitors, state);
return state.result;
}

View File

@@ -0,0 +1,20 @@
import type { NodePath } from '@babel/traverse';
import type { CallExpression, ClassDeclaration, ClassExpression, ClassMethod, Expression, ObjectExpression, ObjectMethod, TaggedTemplateExpression, VariableDeclaration } from '@babel/types';
import type { StatelessComponentNode } from '../resolver/index.js';
type SupportedNodes = CallExpression | ClassDeclaration | ClassExpression | ObjectExpression | StatelessComponentNode | TaggedTemplateExpression | VariableDeclaration;
export declare function isSupportedDefinitionType(path: NodePath): path is NodePath<SupportedNodes>;
/**
* This is a helper method for handlers to make it easier to work either with
* an ObjectExpression from `React.createClass` class or with a class
* definition.
*
* Given a path and a name, this function will either return the path of the
* property value if the path is an ObjectExpression, or the value of the
* ClassProperty/MethodDefinition if it is a class definition (declaration or
* expression).
*
* It also normalizes the names so that e.g. `defaultProps` and
* `getDefaultProps` can be used interchangeably.
*/
export default function getMemberValuePath(componentDefinition: NodePath<SupportedNodes>, memberName: string): NodePath<ClassMethod | Expression | ObjectMethod> | null;
export {};

View File

@@ -0,0 +1,85 @@
import getClassMemberValuePath from './getClassMemberValuePath.js';
import getMemberExpressionValuePath from './getMemberExpressionValuePath.js';
import getPropertyValuePath from './getPropertyValuePath.js';
import resolveFunctionDefinitionToReturnValue from '../utils/resolveFunctionDefinitionToReturnValue.js';
const postprocessPropTypes = (path) => path.isFunction() ? resolveFunctionDefinitionToReturnValue(path) : path;
const POSTPROCESS_MEMBERS = new Map([['propTypes', postprocessPropTypes]]);
const SUPPORTED_DEFINITION_TYPES = [
// potential stateless function component
'ArrowFunctionExpression',
/**
* Adds support for libraries such as
* [system-components]{@link https://jxnblk.com/styled-system/system-components} that use
* CallExpressions to generate components.
*
* While react-docgen's built-in resolvers do not support resolving
* CallExpressions definitions, third-party resolvers (such as
* https://github.com/Jmeyering/react-docgen-annotation-resolver) could be
* used to add these definitions.
*/
'CallExpression',
'ClassDeclaration',
'ClassExpression',
// potential stateless function component
'FunctionDeclaration',
// potential stateless function component
'FunctionExpression',
'ObjectExpression',
// potential stateless function component
'ObjectMethod',
/**
* Adds support for libraries such as
* [styled components]{@link https://github.com/styled-components} that use
* TaggedTemplateExpression's to generate components.
*
* While react-docgen's built-in resolvers do not support resolving
* TaggedTemplateExpression definitions, third-party resolvers (such as
* https://github.com/Jmeyering/react-docgen-annotation-resolver) could be
* used to add these definitions.
*/
'TaggedTemplateExpression',
'VariableDeclaration',
];
export function isSupportedDefinitionType(path) {
return SUPPORTED_DEFINITION_TYPES.includes(path.node.type);
}
/**
* This is a helper method for handlers to make it easier to work either with
* an ObjectExpression from `React.createClass` class or with a class
* definition.
*
* Given a path and a name, this function will either return the path of the
* property value if the path is an ObjectExpression, or the value of the
* ClassProperty/MethodDefinition if it is a class definition (declaration or
* expression).
*
* It also normalizes the names so that e.g. `defaultProps` and
* `getDefaultProps` can be used interchangeably.
*/
export default function getMemberValuePath(componentDefinition, memberName) {
let result;
if (componentDefinition.isObjectExpression()) {
result = getPropertyValuePath(componentDefinition, memberName);
if (!result && memberName === 'defaultProps') {
result = getPropertyValuePath(componentDefinition, 'getDefaultProps');
}
}
else if (componentDefinition.isClassDeclaration() ||
componentDefinition.isClassExpression()) {
result = getClassMemberValuePath(componentDefinition, memberName);
if (!result && memberName === 'defaultProps') {
result = getClassMemberValuePath(componentDefinition, 'getDefaultProps');
}
}
else {
result = getMemberExpressionValuePath(componentDefinition, memberName);
if (!result && memberName === 'defaultProps') {
result = getMemberExpressionValuePath(componentDefinition, 'getDefaultProps');
}
}
const postprocessMethod = POSTPROCESS_MEMBERS.get(memberName);
if (result && postprocessMethod) {
result = postprocessMethod(result);
}
return result;
}

View File

@@ -0,0 +1,21 @@
import type { NodePath } from '@babel/traverse';
import type { Expression, PrivateName } from '@babel/types';
interface MemberDescriptor {
path: NodePath<Expression | PrivateName>;
computed: boolean;
argumentPaths: NodePath[];
}
/**
* Given a "nested" Member/CallExpression, e.g.
*
* foo.bar()[baz][42]
*
* this returns a list of "members". In this example it would be something like
* [
* {path: NodePath<bar>, arguments: NodePath<empty>, computed: false},
* {path: NodePath<baz>, arguments: null, computed: true},
* {path: NodePath<42>, arguments: null, computed: false}
* ]
*/
export default function getMembers(path: NodePath, includeRoot?: boolean): MemberDescriptor[];
export {};

View File

@@ -0,0 +1,50 @@
/**
* Given a "nested" Member/CallExpression, e.g.
*
* foo.bar()[baz][42]
*
* this returns a list of "members". In this example it would be something like
* [
* {path: NodePath<bar>, arguments: NodePath<empty>, computed: false},
* {path: NodePath<baz>, arguments: null, computed: true},
* {path: NodePath<42>, arguments: null, computed: false}
* ]
*/
export default function getMembers(path, includeRoot = false) {
const result = [];
let argumentPaths = [];
let resultPath = path;
while (true) {
if (resultPath.isMemberExpression()) {
const property = resultPath.get('property');
result.push({
path: property,
computed: resultPath.node.computed,
argumentPaths,
});
argumentPaths = [];
resultPath = resultPath.get('object');
}
else if (resultPath.isCallExpression()) {
const callee = resultPath.get('callee');
if (callee.isExpression()) {
argumentPaths = resultPath.get('arguments');
resultPath = callee;
}
else {
break;
}
}
else {
break;
}
}
if (includeRoot && result.length > 0) {
result.push({
path: resultPath,
computed: false,
argumentPaths,
});
}
return result.reverse();
}

View File

@@ -0,0 +1,7 @@
import type { NodePath } from '@babel/traverse';
import type { AssignmentExpression, ClassMethod, ClassPrivateMethod, ClassProperty, ObjectMethod, ObjectProperty } from '@babel/types';
import type { MethodDescriptor } from '../Documentation.js';
export type MethodNodePath = NodePath<AssignmentExpression> | NodePath<ClassMethod> | NodePath<ClassPrivateMethod> | NodePath<ClassProperty> | NodePath<ObjectMethod> | NodePath<ObjectProperty>;
export default function getMethodDocumentation(methodPath: MethodNodePath, options?: {
isStatic?: boolean;
}): MethodDescriptor | null;

View File

@@ -0,0 +1,166 @@
import { getDocblock } from './docblock.js';
import getFlowType from './getFlowType.js';
import getTSType from './getTSType.js';
import getParameterName from './getParameterName.js';
import getPropertyName from './getPropertyName.js';
import getTypeAnnotation from './getTypeAnnotation.js';
import resolveToValue from './resolveToValue.js';
import printValue from './printValue.js';
function getMethodFunctionExpression(methodPath) {
if (methodPath.isClassMethod() || methodPath.isObjectMethod()) {
return methodPath;
}
const potentialFunctionExpression = methodPath.isAssignmentExpression()
? methodPath.get('right')
: methodPath.get('value');
const functionExpression = resolveToValue(potentialFunctionExpression);
if (functionExpression.isFunction()) {
return functionExpression;
}
return null;
}
function getMethodParamOptional(path) {
let identifier = path;
if (identifier.isTSParameterProperty()) {
identifier = identifier.get('parameter');
}
if (identifier.isAssignmentPattern()) {
// A default value always makes the param optional
return true;
}
return identifier.isIdentifier() ? Boolean(identifier.node.optional) : false;
}
function getMethodParamsDoc(methodPath) {
const params = [];
const functionExpression = getMethodFunctionExpression(methodPath);
if (functionExpression) {
// Extract param types.
functionExpression.get('params').forEach((paramPath) => {
let type = null;
const typePath = getTypeAnnotation(paramPath);
if (typePath) {
if (typePath.isFlowType()) {
type = getFlowType(typePath, null);
if (typePath.isGenericTypeAnnotation()) {
type.alias = printValue(typePath.get('id'));
}
}
else if (typePath.isTSType()) {
type = getTSType(typePath, null);
if (typePath.isTSTypeReference()) {
type.alias = printValue(typePath.get('typeName'));
}
}
}
const param = {
name: getParameterName(paramPath),
optional: getMethodParamOptional(paramPath),
type,
};
params.push(param);
});
}
return params;
}
// Extract flow return type.
function getMethodReturnDoc(methodPath) {
const functionExpression = getMethodFunctionExpression(methodPath);
if (functionExpression && functionExpression.node.returnType) {
const returnType = getTypeAnnotation(functionExpression.get('returnType'));
if (!returnType) {
return null;
}
if (returnType.isFlowType()) {
return { type: getFlowType(returnType, null) };
}
else if (returnType.isTSType()) {
return { type: getTSType(returnType, null) };
}
}
return null;
}
function getMethodModifiers(methodPath, options) {
if (methodPath.isAssignmentExpression()) {
return ['static'];
}
// Otherwise this is a method/property node
const modifiers = [];
if (options.isStatic === true ||
((methodPath.isClassProperty() || methodPath.isClassMethod()) &&
methodPath.node.static)) {
modifiers.push('static');
}
const functionExpression = getMethodFunctionExpression(methodPath);
if (functionExpression) {
if (functionExpression.isClassMethod() ||
functionExpression.isObjectMethod()) {
if (functionExpression.node.kind === 'get' ||
functionExpression.node.kind === 'set') {
modifiers.push(functionExpression.node.kind);
}
}
if (functionExpression.node.generator) {
modifiers.push('generator');
}
if (functionExpression.node.async) {
modifiers.push('async');
}
}
return modifiers;
}
function getMethodName(methodPath) {
if (methodPath.isAssignmentExpression()) {
const left = methodPath.get('left');
if (left.isMemberExpression()) {
const property = left.get('property');
if (!left.node.computed && property.isIdentifier()) {
return property.node.name;
}
if (property.isStringLiteral() || property.isNumericLiteral()) {
return String(property.node.value);
}
}
return null;
}
return getPropertyName(methodPath);
}
function getMethodAccessibility(methodPath) {
if (methodPath.isClassMethod() || methodPath.isClassProperty()) {
return methodPath.node.accessibility || null;
}
// Otherwise this is a object method/property or assignment expression
return null;
}
function getMethodDocblock(methodPath) {
if (methodPath.isAssignmentExpression()) {
let path = methodPath;
do {
path = path.parentPath;
} while (path && !path.isExpressionStatement());
if (path) {
return getDocblock(path);
}
return null;
}
// Otherwise this is a method/property node
return getDocblock(methodPath);
}
// Gets the documentation object for a component method.
// Component methods may be represented as class/object method/property nodes
// or as assignment expression of the form `Component.foo = function() {}`
export default function getMethodDocumentation(methodPath, options = {}) {
if (getMethodAccessibility(methodPath) === 'private' ||
methodPath.isClassPrivateMethod()) {
return null;
}
const name = getMethodName(methodPath);
if (!name)
return null;
return {
name,
docblock: getMethodDocblock(methodPath),
modifiers: getMethodModifiers(methodPath, options),
params: getMethodParamsDoc(methodPath),
returns: getMethodReturnDoc(methodPath),
};
}

View File

@@ -0,0 +1,6 @@
import type { NodePath } from '@babel/traverse';
/**
* If node is an Identifier, it returns its name. If it is a literal, it returns
* its value.
*/
export default function getNameOrValue(path: NodePath): boolean | number | string | null;

View File

@@ -0,0 +1,25 @@
import printValue from './printValue.js';
/**
* If node is an Identifier, it returns its name. If it is a literal, it returns
* its value.
*/
export default function getNameOrValue(path) {
if (path.isIdentifier()) {
return path.node.name;
}
else if (path.isQualifiedTypeIdentifier() || path.isTSQualifiedName()) {
return printValue(path);
}
else if (path.isStringLiteral() ||
path.isNumericLiteral() ||
path.isBooleanLiteral()) {
return path.node.value;
}
else if (path.isRegExpLiteral()) {
return path.node.pattern;
}
else if (path.isNullLiteral()) {
return null;
}
throw new TypeError(`Argument must be Identifier, Literal, QualifiedTypeIdentifier or TSQualifiedName. Received '${path.node.type}'`);
}

View File

@@ -0,0 +1,5 @@
import type { NodePath } from '@babel/traverse';
import type { ArrayPattern, AssignmentPattern, Identifier, ObjectPattern, RestElement, TSParameterProperty } from '@babel/types';
type ParameterNodePath = NodePath<ArrayPattern | AssignmentPattern | Identifier | ObjectPattern | RestElement | TSParameterProperty>;
export default function getParameterName(parameterPath: ParameterNodePath): string;
export {};

View File

@@ -0,0 +1,21 @@
import printValue from './printValue.js';
export default function getParameterName(parameterPath) {
if (parameterPath.isIdentifier()) {
return parameterPath.node.name;
}
else if (parameterPath.isAssignmentPattern()) {
return getParameterName(parameterPath.get('left'));
}
else if (parameterPath.isObjectPattern() ||
parameterPath.isArrayPattern()) {
return printValue(parameterPath);
}
else if (parameterPath.isRestElement()) {
return `...${getParameterName(parameterPath.get('argument'))}`;
}
else if (parameterPath.isTSParameterProperty()) {
return getParameterName(parameterPath.get('parameter'));
}
throw new TypeError('Parameter name must be one of Identifier, AssignmentPattern, ArrayPattern, ' +
`ObjectPattern or RestElement, instead got ${parameterPath.node.type}`);
}

View File

@@ -0,0 +1,11 @@
import type { NodePath } from '@babel/traverse';
import type { PropTypeDescriptor } from '../Documentation.js';
/**
* Tries to identify the prop type by inspecting the path for known
* prop type names. This method doesn't check whether the found type is actually
* from React.PropTypes. It simply assumes that a match has the same meaning
* as the React.PropTypes one.
*
* If there is no match, "custom" is returned.
*/
export default function getPropType(path: NodePath): PropTypeDescriptor;

View File

@@ -0,0 +1,239 @@
import { getDocblock } from '../utils/docblock.js';
import getMembers from './getMembers.js';
import getPropertyName from './getPropertyName.js';
import isRequiredPropType from '../utils/isRequiredPropType.js';
import printValue from './printValue.js';
import resolveToValue from './resolveToValue.js';
import resolveObjectKeysToArray from './resolveObjectKeysToArray.js';
import resolveObjectValuesToArray from './resolveObjectValuesToArray.js';
function getEnumValuesFromArrayExpression(path) {
const values = [];
path.get('elements').forEach((elementPath) => {
if (!elementPath.hasNode())
return;
if (elementPath.isSpreadElement()) {
const value = resolveToValue(elementPath.get('argument'));
if (value.isArrayExpression()) {
// if the SpreadElement resolved to an Array, add all their elements too
return values.push(...getEnumValuesFromArrayExpression(value));
}
else {
// otherwise we'll just print the SpreadElement itself
return values.push({
value: printValue(elementPath),
computed: !elementPath.isLiteral(),
});
}
}
// try to resolve the array element to it's value
const value = resolveToValue(elementPath);
return values.push({
value: printValue(value.parentPath?.isImportDeclaration() ? elementPath : value),
computed: !value.isLiteral(),
});
});
return values;
}
function getPropTypeOneOf(type, argumentPath) {
const value = resolveToValue(argumentPath);
if (value.isArrayExpression()) {
type.value = getEnumValuesFromArrayExpression(value);
}
else {
const objectValues = resolveObjectKeysToArray(value) || resolveObjectValuesToArray(value);
if (objectValues) {
type.value = objectValues.map((objectValue) => ({
value: objectValue,
computed: false,
}));
}
else {
// could not easily resolve to an Array, let's print the original value
type.computed = true;
type.value = printValue(argumentPath);
}
}
return type;
}
function getPropTypeOneOfType(type, argumentPath) {
if (argumentPath.isArrayExpression()) {
type.value = argumentPath.get('elements').map((elementPath) => {
if (!elementPath.hasNode())
return;
const descriptor = getPropType(elementPath);
const docs = getDocblock(elementPath);
if (docs) {
descriptor.description = docs;
}
return descriptor;
});
}
return type;
}
function getPropTypeArrayOf(type, argumentPath) {
const docs = getDocblock(argumentPath);
if (docs) {
type.description = docs;
}
const subType = getPropType(argumentPath);
type.value = subType;
return type;
}
function getPropTypeObjectOf(type, argumentPath) {
const docs = getDocblock(argumentPath);
if (docs) {
type.description = docs;
}
const subType = getPropType(argumentPath);
type.value = subType;
return type;
}
function getFirstArgument(path) {
let argument;
if (path.isCallExpression()) {
argument = path.get('arguments')[0];
}
else {
const members = getMembers(path, true);
if (members[0] && members[0].argumentPaths[0]) {
argument = members[0].argumentPaths[0];
}
}
return argument;
}
function isCyclicReference(argument, argumentPath) {
return Boolean(argument && resolveToValue(argument) === argumentPath);
}
/**
* Handles shape and exact prop types
*/
function getPropTypeShapish(type, argumentPath) {
if (!argumentPath.isObjectExpression()) {
argumentPath = resolveToValue(argumentPath);
}
if (argumentPath.isObjectExpression()) {
const value = {};
let rawValue;
argumentPath.get('properties').forEach((propertyPath) => {
// We only handle ObjectProperty as there is nothing to handle for
// SpreadElements and ObjectMethods
if (propertyPath.isObjectProperty()) {
const propertyName = getPropertyName(propertyPath);
if (!propertyName)
return;
const valuePath = propertyPath.get('value');
const argument = getFirstArgument(valuePath);
// This indicates we have a cyclic reference in the shape
// In this case we simply print the argument to shape and bail
if (argument && isCyclicReference(argument, argumentPath)) {
rawValue = printValue(argument);
return;
}
const descriptor = getPropType(valuePath);
const docs = getDocblock(propertyPath);
if (docs) {
descriptor.description = docs;
}
descriptor.required = isRequiredPropType(valuePath);
value[propertyName] = descriptor;
}
});
type.value = rawValue ?? value;
}
return type;
}
function getPropTypeInstanceOf(_type, argumentPath) {
return {
name: 'instanceOf',
value: printValue(argumentPath),
};
}
const simplePropTypes = [
'array',
'bool',
'func',
'number',
'object',
'string',
'any',
'element',
'node',
'symbol',
'elementType',
];
function isSimplePropType(name) {
return simplePropTypes.includes(name);
}
const propTypes = new Map([
['oneOf', callPropTypeHandler.bind(null, 'enum', getPropTypeOneOf)],
['oneOfType', callPropTypeHandler.bind(null, 'union', getPropTypeOneOfType)],
[
'instanceOf',
callPropTypeHandler.bind(null, 'instanceOf', getPropTypeInstanceOf),
],
['arrayOf', callPropTypeHandler.bind(null, 'arrayOf', getPropTypeArrayOf)],
['objectOf', callPropTypeHandler.bind(null, 'objectOf', getPropTypeObjectOf)],
['shape', callPropTypeHandler.bind(null, 'shape', getPropTypeShapish)],
['exact', callPropTypeHandler.bind(null, 'exact', getPropTypeShapish)],
]);
function callPropTypeHandler(name, handler, argumentPath) {
let type = { name };
if (argumentPath) {
type = handler(type, argumentPath);
}
if (!type.value) {
// If there is no argument then leave the value an empty string
type.value = argumentPath ? printValue(argumentPath) : '';
type.computed = true;
}
return type;
}
/**
* Tries to identify the prop type by inspecting the path for known
* prop type names. This method doesn't check whether the found type is actually
* from React.PropTypes. It simply assumes that a match has the same meaning
* as the React.PropTypes one.
*
* If there is no match, "custom" is returned.
*/
export default function getPropType(path) {
let descriptor = null;
getMembers(path, true).some((member) => {
const memberPath = member.path;
let name = null;
if (memberPath.isStringLiteral()) {
name = memberPath.node.value;
}
else if (memberPath.isIdentifier() && !member.computed) {
name = memberPath.node.name;
}
if (name) {
if (isSimplePropType(name)) {
descriptor = { name };
return true;
}
const propTypeHandler = propTypes.get(name);
if (propTypeHandler) {
descriptor = propTypeHandler(member.argumentPaths[0]);
return true;
}
}
return;
});
if (descriptor) {
return descriptor;
}
if (path.isIdentifier() && isSimplePropType(path.node.name)) {
return { name: path.node.name };
}
if (path.isCallExpression()) {
const callee = path.get('callee');
if (callee.isIdentifier()) {
const propTypeHandler = propTypes.get(callee.node.name);
if (propTypeHandler) {
return propTypeHandler(path.get('arguments')[0]);
}
}
}
return { name: 'custom', raw: printValue(path) };
}

View File

@@ -0,0 +1,9 @@
import type { NodePath } from '@babel/traverse';
import type { ClassMethod, ClassProperty, ObjectMethod, ObjectProperty, ObjectTypeProperty, ObjectTypeSpreadProperty, SpreadElement, TSMethodSignature, TSPropertySignature } from '@babel/types';
export declare const COMPUTED_PREFIX = "@computed#";
/**
* In an ObjectExpression, the name of a property can either be an identifier
* or a literal (or dynamic, but we don't support those). This function simply
* returns the value of the literal or name of the identifier.
*/
export default function getPropertyName(propertyPath: NodePath<ClassMethod | ClassProperty | ObjectMethod | ObjectProperty | ObjectTypeProperty | ObjectTypeSpreadProperty | SpreadElement | TSMethodSignature | TSPropertySignature>): string | null;

View File

@@ -0,0 +1,36 @@
import getNameOrValue from './getNameOrValue.js';
import resolveToValue from './resolveToValue.js';
export const COMPUTED_PREFIX = '@computed#';
/**
* In an ObjectExpression, the name of a property can either be an identifier
* or a literal (or dynamic, but we don't support those). This function simply
* returns the value of the literal or name of the identifier.
*/
export default function getPropertyName(propertyPath) {
if (propertyPath.isObjectTypeSpreadProperty()) {
const argument = propertyPath.get('argument');
if (argument.isGenericTypeAnnotation()) {
return getNameOrValue(argument.get('id'));
}
return null;
}
else if (propertyPath.has('computed')) {
const key = propertyPath.get('key');
// Try to resolve variables and member expressions
if (key.isIdentifier() || key.isMemberExpression()) {
const valuePath = resolveToValue(key);
if (valuePath.isStringLiteral() || valuePath.isNumericLiteral()) {
return `${valuePath.node.value}`;
}
}
// generate name for identifier
if (key.isIdentifier()) {
return `${COMPUTED_PREFIX}${key.node.name}`;
}
if (key.isStringLiteral() || key.isNumericLiteral()) {
return `${key.node.value}`;
}
return null;
}
return `${getNameOrValue(propertyPath.get('key'))}`;
}

View File

@@ -0,0 +1,8 @@
import type { NodePath } from '@babel/traverse';
import type { Expression, ObjectExpression, ObjectMethod } from '@babel/types';
/**
* Given an ObjectExpression, this function returns the path of the value of
* the property with name `propertyName`. if the property is an ObjectMethod we
* return the ObjectMethod itself.
*/
export default function getPropertyValuePath(path: NodePath<ObjectExpression>, propertyName: string): NodePath<Expression | ObjectMethod> | null;

View File

@@ -0,0 +1,18 @@
import getPropertyName from './getPropertyName.js';
/**
* Given an ObjectExpression, this function returns the path of the value of
* the property with name `propertyName`. if the property is an ObjectMethod we
* return the ObjectMethod itself.
*/
export default function getPropertyValuePath(path, propertyName) {
const property = path
.get('properties')
.find((propertyPath) => !propertyPath.isSpreadElement() &&
getPropertyName(propertyPath) === propertyName);
if (property) {
return property.isObjectMethod()
? property
: property.get('value');
}
return null;
}

View File

@@ -0,0 +1,12 @@
import type { TypeParameters } from '../utils/getTypeParameters.js';
import type { TypeDescriptor, TSFunctionSignatureType } from '../Documentation.js';
import type { NodePath } from '@babel/traverse';
import type { TypeScript } from '@babel/types';
/**
* Tries to identify the typescript type by inspecting the path for known
* typescript type names. This method doesn't check whether the found type is actually
* existing. It simply assumes that a match is always valid.
*
* If there is no match, "unknown" is returned.
*/
export default function getTSType(path: NodePath<TypeScript>, typeParamMap?: TypeParameters | null): TypeDescriptor<TSFunctionSignatureType>;

View File

@@ -0,0 +1,374 @@
import getPropertyName from './getPropertyName.js';
import printValue from './printValue.js';
import getTypeAnnotation from '../utils/getTypeAnnotation.js';
import resolveToValue from '../utils/resolveToValue.js';
import { resolveObjectToNameArray } from '../utils/resolveObjectKeysToArray.js';
import getTypeParameters from '../utils/getTypeParameters.js';
import { getDocblock } from './docblock.js';
const tsTypes = {
TSAnyKeyword: 'any',
TSBooleanKeyword: 'boolean',
TSUnknownKeyword: 'unknown',
TSNeverKeyword: 'never',
TSNullKeyword: 'null',
TSUndefinedKeyword: 'undefined',
TSNumberKeyword: 'number',
TSStringKeyword: 'string',
TSSymbolKeyword: 'symbol',
TSThisType: 'this',
TSObjectKeyword: 'object',
TSVoidKeyword: 'void',
};
const namedTypes = {
TSArrayType: handleTSArrayType,
TSTypeReference: handleTSTypeReference,
TSTypeLiteral: handleTSTypeLiteral,
TSInterfaceDeclaration: handleTSInterfaceDeclaration,
TSUnionType: handleTSUnionType,
TSFunctionType: handleTSFunctionType,
TSIntersectionType: handleTSIntersectionType,
TSMappedType: handleTSMappedType,
TSTupleType: handleTSTupleType,
TSTypeQuery: handleTSTypeQuery,
TSTypeOperator: handleTSTypeOperator,
TSIndexedAccessType: handleTSIndexedAccessType,
TSLiteralType: handleTSLiteralType,
};
function handleTSQualifiedName(path) {
const left = path.get('left');
const right = path.get('right');
if (left.isIdentifier({ name: 'React' }) && right.isIdentifier()) {
return {
name: `${left.node.name}${right.node.name}`,
raw: printValue(path),
};
}
return { name: printValue(path).replace(/<.*>$/, '') };
}
function handleTSLiteralType(path) {
const literal = path.get('literal');
return {
name: 'literal',
value: printValue(literal),
};
}
function handleTSArrayType(path, typeParams) {
return {
name: 'Array',
elements: [getTSTypeWithResolvedTypes(path.get('elementType'), typeParams)],
raw: printValue(path),
};
}
function handleTSTypeReference(path, typeParams) {
let type;
const typeName = path.get('typeName');
if (typeName.isTSQualifiedName()) {
type = handleTSQualifiedName(typeName);
}
else {
type = { name: typeName.node.name };
}
const resolvedPath = (typeParams && typeParams[type.name]) ||
resolveToValue(path.get('typeName'));
const typeParameters = path.get('typeParameters');
const resolvedTypeParameters = resolvedPath.get('typeParameters');
if (typeParameters.hasNode() && resolvedTypeParameters.hasNode()) {
typeParams = getTypeParameters(resolvedTypeParameters, typeParameters, typeParams);
}
if (typeParams && typeParams[type.name]) {
// Open question: Why is this `null` instead of `typeParams`
type = getTSTypeWithResolvedTypes(resolvedPath, null);
}
const resolvedTypeAnnotation = resolvedPath.get('typeAnnotation');
if (resolvedTypeAnnotation.hasNode()) {
type = getTSTypeWithResolvedTypes(resolvedTypeAnnotation, typeParams);
}
else if (typeParameters.hasNode()) {
const params = typeParameters.get('params');
type = {
...type,
elements: params.map((param) => getTSTypeWithResolvedTypes(param, typeParams)),
raw: printValue(path),
};
}
return type;
}
function getTSTypeWithRequirements(path, typeParams) {
const type = getTSTypeWithResolvedTypes(path, typeParams);
type.required =
!('optional' in path.parentPath.node) || !path.parentPath.node.optional;
return type;
}
function handleTSTypeLiteral(path, typeParams) {
const type = {
name: 'signature',
type: 'object',
raw: printValue(path),
signature: { properties: [] },
};
path.get('members').forEach((param) => {
const typeAnnotation = param.get('typeAnnotation');
if ((param.isTSPropertySignature() || param.isTSMethodSignature()) &&
typeAnnotation.hasNode()) {
const propName = getPropertyName(param);
if (!propName) {
return;
}
const docblock = getDocblock(param);
let doc = {};
if (docblock) {
doc = { description: docblock };
}
type.signature.properties.push({
key: propName,
value: getTSTypeWithRequirements(typeAnnotation, typeParams),
...doc,
});
}
else if (param.isTSCallSignatureDeclaration()) {
type.signature.constructor = handleTSFunctionType(param, typeParams);
}
else if (param.isTSIndexSignature() && typeAnnotation.hasNode()) {
const parameters = param.get('parameters');
if (parameters[0]) {
const idTypeAnnotation = parameters[0].get('typeAnnotation');
if (idTypeAnnotation.hasNode()) {
type.signature.properties.push({
key: getTSTypeWithResolvedTypes(idTypeAnnotation, typeParams),
value: getTSTypeWithRequirements(typeAnnotation, typeParams),
});
}
}
}
});
return type;
}
function handleTSInterfaceDeclaration(path) {
// Interfaces are handled like references which would be documented separately,
// rather than inlined like type aliases.
return {
name: path.node.id.name,
};
}
function handleTSUnionType(path, typeParams) {
return {
name: 'union',
raw: printValue(path),
elements: path
.get('types')
.map((subType) => getTSTypeWithResolvedTypes(subType, typeParams)),
};
}
function handleTSIntersectionType(path, typeParams) {
return {
name: 'intersection',
raw: printValue(path),
elements: path
.get('types')
.map((subType) => getTSTypeWithResolvedTypes(subType, typeParams)),
};
}
// type OptionsFlags<Type> = { [Property in keyof Type]; };
function handleTSMappedType(path, typeParams) {
const key = getTSTypeWithResolvedTypes(path.get('typeParameter').get('constraint'), typeParams);
key.required = !path.node.optional;
const typeAnnotation = path.get('typeAnnotation');
let value;
if (typeAnnotation.hasNode()) {
value = getTSTypeWithResolvedTypes(typeAnnotation, typeParams);
}
else {
value = { name: 'any' };
}
return {
name: 'signature',
type: 'object',
raw: printValue(path),
signature: {
properties: [
{
key,
value,
},
],
},
};
}
function handleTSFunctionType(path, typeParams) {
let returnType;
const annotation = path.get('typeAnnotation');
if (annotation.hasNode()) {
returnType = getTSTypeWithResolvedTypes(annotation, typeParams);
}
const type = {
name: 'signature',
type: 'function',
raw: printValue(path),
signature: {
arguments: [],
return: returnType,
},
};
path.get('parameters').forEach((param) => {
const typeAnnotation = getTypeAnnotation(param);
const arg = {
type: typeAnnotation
? getTSTypeWithResolvedTypes(typeAnnotation, typeParams)
: undefined,
name: '',
};
if (param.isIdentifier()) {
arg.name = param.node.name;
if (param.node.name === 'this') {
type.signature.this = arg.type;
return;
}
}
else if (param.isRestElement()) {
const restArgument = param.get('argument');
if (restArgument.isIdentifier()) {
arg.name = restArgument.node.name;
}
else {
arg.name = printValue(restArgument);
}
arg.rest = true;
}
type.signature.arguments.push(arg);
});
return type;
}
function handleTSTupleType(path, typeParams) {
const type = {
name: 'tuple',
raw: printValue(path),
elements: [],
};
path.get('elementTypes').forEach((param) => {
type.elements.push(getTSTypeWithResolvedTypes(param, typeParams));
});
return type;
}
function handleTSTypeQuery(path, typeParams) {
const exprName = path.get('exprName');
if (exprName.isIdentifier()) {
const resolvedPath = resolveToValue(path.get('exprName'));
if (resolvedPath.has('typeAnnotation')) {
return getTSTypeWithResolvedTypes(resolvedPath.get('typeAnnotation'), typeParams);
}
return { name: exprName.node.name };
}
else if (exprName.isTSQualifiedName()) {
return handleTSQualifiedName(exprName);
}
else {
// TSImportType
return { name: printValue(exprName) };
}
}
function handleTSTypeOperator(path, typeParams) {
if (path.node.operator !== 'keyof') {
return null;
}
let value = path.get('typeAnnotation');
if (value.isTSTypeQuery()) {
value = value.get('exprName');
}
else if ('id' in value.node) {
value = value.get('id');
}
else if (value.isTSTypeReference()) {
return getTSTypeWithResolvedTypes(value, typeParams);
}
const resolvedPath = resolveToValue(value);
if (resolvedPath.isObjectExpression() || resolvedPath.isTSTypeLiteral()) {
const keys = resolveObjectToNameArray(resolvedPath, true);
if (keys) {
return {
name: 'union',
raw: printValue(path),
elements: keys.map((key) => ({ name: 'literal', value: key })),
};
}
}
return null;
}
function handleTSIndexedAccessType(path, typeParams) {
const objectType = getTSTypeWithResolvedTypes(path.get('objectType'), typeParams);
const indexType = getTSTypeWithResolvedTypes(path.get('indexType'), typeParams);
// We only get the signature if the objectType is a type (vs interface)
if (!objectType.signature) {
return {
name: `${objectType.name}[${indexType.value ? indexType.value.toString() : indexType.name}]`,
raw: printValue(path),
};
}
const resolvedType = objectType.signature.properties.find((p) => {
// indexType.value = "'foo'"
return indexType.value && p.key === indexType.value.replace(/['"]+/g, '');
});
if (!resolvedType) {
return { name: 'unknown' };
}
return {
name: resolvedType.value.name,
raw: printValue(path),
};
}
let visitedTypes = {};
function getTSTypeWithResolvedTypes(path, typeParams) {
if (path.isTSTypeAnnotation()) {
path = path.get('typeAnnotation');
}
const node = path.node;
let type = null;
let typeAliasName = null;
if (path.parentPath.isTSTypeAliasDeclaration()) {
typeAliasName = path.parentPath.node.id.name;
}
// When we see a typealias mark it as visited so that the next
// call of this function does not run into an endless loop
if (typeAliasName) {
if (visitedTypes[typeAliasName] === true) {
// if we are currently visiting this node then just return the name
// as we are starting to endless loop
return { name: typeAliasName };
}
else if (typeof visitedTypes[typeAliasName] === 'object') {
// if we already resolved the type simple return it
return visitedTypes[typeAliasName];
}
// mark the type as visited
visitedTypes[typeAliasName] = true;
}
if (node.type in tsTypes) {
type = { name: tsTypes[node.type] };
}
else if (node.type in namedTypes) {
type = namedTypes[node.type](path, typeParams);
}
if (!type) {
type = { name: 'unknown' };
}
if (typeAliasName) {
// mark the type as unvisited so that further calls can resolve the type again
visitedTypes[typeAliasName] = type;
}
return type;
}
/**
* Tries to identify the typescript type by inspecting the path for known
* typescript type names. This method doesn't check whether the found type is actually
* existing. It simply assumes that a match is always valid.
*
* If there is no match, "unknown" is returned.
*/
export default function getTSType(path, typeParamMap = null) {
// Empty visited types before an after run
// Before: in case the detection threw and we rerun again
// After: cleanup memory after we are done here
visitedTypes = {};
const type = getTSTypeWithResolvedTypes(path, typeParamMap);
visitedTypes = {};
return type;
}

View File

@@ -0,0 +1,7 @@
import type { NodePath } from '@babel/traverse';
import type { FlowType, Node, TSType } from '@babel/types';
/**
* Gets the most inner valuable TypeAnnotation from path. If no TypeAnnotation
* can be found null is returned
*/
export default function getTypeAnnotation<T extends Node = FlowType | TSType>(path: NodePath<Node | null | undefined>): NodePath<T> | null;

View File

@@ -0,0 +1,15 @@
/**
* Gets the most inner valuable TypeAnnotation from path. If no TypeAnnotation
* can be found null is returned
*/
export default function getTypeAnnotation(path) {
if (!path.has('typeAnnotation'))
return null;
let resultPath = path;
do {
resultPath = resultPath.get('typeAnnotation');
} while (resultPath.has('typeAnnotation') &&
!resultPath.isFlowType() &&
!resultPath.isTSType());
return resultPath;
}

View File

@@ -0,0 +1,12 @@
import type { NodePath } from '@babel/traverse';
import type Documentation from '../Documentation.js';
import type { TypeParameters } from './getTypeParameters.js';
/**
* Given an React component (stateless or class) tries to find
* flow or TS types for the props. It may find multiple types.
* If not found or it is not one of the supported component types,
* this function returns an empty array.
*/
declare const _default: (componentDefinition: NodePath) => NodePath[];
export default _default;
export declare function applyToTypeProperties(documentation: Documentation, path: NodePath, callback: (propertyPath: NodePath, params: TypeParameters | null) => void, typeParams: TypeParameters | null): void;

View File

@@ -0,0 +1,153 @@
import getMemberValuePath from './getMemberValuePath.js';
import getTypeAnnotation from './getTypeAnnotation.js';
import getTypeParameters from './getTypeParameters.js';
import isReactComponentClass from './isReactComponentClass.js';
import isReactForwardRefCall from './isReactForwardRefCall.js';
import resolveGenericTypeAnnotation from './resolveGenericTypeAnnotation.js';
import resolveToValue from './resolveToValue.js';
import getTypeIdentifier from './getTypeIdentifier.js';
import isReactBuiltinReference from './isReactBuiltinReference.js';
import unwrapBuiltinTSPropTypes from './unwrapBuiltinTSPropTypes.js';
function getStatelessPropsPath(componentDefinition) {
if (!componentDefinition.isFunction())
return;
return componentDefinition.get('params')[0];
}
function getForwardRefGenericsType(componentDefinition) {
const typeParameters = componentDefinition.get('typeParameters');
if (typeParameters && typeParameters.hasNode()) {
const params = typeParameters.get('params');
return params[1] ?? null;
}
return null;
}
function findAssignedVariableType(componentDefinition) {
const variableDeclarator = componentDefinition.findParent((path) => path.isVariableDeclarator());
if (!variableDeclarator)
return null;
const typeAnnotation = getTypeAnnotation(variableDeclarator.get('id'));
if (!typeAnnotation)
return null;
if (typeAnnotation.isTSTypeReference()) {
const typeName = typeAnnotation.get('typeName');
if (isReactBuiltinReference(typeName, 'FunctionComponent') ||
isReactBuiltinReference(typeName, 'FC') ||
isReactBuiltinReference(typeName, 'VoidFunctionComponent') ||
isReactBuiltinReference(typeName, 'VFC')) {
const typeParameters = typeAnnotation.get('typeParameters');
if (typeParameters.hasNode()) {
return typeParameters.get('params')[0] ?? null;
}
}
}
return null;
}
/**
* Given an React component (stateless or class) tries to find
* flow or TS types for the props. It may find multiple types.
* If not found or it is not one of the supported component types,
* this function returns an empty array.
*/
export default (componentDefinition) => {
const typePaths = [];
if (isReactComponentClass(componentDefinition)) {
const superTypes = componentDefinition.get('superTypeParameters');
if (superTypes.hasNode()) {
const params = superTypes.get('params');
if (params.length >= 1) {
typePaths.push(params[params.length === 3 ? 1 : 0]);
}
}
else {
const propsMemberPath = getMemberValuePath(componentDefinition, 'props');
if (!propsMemberPath) {
return [];
}
const typeAnnotation = getTypeAnnotation(propsMemberPath.parentPath);
if (typeAnnotation) {
typePaths.push(typeAnnotation);
}
}
}
else {
if (isReactForwardRefCall(componentDefinition)) {
const genericTypeAnnotation = getForwardRefGenericsType(componentDefinition);
if (genericTypeAnnotation) {
typePaths.push(genericTypeAnnotation);
}
componentDefinition = resolveToValue(componentDefinition.get('arguments')[0]);
}
const propsParam = getStatelessPropsPath(componentDefinition);
if (propsParam) {
const typeAnnotation = getTypeAnnotation(propsParam);
if (typeAnnotation) {
typePaths.push(typeAnnotation);
}
}
const assignedVariableType = findAssignedVariableType(componentDefinition);
if (assignedVariableType) {
typePaths.push(assignedVariableType);
}
}
return typePaths.map((typePath) => unwrapBuiltinTSPropTypes(typePath));
};
export function applyToTypeProperties(documentation, path, callback, typeParams) {
if (path.isObjectTypeAnnotation()) {
path
.get('properties')
.forEach((propertyPath) => callback(propertyPath, typeParams));
}
else if (path.isTSTypeLiteral()) {
path
.get('members')
.forEach((propertyPath) => callback(propertyPath, typeParams));
}
else if (path.isInterfaceDeclaration()) {
applyExtends(documentation, path, callback, typeParams);
path
.get('body')
.get('properties')
.forEach((propertyPath) => callback(propertyPath, typeParams));
}
else if (path.isTSInterfaceDeclaration()) {
applyExtends(documentation, path, callback, typeParams);
path
.get('body')
.get('body')
.forEach((propertyPath) => callback(propertyPath, typeParams));
}
else if (path.isIntersectionTypeAnnotation() ||
path.isTSIntersectionType()) {
path.get('types').forEach((typesPath) => applyToTypeProperties(documentation, typesPath, callback, typeParams));
}
else if (!path.isUnionTypeAnnotation()) {
// The react-docgen output format does not currently allow
// for the expression of union types
const typePath = resolveGenericTypeAnnotation(path);
if (typePath) {
applyToTypeProperties(documentation, typePath, callback, typeParams);
}
}
}
function applyExtends(documentation, path, callback, typeParams) {
const classExtends = path.get('extends');
if (!Array.isArray(classExtends)) {
return;
}
classExtends.forEach((extendsPath) => {
const resolvedPath = resolveGenericTypeAnnotation(extendsPath);
if (resolvedPath) {
if (resolvedPath.has('typeParameters') &&
extendsPath.node.typeParameters) {
typeParams = getTypeParameters(resolvedPath.get('typeParameters'), extendsPath.get('typeParameters'), typeParams);
}
applyToTypeProperties(documentation, resolvedPath, callback, typeParams);
}
else {
const idPath = getTypeIdentifier(extendsPath);
if (idPath && idPath.isIdentifier()) {
documentation.addComposes(idPath.node.name);
}
}
});
}

View File

@@ -0,0 +1,2 @@
import type { NodePath } from '@babel/traverse';
export default function getTypeIdentifier(path: NodePath): NodePath | null;

View File

@@ -0,0 +1,12 @@
export default function getTypeIdentifier(path) {
if (path.has('id')) {
return path.get('id');
}
else if (path.isTSTypeReference()) {
return path.get('typeName');
}
else if (path.isTSExpressionWithTypeArguments()) {
return path.get('expression');
}
return null;
}

View File

@@ -0,0 +1,4 @@
import type { NodePath } from '@babel/traverse';
import type { TSTypeParameterDeclaration, TSTypeParameterInstantiation, TypeParameterDeclaration, TypeParameterInstantiation } from '@babel/types';
export type TypeParameters = Record<string, NodePath>;
export default function getTypeParameters(declaration: NodePath<TSTypeParameterDeclaration | TypeParameterDeclaration>, instantiation: NodePath<TSTypeParameterInstantiation | TypeParameterInstantiation>, inputParams: TypeParameters | null | undefined): TypeParameters;

View File

@@ -0,0 +1,34 @@
import resolveGenericTypeAnnotation from '../utils/resolveGenericTypeAnnotation.js';
export default function getTypeParameters(declaration, instantiation, inputParams) {
const params = {};
const numInstantiationParams = instantiation.node.params.length;
let i = 0;
declaration
.get('params')
.forEach((paramPath) => {
const key = paramPath.node.name;
const defaultProp = paramPath.get('default');
const defaultTypePath = defaultProp.hasNode() ? defaultProp : null;
const typePath = i < numInstantiationParams
? instantiation.get('params')[i++]
: defaultTypePath;
if (typePath) {
let resolvedTypePath = resolveGenericTypeAnnotation(typePath) || typePath;
let typeName;
if (resolvedTypePath.isTSTypeReference()) {
typeName = resolvedTypePath.get('typeName');
}
else if (resolvedTypePath.isGenericTypeAnnotation()) {
typeName = resolvedTypePath.get('id');
}
if (typeName &&
inputParams &&
typeName.isIdentifier() &&
inputParams[typeName.node.name]) {
resolvedTypePath = inputParams[typeName.node.name];
}
params[key] = resolvedTypePath;
}
});
return params;
}

Some files were not shown because too many files have changed in this diff Show More