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:
co-authored by
Kasper Fabricius Kristensen
parent
edc49bfe1d
commit
245e5c9a69
@@ -0,0 +1,18 @@
|
||||
# react-docs-generator
|
||||
|
||||
Tool that generates documentation for React components. It's built with [react-docgen](https://react-docgen.dev/) and [Typedoc](https://typedoc.org/).
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
yarn start --src ./path/to/src --output ./path/to/output/dir
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
- `--src <srcPath>`: (required) A path to a file containing React components or a directory to scan its sub-directories and files for components.
|
||||
- `--output <outputPath>`: (required) Path to the directory to store the output in.
|
||||
- `--clean`: If used, the output directory is emptied before generating the new output.
|
||||
- `--tsconfigPath <path>`: Path to a TS Config file which is used by Typedoc. By default, the file at `docs-util/packages/typedoc-config/extended-tsconfig/ui.json` is used.
|
||||
- `--disable-typedoc`: Whether to disable Typedoc and generate the spec file using `react-docgen` only. Useful for debugging.
|
||||
- `--verbose-typedoc`: Whether to show the output of Typedoc. By default, it's disabeld.
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "react-docs-generator",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"start": "ts-node src/index.ts",
|
||||
"generate:ui": "yarn start --src ../../../packages/design-system/ui/src --output ../../../www/apps/ui/src/specs --clean",
|
||||
"build": "tsc",
|
||||
"watch": "tsc --watch",
|
||||
"prepublishOnly": "cross-env NODE_ENV=production tsc --build"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"exports": "./dist/index.js",
|
||||
"bin": "dist/index.js",
|
||||
"dependencies": {
|
||||
"chalk": "^5.3.0",
|
||||
"commander": "^11.1.0",
|
||||
"glob": "^10.3.10",
|
||||
"react-docgen": "^7.0.1",
|
||||
"resolve": "^1.22.8",
|
||||
"ts-node": "^10.9.1",
|
||||
"typedoc": "^0.25.4",
|
||||
"typedoc-plugin-custom": "*",
|
||||
"typescript": "5.2",
|
||||
"utils": "*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.9.4"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typedoc": "0.25.x"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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}`))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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())
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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]
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
const BLACKLISTED_PROPS = ["className", "children"]
|
||||
|
||||
export default function isPropBlacklisted(propName: string) {
|
||||
return BLACKLISTED_PROPS.includes(propName)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"extends": "../../tsconfig",
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"module": "Node16",
|
||||
"moduleResolution": "node16",
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"esModuleInterop": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"resolveJsonModule": true,
|
||||
},
|
||||
"include": ["src", "../typedoc-plugin-custom/src/types"],
|
||||
"ts-node": {
|
||||
"esm": true,
|
||||
"experimentalSpecifierResolution": "node",
|
||||
"transpileOnly": true
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@
|
||||
"glob": "^10.3.10",
|
||||
"randomcolor": "^0.6.2",
|
||||
"ts-node": "^10.9.1",
|
||||
"typedoc": "0.25.1",
|
||||
"typedoc": "^0.25.4",
|
||||
"typedoc-config": "*",
|
||||
"typedoc-monorepo-link-types": "^0.0.2",
|
||||
"typedoc-plugin-custom": "*",
|
||||
|
||||
@@ -25,6 +25,14 @@
|
||||
{
|
||||
"tagName": "@docHideSignature",
|
||||
"syntaxKind": "modifier"
|
||||
},
|
||||
{
|
||||
"tagName": "@keep",
|
||||
"syntaxKind": "modifier"
|
||||
},
|
||||
{
|
||||
"tagName": "@excludeExternal",
|
||||
"syntaxKind": "modifier"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"$schema": "http://json.schemastore.org/tsconfig",
|
||||
"extends": [
|
||||
"../../../../packages/design-system/ui/tsconfig.esm.json"
|
||||
]
|
||||
}
|
||||
@@ -180,8 +180,8 @@ function addComments(schema: Schema, reflection: Reflection) {
|
||||
"type" in reflection
|
||||
? getTypeChildren(reflection.type as SomeType, reflection.project)
|
||||
: "children" in reflection
|
||||
? (reflection.children as DeclarationReflection[])
|
||||
: []
|
||||
? (reflection.children as DeclarationReflection[])
|
||||
: []
|
||||
|
||||
Object.entries(schema.properties).forEach(([key, value]) => {
|
||||
const childItem =
|
||||
|
||||
@@ -22,9 +22,7 @@ let hasMonkeyPatched = false
|
||||
|
||||
export function load(app: Application) {
|
||||
if (hasMonkeyPatched) {
|
||||
throw new Error(
|
||||
"typedoc-plugin-missing-exports cannot be loaded multiple times"
|
||||
)
|
||||
throw new Error("typedoc-plugin-custom cannot be loaded multiple times")
|
||||
}
|
||||
hasMonkeyPatched = true
|
||||
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
export declare module "typedoc" {
|
||||
declare interface TypeDocOptionMap {
|
||||
enableInternalResolve: boolean
|
||||
internalModule: string
|
||||
}
|
||||
}
|
||||
@@ -4,5 +4,6 @@
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["src"]
|
||||
"include": ["src", "src/types"],
|
||||
"typeRoots": ["./src/types/index.d.ts"]
|
||||
}
|
||||
+2
-1
@@ -1,7 +1,8 @@
|
||||
import * as Handlebars from "handlebars"
|
||||
import { PageEvent } from "typedoc"
|
||||
import { MarkdownTheme } from "../../theme"
|
||||
import { escapeChars, getDisplayName } from "../../utils"
|
||||
import { getDisplayName } from "../../utils"
|
||||
import { escapeChars } from "utils"
|
||||
|
||||
export default function (theme: MarkdownTheme) {
|
||||
Handlebars.registerHelper("breadcrumbs", function (this: PageEvent) {
|
||||
|
||||
@@ -3,7 +3,7 @@ import * as Handlebars from "handlebars"
|
||||
import * as Path from "path"
|
||||
import { CommentDisplayPart } from "typedoc/dist/lib/models/comments/comment"
|
||||
import { MarkdownTheme } from "../../theme"
|
||||
import { escapeChars } from "../../utils"
|
||||
import { escapeChars } from "utils"
|
||||
|
||||
export default function (theme: MarkdownTheme) {
|
||||
Handlebars.registerHelper("comment", function (parts: CommentDisplayPart[]) {
|
||||
@@ -30,8 +30,8 @@ export default function (theme: MarkdownTheme) {
|
||||
typeof part.target === "string"
|
||||
? part.target
|
||||
: "url" in part.target
|
||||
? Handlebars.helpers.relativeURL(part.target.url)
|
||||
: ""
|
||||
? Handlebars.helpers.relativeURL(part.target.url)
|
||||
: ""
|
||||
const wrap = part.tag === "@linkcode" ? "`" : ""
|
||||
result.push(
|
||||
url ? `[${wrap}${part.text}${wrap}](${url})` : part.text
|
||||
|
||||
+2
-6
@@ -7,12 +7,8 @@ import {
|
||||
ReflectionType,
|
||||
} from "typedoc"
|
||||
import { MarkdownTheme } from "../../theme"
|
||||
import {
|
||||
escapeChars,
|
||||
memberSymbol,
|
||||
stripComments,
|
||||
stripLineBreaks,
|
||||
} from "../../utils"
|
||||
import { memberSymbol, stripComments } from "../../utils"
|
||||
import { escapeChars, stripLineBreaks } from "utils"
|
||||
|
||||
export default function (theme: MarkdownTheme) {
|
||||
Handlebars.registerHelper(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as Handlebars from "handlebars"
|
||||
import { escapeChars } from "../../utils"
|
||||
import { escapeChars } from "utils"
|
||||
|
||||
export default function () {
|
||||
Handlebars.registerHelper("escape", function (str: string) {
|
||||
|
||||
+2
-1
@@ -1,7 +1,8 @@
|
||||
import * as Handlebars from "handlebars"
|
||||
import { PageEvent, ParameterReflection, ReflectionKind } from "typedoc"
|
||||
import { MarkdownTheme } from "../../theme"
|
||||
import { escapeChars, getDisplayName } from "../../utils"
|
||||
import { getDisplayName } from "../../utils"
|
||||
import { escapeChars } from "utils"
|
||||
|
||||
export default function (theme: MarkdownTheme) {
|
||||
Handlebars.registerHelper(
|
||||
|
||||
+2
-1
@@ -4,8 +4,9 @@ import {
|
||||
ReflectionKind,
|
||||
SignatureReflection,
|
||||
} from "typedoc"
|
||||
import { getHTMLChar, memberSymbol } from "../../utils"
|
||||
import { memberSymbol } from "../../utils"
|
||||
import { MarkdownTheme } from "../../theme"
|
||||
import { getHTMLChar } from "utils"
|
||||
|
||||
export default function (theme: MarkdownTheme) {
|
||||
Handlebars.registerHelper(
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
ReflectionGroup,
|
||||
} from "typedoc"
|
||||
import { MarkdownTheme } from "../../theme"
|
||||
import { escapeChars } from "../../utils"
|
||||
import { escapeChars } from "utils"
|
||||
|
||||
export default function (theme: MarkdownTheme) {
|
||||
Handlebars.registerHelper(
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import * as Handlebars from "handlebars"
|
||||
import { SignatureReflection } from "typedoc"
|
||||
import { ArrayType, ReferenceType } from "typedoc/dist/lib/models/types"
|
||||
import { escapeChars } from "../../utils"
|
||||
import { escapeChars } from "utils"
|
||||
|
||||
export default function () {
|
||||
Handlebars.registerHelper(
|
||||
|
||||
+1
-1
@@ -1,10 +1,10 @@
|
||||
import * as Handlebars from "handlebars"
|
||||
import { DeclarationReflection, ReflectionType } from "typedoc"
|
||||
import { MarkdownTheme } from "../../theme"
|
||||
import { escapeChars, stripLineBreaks } from "../../utils"
|
||||
import { parseParams } from "../../utils/params-utils"
|
||||
import { ReflectionParameterType } from "../../types"
|
||||
import reflectionFormatter from "../../utils/reflection-formatter"
|
||||
import { escapeChars, stripLineBreaks } from "utils"
|
||||
|
||||
export default function (theme: MarkdownTheme) {
|
||||
Handlebars.registerHelper(
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ import {
|
||||
getTableHeaders,
|
||||
reflectionTableFormatter,
|
||||
} from "../../utils/reflection-formatter"
|
||||
import { hasTypes } from "../../utils/type-utils"
|
||||
import { hasTypes } from "utils"
|
||||
|
||||
export default function () {
|
||||
Handlebars.registerHelper(
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
UnionType,
|
||||
UnknownType,
|
||||
} from "typedoc"
|
||||
import getType, { Collapse } from "../../utils/type-utils"
|
||||
import { Collapse, getType } from "utils"
|
||||
|
||||
export default function () {
|
||||
Handlebars.registerHelper(
|
||||
@@ -43,6 +43,7 @@ export default function () {
|
||||
reflectionType: this,
|
||||
collapse,
|
||||
wrapBackticks: emphasis,
|
||||
getRelativeUrlMethod: Handlebars.helpers.relativeURL,
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
ReflectionKind,
|
||||
SignatureReflection,
|
||||
} from "typedoc"
|
||||
import { stripLineBreaks } from "utils"
|
||||
|
||||
export function formatContents(contents: string) {
|
||||
return (
|
||||
@@ -16,19 +17,6 @@ export function formatContents(contents: string) {
|
||||
)
|
||||
}
|
||||
|
||||
export function getHTMLChar(str: string) {
|
||||
return str
|
||||
.replace(/</g, "<")
|
||||
.replace(/{/g, "{")
|
||||
.replace(/}/g, "}")
|
||||
.replace(/>/g, ">")
|
||||
}
|
||||
|
||||
export function escapeChars(str: string, escapeBackticks = true) {
|
||||
const result = getHTMLChar(str).replace(/_/g, "\\_").replace(/\|/g, "\\|")
|
||||
return escapeBackticks ? result.replace(/`/g, "\\`") : result
|
||||
}
|
||||
|
||||
export function memberSymbol(
|
||||
reflection: DeclarationReflection | ParameterReflection | SignatureReflection
|
||||
) {
|
||||
@@ -57,17 +45,6 @@ export function stripComments(str: string) {
|
||||
.replace(/^\s+|\s+$|(\s)+/g, "$1")
|
||||
}
|
||||
|
||||
export function stripLineBreaks(str: string) {
|
||||
return str
|
||||
? str
|
||||
.replace(/\n/g, " ")
|
||||
.replace(/\r/g, " ")
|
||||
.replace(/\t/g, " ")
|
||||
.replace(/[\s]{2,}/g, " ")
|
||||
.trim()
|
||||
: ""
|
||||
}
|
||||
|
||||
export function stripCode(str: string) {
|
||||
return stripLineBreaks(str.replace("```ts", "").replace("```", ""))
|
||||
}
|
||||
|
||||
@@ -6,10 +6,14 @@ import {
|
||||
ReflectionType,
|
||||
} from "typedoc"
|
||||
import * as Handlebars from "handlebars"
|
||||
import { stripCode, stripLineBreaks } from "../utils"
|
||||
import { stripCode } from "../utils"
|
||||
import { Parameter, ParameterStyle, ReflectionParameterType } from "../types"
|
||||
import getType, { getReflectionType } from "./type-utils"
|
||||
import { getTypeChildren } from "utils"
|
||||
import {
|
||||
getReflectionType,
|
||||
getType,
|
||||
getTypeChildren,
|
||||
stripLineBreaks,
|
||||
} from "utils"
|
||||
import { MarkdownTheme } from "../theme"
|
||||
|
||||
const ALLOWED_KINDS: ReflectionKind[] = [
|
||||
@@ -124,12 +128,14 @@ export function reflectionComponentFormatter({
|
||||
reflectionType: reflection.type,
|
||||
collapse: "object",
|
||||
project: reflection.project,
|
||||
getRelativeUrlMethod: Handlebars.helpers.relativeURL,
|
||||
})
|
||||
: getReflectionType({
|
||||
reflectionType: reflection,
|
||||
collapse: "object",
|
||||
wrapBackticks: true,
|
||||
project: reflection.project,
|
||||
getRelativeUrlMethod: Handlebars.helpers.relativeURL,
|
||||
}),
|
||||
description: comments
|
||||
? Handlebars.helpers.comments(comments, true, false)
|
||||
|
||||
+3
-2
@@ -7,14 +7,13 @@ import {
|
||||
TypeParameterReflection,
|
||||
} from "typedoc"
|
||||
import * as Handlebars from "handlebars"
|
||||
import getType from "./type-utils"
|
||||
import { Parameter } from "../types"
|
||||
import {
|
||||
getDefaultValue,
|
||||
reflectionComponentFormatter,
|
||||
} from "./reflection-formatter"
|
||||
import { MarkdownTheme } from "../theme"
|
||||
import { getProjectChild, getTypeChildren } from "utils"
|
||||
import { getProjectChild, getType, getTypeChildren } from "utils"
|
||||
|
||||
type ReturnReflectionComponentFormatterParams = {
|
||||
reflectionType: SomeType
|
||||
@@ -37,11 +36,13 @@ export function returnReflectionComponentFormatter({
|
||||
wrapBackticks: false,
|
||||
hideLink: true,
|
||||
project,
|
||||
getRelativeUrlMethod: Handlebars.helpers.relativeURL,
|
||||
})
|
||||
const type = getType({
|
||||
reflectionType: reflectionType,
|
||||
collapse: "object",
|
||||
project,
|
||||
getRelativeUrlMethod: Handlebars.helpers.relativeURL,
|
||||
})
|
||||
const componentItem: Parameter[] = []
|
||||
const canRetrieveChildren = level + 1 <= (maxLevel || MarkdownTheme.MAX_LEVEL)
|
||||
|
||||
@@ -22,12 +22,15 @@ export function getTypeChildren(
|
||||
break
|
||||
case "reference":
|
||||
// eslint-disable-next-line no-case-declarations
|
||||
const referencedReflection =
|
||||
reflectionType.reflection && "children" in reflectionType.reflection
|
||||
const referencedReflection = reflectionType.reflection
|
||||
? "children" in reflectionType.reflection
|
||||
? reflectionType.reflection
|
||||
: project
|
||||
? getProjectChild(project, reflectionType.name)
|
||||
? project.getReflectionById(reflectionType.reflection.id)
|
||||
: undefined
|
||||
: project
|
||||
? getProjectChild(project, reflectionType.name)
|
||||
: undefined
|
||||
|
||||
if (referencedReflection instanceof DeclarationReflection) {
|
||||
if (referencedReflection.children) {
|
||||
|
||||
+14
-11
@@ -7,6 +7,7 @@ import {
|
||||
IntersectionType,
|
||||
IntrinsicType,
|
||||
LiteralType,
|
||||
ParameterReflection,
|
||||
ProjectReflection,
|
||||
QueryType,
|
||||
ReferenceType,
|
||||
@@ -19,10 +20,13 @@ import {
|
||||
UnionType,
|
||||
UnknownType,
|
||||
} from "typedoc"
|
||||
import * as Handlebars from "handlebars"
|
||||
import { ReflectionParameterType } from "../types"
|
||||
import { escapeChars, getHTMLChar } from "../utils"
|
||||
import { getProjectChild } from "utils"
|
||||
import { escapeChars, getHTMLChar } from "./str-utils"
|
||||
import { getProjectChild } from "./get-project-child"
|
||||
|
||||
export type ReflectionParameterType =
|
||||
| ParameterReflection
|
||||
| DeclarationReflection
|
||||
| TypeParameterReflection
|
||||
|
||||
export type Collapse = "object" | "function" | "all" | "none"
|
||||
|
||||
@@ -33,12 +37,10 @@ export type TypeOptions<T = SomeType> = {
|
||||
hideLink?: boolean
|
||||
escape?: boolean
|
||||
project?: ProjectReflection
|
||||
getRelativeUrlMethod?: (url: string) => string
|
||||
}
|
||||
|
||||
export default function getType({
|
||||
reflectionType,
|
||||
...options
|
||||
}: TypeOptions): string {
|
||||
export function getType({ reflectionType, ...options }: TypeOptions): string {
|
||||
if (reflectionType instanceof ReferenceType) {
|
||||
return getReferenceType({
|
||||
reflectionType,
|
||||
@@ -304,6 +306,7 @@ export function getReferenceType({
|
||||
hideLink = false,
|
||||
escape,
|
||||
project,
|
||||
getRelativeUrlMethod,
|
||||
...options
|
||||
}: TypeOptions<ReferenceType>): string {
|
||||
escape = getShouldEscape(wrapBackticks, escape)
|
||||
@@ -321,9 +324,9 @@ export function getReferenceType({
|
||||
if (modelReflection?.url) {
|
||||
reflection.push(
|
||||
shouldShowLink
|
||||
? `[${modelReflection.name}](${Handlebars.helpers.relativeURL(
|
||||
modelReflection.url
|
||||
)})`
|
||||
? `[${modelReflection.name}](${
|
||||
getRelativeUrlMethod?.(modelReflection.url) || modelReflection.url
|
||||
})`
|
||||
: getFormattedStr(modelReflection.name, false, escape)
|
||||
)
|
||||
} else {
|
||||
@@ -1,2 +1,4 @@
|
||||
export * from "./get-type-children"
|
||||
export * from "./get-project-child"
|
||||
export * from "./get-type-str"
|
||||
export * from "./str-utils"
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
export function getHTMLChar(str: string) {
|
||||
return str
|
||||
.replace(/</g, "<")
|
||||
.replace(/{/g, "{")
|
||||
.replace(/}/g, "}")
|
||||
.replace(/>/g, ">")
|
||||
}
|
||||
|
||||
export function escapeChars(str: string, escapeBackticks = true) {
|
||||
const result = getHTMLChar(str).replace(/_/g, "\\_").replace(/\|/g, "\\|")
|
||||
return escapeBackticks ? result.replace(/`/g, "\\`") : result
|
||||
}
|
||||
|
||||
export function stripLineBreaks(str: string) {
|
||||
return str
|
||||
? str
|
||||
.replace(/\n/g, " ")
|
||||
.replace(/\r/g, " ")
|
||||
.replace(/\t/g, " ")
|
||||
.replace(/[\s]{2,}/g, " ")
|
||||
.trim()
|
||||
: ""
|
||||
}
|
||||
Reference in New Issue
Block a user