import { Plugin } from "@docusaurus/types"; // import fs from "fs-extra"; import ora from "ora"; import path from "path"; import { ComponentDoc, PropItem, withCustomConfig, } from "react-docgen-typescript"; import { ParentType, Props } from "react-docgen-typescript/lib/parser"; import ts from "typescript"; /** TYPES */ type DeclarationType = Omit & Partial> & { generatedAt?: number; }; type DocgenContent = Record>; /** CONSTANTS */ const packagesDir = path.join(__dirname, "./../..", "./packages"); const sourceDir = "./src"; const excludedFilePatterns = [ "node_modules", "tsup.config.ts", ".test.", ".spec.", ]; const excludedValueDeclarationPatterns = ["node_modules/antd/lib/list/"]; const excludePropPatterns = [/^__.*/]; const excludedProps = [ "className", "classNames", "styles", "unstyled", "component", "key", "ref", "style", "sx", "m", "mx", "my", "mt", "ml", "mr", "mb", "p", "px", "py", "pt", "pl", "pr", "pb", ]; const replacementProps: Record = { // "null | string | number | false | true | ReactElement> | ReactFragment | ReactPortal": "ReactNode", ReactElement: "ReactElement ReactElement) | (new (props: any) => Component)>", "ReactNode | (value: number) => ReactNode": "string | number | boolean | {} | ReactElement ReactElement) | (new (props: any) => Component)> | ReactNodeArray | ReactPortal | ((value: number) => ReactNode)", ActionButtonRenderer: "ReactNode | ({ defaultButtons: ReactNode }) => ReactNode", "DetailedHTMLProps, HTMLDivElement>": "DetailedHTMLProps", "false | OpenNotificationParams | ((data?: unknown, values?: unknown, resource?: string) => OpenNotificationParams)": "false | OpenNotificationParams | (data, values, resource) => OpenNotificationParams", "false | OpenNotificationParams | ((error?: unknown, values?: unknown, resource?: string) => OpenNotificationParams)": "false | OpenNotificationParams | (error, values, resource) => OpenNotificationParams", 'SvgIconProps<"svg", {}>': "SvgIconProps", SpaceProps: "[`SpaceProps`](https://styled-system.com/api#space)", "((value: DeleteOneResponse) => void)": "(value: DeleteOneResponse) => void", "{ [key: string]: any; ids?: BaseKey[]; }": "{ [key]: any; ids?: BaseKey[]; }", "BaseKey | BaseKey[]": "[BaseKey](/docs/core/interface-references/#basekey) | [BaseKey[]](/docs/core/interface-references/#basekey)", BaseKey: "[BaseKey](/docs/core/interface-references/#basekey)", MetaDataQuery: "[MetaDataQuery](/docs/core/interface-references/#metadataquery)", CrudFilters: "[CrudFilters](/docs/core/interface-references/#crudfilters)", CrudSorting: "[CrudSorting](/docs/core/interface-references/#crudsorting)", }; const spinner = ora("Generating Refine declarations..."); /** HELPERS */ const getPackageNamePathMap = async (directory: string) => { const packages = await fs.readdir(directory); const packageNamePathMap: Record = {}; const includedPackages = process.env.INCLUDED_PACKAGES?.split(",") || []; await Promise.all( packages.map(async (packageName) => { const packagePath = path.join( directory, packageName, "package.json", ); if (fs.existsSync(packagePath)) { const packageJson = await fs.readJSON(packagePath); if ( includedPackages.length == 0 || includedPackages.some((p) => packageName.includes(p)) ) { packageNamePathMap[packageJson.name] = path.join( packagePath, "..", ); } } return packageName; }), ); return packageNamePathMap; }; const getPaths = async (packageDir: string, excludedPatterns: string[]) => { const dir = await fs.readdir(packageDir); const filtered: string[] = []; await Promise.all( dir.map(async (file) => { const result = await fs.pathExists(path.join(packageDir, file)); if (result) { filtered.push(file); } }), ); return filtered .map((p) => path.join(packageDir, p)) .filter( (p) => !excludedPatterns.some((pattern) => p.includes(pattern)), ); }; const _getPrefixFromDeclarationPath = async (path: string) => { const map = await getPackageNamePathMap(packagesDir); const packageName = Object.keys(map).find((key) => path.includes(map[key])); return packageName; }; const getComponentName = (name: string, _fileName: string) => { return name; // return `${getPrefixFromDeclarationPath(fileName)}#${name}`; }; const getOutputName = (packageName: string) => { return packageName; }; const declarationFilter = (declaration: ParentType) => { return ( !declaration.fileName.includes("node_modules") || declaration.fileName.includes("@refinedev") ); }; const valueDeclarationFilter = (tsDeclaration?: ts.Declaration) => { // excludedValueDeclarationPatterns includes fileNames of source files to be ignored (partially) const sourceFileName = tsDeclaration?.getSourceFile().fileName; // if sourceFileName includes any of the excludedValueDeclarationPatterns then ignore it const isIgnored = excludedValueDeclarationPatterns.some((pattern) => sourceFileName?.includes(pattern), ); return !isIgnored; }; const createParser = (configPath: string) => { const docgenParser = withCustomConfig(path.join(configPath), { savePropValueAsString: true, shouldExtractLiteralValuesFromEnum: true, shouldRemoveUndefinedFromOptional: true, shouldIncludePropTagMap: true, componentNameResolver: (exp, source) => { const name = getComponentName(exp.getName(), source.fileName); if (valueDeclarationFilter(exp.valueDeclaration)) { return name; } return `IGNORED_${name}`; }, propFilter: (prop: PropItem) => { const isExcluded = excludedProps.includes(prop.name) || excludePropPatterns.some((pattern) => pattern.test(prop.name)); const isExternal = prop.declarations && prop.declarations.length > 0 && !Boolean(prop.declarations.find(declarationFilter)); const isUnknown = typeof prop.declarations === "undefined"; if (isExcluded || isExternal || isUnknown) { return false; } return true; }, }); return docgenParser; }; const normalizeMarkdownLinks = (value: string) => { return value.replace(/\[(.*?)\]\s{1}\((.*?)\)/g, (_, p1, p2) => { return `[${p1}](${p2})`; }); }; const prepareDeclaration = (declaration: ComponentDoc) => { const data: DeclarationType = { ...declaration }; delete data.methods; delete data.tags; data.generatedAt = Date.now(); Object.keys(data.props).forEach((prop) => { data.props[prop].type.name = normalizeMarkdownLinks( data.props[prop].type.name, ); delete data.props[prop].parent; delete data.props[prop].declarations; if (data.props[prop].type.raw === "ReactNode") { data.props[prop].type.name = "ReactNode"; } if (data.props[prop].type.name in replacementProps) { data.props[prop].type.name = replacementProps[data.props[prop].type.name]; } if (data.props[prop].type.name === "enum") { data.props[prop].type.name = data.props[prop].type.value .map((val: { value: string }) => val.value) .join(" | "); } }); const ordered = Object.keys(data.props) // .sort() .reduce((obj, key) => { obj[key] = data.props[key]; return obj; }, {} as Props); data.props = ordered; return data; }; const transposeDeclarations = (declarations: DeclarationType[]) => { const transposed: Record = {}; declarations.forEach((declaration) => { transposed[declaration.displayName] = declaration; }); return transposed; }; const generateDeclarations = async (packagePaths: [string, string][]) => { const generated: Record> = {}; await Promise.all( packagePaths.map(async ([packageName, packagePath]) => { const parser = createParser( path.join(packagePath, "./tsconfig.json"), ); const sourcePath = path.join(packagePath, sourceDir); if (!(await fs.pathExists(sourcePath))) { spinner.fail("Component path does not exist", sourcePath); process.exit(1); } const declarationPaths = await getPaths( sourcePath, excludedFilePatterns, ); const parsed = parser .parse(declarationPaths) .map(prepareDeclaration); const transposed = transposeDeclarations(parsed); const outputName = getOutputName(packageName); generated[outputName] = transposed; spinner.stop(); spinner.start(`- Generated declarations - ${packageName}`); return [packageName, packagePath]; }), ); return generated; }; /** DOCGEN */ const handleDocgen = async () => { const packagePathMap = await getPackageNamePathMap(packagesDir); const packagePathMapArray = Object.entries(packagePathMap); spinner.stop(); spinner.start(`- Found ${packagePathMapArray.length} packages`); const res = await generateDeclarations(packagePathMapArray); spinner.succeed("Generated declarations"); return res; }; export default function plugin(): Plugin { return { name: "docusaurus-plugin-refine-docgen", getPathsToWatch: function () { return [packagesDir]; }, async loadContent() { if (!process.env.DISABLE_DOCGEN) { spinner.start(); return await handleDocgen(); } return {}; }, configureWebpack(config) { return { resolve: { alias: { "@docgen": path.join( config.resolve?.alias?.["@generated"], "docusaurus-plugin-refine-docgen", "default", ), }, }, }; }, async contentLoaded({ content, actions }): Promise { if (!process.env.DISABLE_DOCGEN) { ora("Creating Refine declaration files...").succeed(); const { createData } = actions; const data: Promise[] = []; Object.entries(content).forEach( ([packageName, packageDeclarations]) => { Object.entries(packageDeclarations).forEach( ([componentName, declaration]) => { data.push( createData( `${packageName}/${componentName}.json`, JSON.stringify(declaration), ), ); }, ); }, ); await Promise.all(data); } }, }; }