OpenPanel/documentation/plugins/docgen.ts
2024-02-05 10:23:04 +01:00

388 lines
12 KiB
TypeScript

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<ComponentDoc, "methods"> &
Partial<Pick<ComponentDoc, "methods">> & {
generatedAt?: number;
};
type DocgenContent = Record<string, Record<string, DeclarationType>>;
/** 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<string, string> = {
// "null | string | number | false | true | ReactElement<any, string | JSXElementConstructor<any>> | ReactFragment | ReactPortal": "ReactNode",
ReactElement:
"ReactElement<any, string | ((props: any) => ReactElement<any, any>) | (new (props: any) => Component<any, any, any>)>",
"ReactNode | (value: number) => ReactNode":
"string | number | boolean | {} | ReactElement<any, string | ((props: any) => ReactElement<any, any>) | (new (props: any) => Component<any, any, any>)> | ReactNodeArray | ReactPortal | ((value: number) => ReactNode)",
ActionButtonRenderer:
"ReactNode | ({ defaultButtons: ReactNode }) => ReactNode",
"DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>":
"DetailedHTMLProps<HTMLDivElement>",
"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<BaseRecord>) => 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<string, string> = {};
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<string, DeclarationType> = {};
declarations.forEach((declaration) => {
transposed[declaration.displayName] = declaration;
});
return transposed;
};
const generateDeclarations = async (packagePaths: [string, string][]) => {
const generated: Record<string, Record<string, DeclarationType>> = {};
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<DocgenContent> {
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<void> {
if (!process.env.DISABLE_DOCGEN) {
ora("Creating Refine declaration files...").succeed();
const { createData } = actions;
const data: Promise<string>[] = [];
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);
}
},
};
}