docs: generate documentation for UI components (#5849)

* added tool to generate spec files for React components

* use typedoc for missing descriptions and types

* improvements and fixes

* improvements

* added doc comments for half of the components

* add custom resolver + more doc comments

* added all tsdocs

* general improvements

* add specs to UI docs

* added github action

* remove unnecessary api route

* Added readme for react-docs-generator

* remove comment

* Update packages/design-system/ui/src/components/currency-input/currency-input.tsx

Co-authored-by: Kasper Fabricius Kristensen <45367945+kasperkristensen@users.noreply.github.com>

* remove description of aria fields + add generate script

---------

Co-authored-by: Kasper Fabricius Kristensen <45367945+kasperkristensen@users.noreply.github.com>
This commit is contained in:
Shahed Nasser
2023-12-13 16:02:41 +02:00
committed by GitHub
parent edc49bfe1d
commit 245e5c9a69
288 changed files with 6029 additions and 1447 deletions

View File

@@ -0,0 +1,578 @@
/* eslint-disable no-case-declarations */
import { Documentation } from "react-docgen"
import {
FunctionSignatureType,
ObjectSignatureType,
TSFunctionSignatureType,
TypeDescriptor,
} from "react-docgen/dist/Documentation.js"
import { Comment } from "typedoc"
import {
Application,
Context,
Converter,
DeclarationReflection,
ProjectReflection,
Reflection,
SignatureReflection,
SomeType,
SourceReference,
} from "typedoc"
import {
getFunctionType,
getProjectChild,
getType,
getTypeChildren,
} from "utils"
type MappedReflectionSignature = {
source: SourceReference
signatures: SignatureReflection[]
}
type Options = {
tsconfigPath: string
disable?: boolean
verbose?: boolean
}
type TsType = TypeDescriptor<TSFunctionSignatureType>
type ExcludeExternalOptions = {
parentReflection: DeclarationReflection
childReflection: DeclarationReflection
signature?: SignatureReflection
propDescription?: string
}
const MAX_LEVEL = 3
export default class TypedocManager {
private app: Application | undefined
private options: Options
private mappedReflectionSignatures: MappedReflectionSignature[]
private project: ProjectReflection | undefined
private getTypeOptions = {
hideLink: true,
wrapBackticks: false,
escape: false,
}
constructor(options: Options) {
this.options = options
this.mappedReflectionSignatures = []
}
async setup(filePath: string): Promise<ProjectReflection | undefined> {
if (this.options.disable) {
return
}
this.app = await Application.bootstrapWithPlugins({
entryPoints: [filePath],
tsconfig: this.options.tsconfigPath,
plugin: ["typedoc-plugin-custom"],
enableInternalResolve: true,
logLevel: this.options.verbose ? "Verbose" : "None",
})
// This listener sets the content of mappedReflectionSignatures
this.app.converter.on(
Converter.EVENT_RESOLVE,
(context: Context, reflection: Reflection) => {
if (reflection instanceof DeclarationReflection) {
if (reflection.sources?.length && reflection.signatures?.length) {
this.mappedReflectionSignatures.push({
source: reflection.sources[0],
signatures: reflection.signatures,
})
}
}
}
)
this.project = await this.app.convert()
return this.project
}
tryFillWithTypedocData(
spec: Documentation,
reflectionPathName: string[]
): Documentation {
if (!this.isProjectSetUp()) {
return spec
}
if (!spec.props) {
spec.props = {}
}
// since the component may be a child of an exported component
// we use the reflectionPathName to retrieve the component
// by its "reflection path"
const reflection = this.project?.getChildByName(
reflectionPathName
) as DeclarationReflection
if (!reflection) {
return spec
}
// retrieve the signature of the reflection
// this is helpful to retrieve the props of the component
const mappedSignature = reflection.sources?.length
? this.getMappedSignatureFromSource(reflection.sources[0])
: undefined
if (
mappedSignature?.signatures[0].parameters?.length &&
mappedSignature.signatures[0].parameters[0].type
) {
const signature = mappedSignature.signatures[0]
// get the props of the component from the
// first parameter in the signature.
const props = getTypeChildren(
signature.parameters![0].type!,
this.project
)
// this stores props that should be removed from the
// spec
const propsToRemove = new Set<string>()
// loop over props in the spec to either add missing descriptions or
// push a prop into the `propsToRemove` set.
Object.entries(spec.props!).forEach(([propName, propDetails]) => {
// retrieve the reflection of the prop
const reflectionPropType = props.find(
(propType) => propType.name === propName
)
if (!reflectionPropType) {
// if the reflection doesn't exist and the
// prop doesn't have a description, it should
// be removed.
if (!propDetails.description) {
propsToRemove.add(propName)
}
return
}
// if the component has the `@excludeExternal` tag,
// the prop is external, and it doesn't have the
// `@keep` tag, the prop is removed.
if (
this.shouldExcludeExternal({
parentReflection: reflection,
childReflection: reflectionPropType,
propDescription: propDetails.description,
signature,
})
) {
propsToRemove.add(propName)
return
}
// if the prop doesn't have description, retrieve it using Typedoc
propDetails.description =
propDetails.description || this.getDescription(reflectionPropType)
// if the prop still doesn't have description, remove it.
if (!propDetails.description) {
propsToRemove.add(propName)
} else {
propDetails.description = this.normalizeDescription(
propDetails.description
)
}
})
// delete props in the `propsToRemove` set from the specs.
propsToRemove.forEach((prop) => delete spec.props![prop])
// try to add missing props
props
.filter(
(prop) =>
// Filter out props that are already in the
// specs, are already in the `propsToRemove` set
// (meaning they've been removed), don't have a
// comment, are React props, and are external props (if
// the component excludes them).
!Object.hasOwn(spec.props!, prop.name) &&
!propsToRemove.has(prop.name) &&
this.getReflectionComment(prop) &&
!this.isFromReact(prop) &&
!this.shouldExcludeExternal({
parentReflection: reflection,
childReflection: prop,
signature,
})
)
.forEach((prop) => {
// If the prop has description (retrieved)
// through Typedoc, it's added into the spec.
const description = this.normalizeDescription(
this.getDescription(prop)
)
if (!description) {
return
}
spec.props![prop.name] = {
description: this.normalizeDescription(this.getDescription(prop)),
required: !prop.flags.isOptional,
tsType: prop.type
? this.getTsType(prop.type)
: prop.signatures?.length
? this.getFunctionTsType(prop.signatures[0])
: undefined,
}
if (!spec.props![prop.name].tsType) {
delete spec.props![prop.name].tsType
}
})
}
return spec
}
isProjectSetUp() {
return this.project && this.mappedReflectionSignatures
}
getMappedSignatureFromSource(
origSource: SourceReference
): MappedReflectionSignature | undefined {
return this.mappedReflectionSignatures.find(({ source }) => {
return (
source.fileName === origSource.fileName &&
source.line === origSource.line &&
source.character === origSource.character
)
})
}
// Retrieves the `tsType` stored in a spec's prop
// The format is based on the expected format of React Docgen.
getTsType(reflectionType: SomeType, level = 1): TsType {
const rawValue = getType({
reflectionType,
...this.getTypeOptions,
})
if (level > MAX_LEVEL) {
return {
name: rawValue,
}
}
switch (reflectionType.type) {
case "array": {
const elements = this.getTsType(reflectionType.elementType, level + 1)
return {
name: "Array",
elements: [elements],
raw: rawValue,
}
}
case "reference":
const referenceReflection: DeclarationReflection | undefined =
(reflectionType.reflection as DeclarationReflection) ||
getProjectChild(this.project!, reflectionType.name)
const elements: TsType[] = []
if (referenceReflection?.children) {
referenceReflection.children?.forEach((child) => {
if (!child.type) {
return
}
elements.push(this.getTsType(child.type, level + 1))
})
} else if (referenceReflection?.type) {
elements.push(this.getTsType(referenceReflection.type, level + 1))
}
return {
name: reflectionType.name,
elements: elements,
raw: rawValue,
}
case "reflection":
const reflection = reflectionType.declaration
if (reflection.signatures?.length) {
return this.getFunctionTsType(
reflection.signatures[0],
rawValue,
level + 1
)
} else {
const typeData: ObjectSignatureType = {
name: "signature",
type: "object",
raw: rawValue,
signature: {
properties: [],
},
}
reflection.children?.map((property) => {
typeData.signature.properties.push({
key: property.name,
value: property.type
? this.getTsType(property.type, level + 1)
: {
name: "unknown",
},
description: this.getDescription(property),
})
})
return typeData
}
case "literal":
if (reflectionType.value === null) {
return {
name: `null`,
}
}
return {
name: "literal",
value: rawValue,
}
case "union":
case "intersection":
return {
name: reflectionType.type,
raw: rawValue,
elements: this.getElementsTypes(reflectionType.types, level),
}
case "tuple":
return {
name: "tuple",
raw: rawValue,
elements: this.getElementsTypes(reflectionType.elements, level),
}
default:
return {
name: rawValue,
}
}
}
// Retrieves the TsType of nested elements. (Helpful for
// Reflection types like `intersection` or `union`).
getElementsTypes(elements: SomeType[], level = 1): TsType[] {
const elementData: TsType[] = []
elements.forEach((element) => {
elementData.push(this.getTsType(element, level + 1))
})
return elementData
}
// Removes tags like `@keep` and `@ignore` from a
// prop or component's description. These aren't removed
// by React Docgen.
normalizeDescription(description: string): string {
return description
.replace("@keep", "")
.replace("@ignore", "")
.replace("@excludeExternal", "")
.trim()
}
// Retrieve the description of a reflection (component or prop)
// through its summary.
getDescription(reflection: DeclarationReflection): string {
let commentDisplay = this.getReflectionComment(reflection)?.summary
if (!commentDisplay) {
const signature = reflection.signatures?.find(
(sig) => this.getReflectionComment(sig)?.summary.length
)
if (signature) {
commentDisplay = this.getReflectionComment(signature)!.summary
}
}
return commentDisplay?.map(({ text }) => text).join("") || ""
}
// Retrieves the TsType for a function
getFunctionTsType(
signature: SignatureReflection,
rawValue?: string,
level = 1
): TsType {
if (!rawValue) {
rawValue = getFunctionType({
modelSignatures: [signature],
...this.getTypeOptions,
})
}
const typeData: FunctionSignatureType = {
name: "signature",
type: "function",
raw: rawValue!,
signature: {
arguments: [],
return: undefined,
},
}
signature.parameters?.forEach((parameter) => {
const parameterType = parameter.type
? this.getTsType(parameter.type, level + 1)
: undefined
typeData.signature.arguments.push({
name: parameter.name,
type: parameterType,
rest: parameter.flags.isRest,
})
})
typeData.signature.return = signature.type
? this.getTsType(signature.type, level + 1)
: undefined
return typeData
}
// Checks if a TsType only has a `name` field.
doesOnlyHaveName(obj: TsType): boolean {
const primitiveTypes = ["string", "number", "object", "boolean", "function"]
const keys = Object.keys(obj)
return (
keys.length === 1 &&
keys[0] === "name" &&
!primitiveTypes.includes(obj.name)
)
}
// retrieves a reflection by the provided name
// and check if its type is ReactNode
// this is useful for the CustomResolver to check
// if a variable is a React component.
isReactComponent(name: string): boolean {
const reflection = this.getReflectionByName(name)
if (
!reflection ||
!(reflection instanceof DeclarationReflection) ||
!reflection.signatures
) {
return false
}
return reflection.signatures.some(
(signature) =>
signature.type?.type === "reference" &&
signature.type.name === "ReactNode"
)
}
// Returns the TsType of a child of a reflection.
resolveChildType(reflectionName: string, childName: string): TsType | null {
if (!this.project) {
return null
}
const reflection = this.getReflectionByName(reflectionName)
if (!reflection) {
return null
}
let childReflection: DeclarationReflection | undefined
if (reflection.children) {
childReflection = reflection.getChildByName(
childName
) as DeclarationReflection
} else if (reflection.type) {
getTypeChildren(reflection.type, this.project).some((child) => {
if (child.name === childName) {
childReflection = child
return true
}
return false
})
}
if (
!childReflection ||
!("type" in childReflection) ||
(this.isFromReact(childReflection as DeclarationReflection) &&
!this.getReflectionComment(childReflection)?.summary)
) {
return null
}
return this.getTsType(childReflection.type as SomeType)
}
// used to check if a reflection (typically of a prop)
// is inherited from React (for example, the className prop)
isFromReact(reflection: DeclarationReflection) {
// check first if the reflection has the `@keep` modifier
if (this.getReflectionComment(reflection)?.hasModifier("@keep")) {
return false
}
return reflection.sources?.some((source) =>
source.fileName.includes("@types/react")
)
}
// Checks if a parent reflection has the `@excludeExternal`
// which means external child reflections should be ignored.
// external child reflections aren't ignored if they have the
// `@keep` tag.
shouldExcludeExternal({
parentReflection,
childReflection,
propDescription,
signature,
}: ExcludeExternalOptions): boolean {
const parentHasExcludeExternalsModifier =
this.getReflectionComment(parentReflection)?.hasModifier(
"@excludeExternal"
) ||
(signature &&
this.getReflectionComment(signature)?.hasModifier(
"@excludeExternal"
)) ||
false
const childHasKeepModifier =
this.getReflectionComment(childReflection)?.hasModifier("@keep") ||
propDescription?.includes("@keep")
const childHasExternalSource =
childReflection.sources?.some((source) =>
source.fileName.startsWith("node_modules")
) || false
return (
parentHasExcludeExternalsModifier &&
!childHasKeepModifier &&
childHasExternalSource
)
}
// Gets comments of a reflection.
getReflectionComment(reflection: Reflection): Comment | undefined {
if (reflection.comment) {
return reflection.comment
}
if (
reflection instanceof DeclarationReflection &&
reflection.signatures?.length
) {
return reflection.signatures.find(
(signature) => signature.comment !== undefined
)?.comment
}
return undefined
}
// Gets a reflection by its name.
getReflectionByName(name: string): DeclarationReflection | undefined {
return this.project
? (Object.values(this.project?.reflections || {}).find(
(ref) => ref.name === name
) as DeclarationReflection)
: undefined
}
}

View File

@@ -0,0 +1,115 @@
import path from "path"
import readFiles from "../utils/read-files.js"
import { builtinHandlers, parse } from "react-docgen"
import { existsSync, mkdirSync, rmSync, writeFileSync } from "fs"
import TypedocManager from "../classes/typedoc-manager.js"
import chalk from "chalk"
import CustomResolver from "../resolvers/custom-resolver.js"
import argsPropHandler from "../handlers/argsPropHandler.js"
type GenerateOptions = {
src: string
output: string
clean?: boolean
tsconfigPath: string
disableTypedoc: boolean
verboseTypedoc: boolean
}
export default async function ({
src,
output,
clean,
tsconfigPath,
disableTypedoc,
verboseTypedoc,
}: GenerateOptions) {
const fileContents = readFiles(src)
let outputExists = existsSync(output)
if (clean && outputExists) {
// remove the directory which will be created in the next condition block
rmSync(output, { recursive: true, force: true })
outputExists = false
}
// create output directory if it doesn't exist
if (!outputExists) {
mkdirSync(output)
}
// optionally use typedoc to add missing props, descriptions
// or types.
const typedocManager = new TypedocManager({
tsconfigPath,
disable: disableTypedoc,
verbose: verboseTypedoc,
})
await typedocManager.setup(src)
for (const [filePath, fileContent] of fileContents) {
try {
const relativePath = path.resolve(filePath)
// retrieve the specs of a file
const specs = parse(fileContent, {
filename: relativePath,
resolver: new CustomResolver(typedocManager),
handlers: [
// Built-in handlers
builtinHandlers.childContextTypeHandler,
builtinHandlers.codeTypeHandler,
builtinHandlers.componentDocblockHandler,
builtinHandlers.componentMethodsHandler,
builtinHandlers.contextTypeHandler,
builtinHandlers.defaultPropsHandler,
builtinHandlers.displayNameHandler,
builtinHandlers.propDocblockHandler,
builtinHandlers.propTypeCompositionHandler,
builtinHandlers.propTypeHandler,
// Custom handlers
(documentation, componentPath) =>
argsPropHandler(documentation, componentPath, typedocManager),
],
babelOptions: {
ast: true,
},
})
// write each of the specs into output directory
specs.forEach((spec) => {
if (!spec.displayName) {
return
}
const specNameSplit = spec.displayName.split(".")
let filePath = output
if (spec.description) {
spec.description = typedocManager.normalizeDescription(
spec.description
)
}
// if typedoc isn't disabled, this method will try to fill
// missing descriptions and types, and add missing props.
spec = typedocManager.tryFillWithTypedocData(spec, specNameSplit)
// put the spec in a sub-directory
filePath = path.join(filePath, specNameSplit[0])
if (!existsSync(filePath)) {
mkdirSync(filePath)
}
// write spec to output path
writeFileSync(
path.join(filePath, `${spec.displayName}.json`),
JSON.stringify(spec, null, 2)
)
console.log(chalk.green(`Created spec file for ${spec.displayName}.`))
})
} catch (e) {
console.error(chalk.red(`Failed to parse ${filePath}: ${e}`))
}
}
}

View File

@@ -0,0 +1,126 @@
import { utils, DocumentationBuilder, NodePath } from "react-docgen"
import { ComponentNode } from "react-docgen/dist/resolver/index.js"
import { getDocblock } from "react-docgen/dist/utils/docblock.js"
import TypedocManager from "../classes/typedoc-manager.js"
import emptyPropDescriptor from "../utils/empty-prop-descriptor.js"
import { Node } from "@babel/core"
import isEmptyPropDescriptor from "../utils/is-empty-prop-descriptor.js"
function resolveDocumentation(
documentation: DocumentationBuilder,
path: NodePath<ComponentNode>,
typedocManager?: TypedocManager
) {
if (!path.isObjectExpression() && !path.isObjectPattern()) {
return
}
path.get("properties").forEach((propertyPath) => {
if (propertyPath.isSpreadElement() || propertyPath.isRestElement()) {
const resolvedValuePath = utils.resolveToValue(
propertyPath.get("argument")
) as NodePath<ComponentNode>
resolveDocumentation(documentation, resolvedValuePath)
} else if (
propertyPath.isObjectProperty() ||
propertyPath.isObjectMethod()
) {
const propertyName = utils.getPropertyName(propertyPath)
const propDescriptor = propertyName
? documentation.getPropDescriptor(propertyName)
: undefined
const description =
propDescriptor?.description || getDocblock(propertyPath)
const propExists = propertyName !== null && propDescriptor !== undefined
const shouldRemoveProp =
description?.includes("@ignore") ||
(!description &&
(!propDescriptor || isEmptyPropDescriptor(propDescriptor)))
// remove property if it doesn't have a description or
// if its description includes the `@ignore` tag.
if (!propExists || shouldRemoveProp) {
if (shouldRemoveProp && propExists) {
// prop is removed if its descriptor is empty,
// so we empty it to remove it.
emptyPropDescriptor(propDescriptor)
}
return
}
// set description
utils.setPropDescription(documentation, propertyPath)
// set type if missing
if (!propDescriptor.tsType && typedocManager) {
const typeAnnotation = utils.getTypeAnnotation(path)
if (typeAnnotation?.isTSTypeReference) {
const typeName = typeAnnotation.get("typeName")
if (
!Array.isArray(typeName) &&
typeName.hasNode() &&
typeName.isIdentifier()
) {
const tsType = typedocManager.resolveChildType(
typeName.node.name,
propertyName
)
if (tsType) {
propDescriptor.tsType = tsType
}
}
}
} else if (
propDescriptor.tsType &&
typedocManager?.doesOnlyHaveName(propDescriptor.tsType)
) {
// see if the type needs to be resolved.
const typeReflection = typedocManager?.getReflectionByName(
propDescriptor.tsType.name
)
if (typeReflection && typeReflection.type) {
propDescriptor.tsType =
typedocManager?.getTsType(typeReflection.type) ||
propDescriptor.tsType
}
}
}
})
}
/**
* A handler that resolves props from arguments and
* sets their description, type, etc...
*/
const argsPropHandler = (
documentation: DocumentationBuilder,
componentDefinition: NodePath<ComponentNode>,
typedocManager?: TypedocManager
) => {
let componentParams: NodePath<Node>[] = []
if (componentDefinition.isCallExpression()) {
const args = componentDefinition.get("arguments")
args.forEach((arg) => {
const params = arg.get("params")
if (Array.isArray(params)) {
componentParams.push(...params)
} else {
componentParams.push(params)
}
})
} else if (componentDefinition.isArrowFunctionExpression()) {
componentParams = componentDefinition.get("params")
}
componentParams.forEach((param) => {
const resolvedParam = utils.resolveToValue(param) as NodePath<ComponentNode>
if (!resolvedParam) {
return
}
// set description and type of prop
resolveDocumentation(documentation, resolvedParam, typedocManager)
})
}
export default argsPropHandler

View File

@@ -0,0 +1,38 @@
#!/usr/bin/env node
import { program } from "commander"
import generate from "./commands/generate.js"
import path from "path"
import { fileURLToPath } from "url"
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
program
.description("Generate React specs used for documentation purposes.")
.requiredOption(
"-s, --src <srcPath>",
"Path to a file containing a React component or a directory of React components."
)
.requiredOption("-o, --output <outputPath>", "Path to the output directory.")
.option(
"--clean",
"Clean the output directory before creating the new specs",
false
)
.option(
"--tsconfigPath <tsconfigPath>",
"Path to TSConfig file.",
path.join(
__dirname,
"..",
"..",
"typedoc-config",
"extended-tsconfig",
"ui.json"
)
)
.option("--disable-typedoc", "Whether to disable Typedoc", false)
.option("--verbose-typedoc", "Whether to show Typedoc logs.", false)
.parse()
void generate(program.opts())

View File

@@ -0,0 +1,105 @@
import { visitors } from "@babel/traverse"
import { utils, builtinResolvers, FileState, NodePath } from "react-docgen"
import { ComponentNodePath } from "react-docgen/dist/resolver/index.js"
import TypedocManager from "../classes/typedoc-manager.js"
type State = {
foundDefinitions: Set<ComponentNodePath>
}
/**
* This resolver extends react-docgen's FindAllDefinitionsResolver
* + adds the ability to resolve variable components such as:
*
* ```tsx
* const Value = SelectPrimitive.Value
* Value.displayName = "Select.Value"
* ```
*/
export default class CustomResolver
implements builtinResolvers.FindAllDefinitionsResolver
{
private typedocManager: TypedocManager
constructor(typedocManager: TypedocManager) {
this.typedocManager = typedocManager
}
resolve(file: FileState): ComponentNodePath[] {
const state = {
foundDefinitions: new Set<ComponentNodePath>(),
}
file.traverse(
visitors.explode({
FunctionDeclaration: { enter: this.statelessVisitor },
FunctionExpression: { enter: this.statelessVisitor },
ObjectMethod: { enter: this.statelessVisitor },
ArrowFunctionExpression: { enter: this.statelessVisitor },
ClassExpression: { enter: this.classVisitor },
ClassDeclaration: { enter: this.classVisitor },
VariableDeclaration: {
enter: (path, state: State) => {
const found = path.node.declarations.some((declaration) => {
if (
"name" in declaration.id &&
this.typedocManager.isReactComponent(declaration.id.name) &&
declaration.init
) {
const init = path.get("declarations")[0].get("init")
if (init.isMemberExpression()) {
state.foundDefinitions.add(
path as unknown as ComponentNodePath
)
return true
}
return false
}
})
if (found) {
return path.skip()
}
},
},
CallExpression: {
enter: (path, state: State) => {
const argument = path.get("arguments")[0]
if (!argument) {
return
}
if (utils.isReactForwardRefCall(path)) {
// If the the inner function was previously identified as a component
// replace it with the parent node
const inner = utils.resolveToValue(argument) as ComponentNodePath
state.foundDefinitions.delete(inner)
state.foundDefinitions.add(path)
// Do not traverse into arguments
return path.skip()
} else if (utils.isReactCreateClassCall(path)) {
const resolvedPath = utils.resolveToValue(argument)
if (resolvedPath.isObjectExpression()) {
state.foundDefinitions.add(resolvedPath)
}
// Do not traverse into arguments
return path.skip()
}
},
},
}),
state
)
return Array.from(state.foundDefinitions)
}
classVisitor(path: NodePath, state: State) {
if (utils.isReactComponentClass(path)) {
utils.normalizeClassDefinition(path)
state.foundDefinitions.add(path)
}
path.skip()
}
statelessVisitor(path: NodePath, state: State) {
if (utils.isStatelessComponent(path)) {
state.foundDefinitions.add(path)
}
path.skip()
}
}

View File

@@ -0,0 +1,8 @@
import { PropDescriptor } from "react-docgen/dist/Documentation.js"
export default function emptyPropDescriptor(propDescriptor: PropDescriptor) {
const objKeys = Object.keys(propDescriptor)
objKeys.forEach((key) => {
delete propDescriptor[key as keyof PropDescriptor]
})
}

View File

@@ -0,0 +1,19 @@
import { PropDescriptor } from "react-docgen/dist/Documentation.js"
export default function isEmptyPropDescriptor(propDescriptor: PropDescriptor) {
const objKeys = Object.keys(propDescriptor)
return (
objKeys.length === 0 ||
objKeys.every((objKey) => {
const value = propDescriptor[objKey as keyof PropDescriptor]
switch (typeof value) {
case "string":
return value.length === 0
case "object":
return Object.keys(value).length === 0
default:
return false
}
})
)
}

View File

@@ -0,0 +1,5 @@
const BLACKLISTED_PROPS = ["className", "children"]
export default function isPropBlacklisted(propName: string) {
return BLACKLISTED_PROPS.includes(propName)
}

View File

@@ -0,0 +1,20 @@
import { statSync, readFileSync } from "fs"
import { globSync } from "glob"
export default function readFiles(path: string): Map<string, string> {
const files = new Map<string, string>()
// check if path is for a file
const fileStats = statSync(path)
if (fileStats.isFile()) {
files.set(path, readFileSync(path, "utf-8"))
} else {
const filePaths = globSync(`${path}/**/*.{tsx,jsx}`, {
ignore: [`${path}/**/*.spec.*`, `${path}/**/*.stories.*`],
})
filePaths.forEach((filePath) => {
files.set(filePath, readFileSync(filePath, "utf-8"))
})
}
return files
}