docgen.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387
  1. import { Plugin } from "@docusaurus/types";
  2. //
  3. import fs from "fs-extra";
  4. import ora from "ora";
  5. import path from "path";
  6. import {
  7. ComponentDoc,
  8. PropItem,
  9. withCustomConfig,
  10. } from "react-docgen-typescript";
  11. import { ParentType, Props } from "react-docgen-typescript/lib/parser";
  12. import ts from "typescript";
  13. /** TYPES */
  14. type DeclarationType = Omit<ComponentDoc, "methods"> &
  15. Partial<Pick<ComponentDoc, "methods">> & {
  16. generatedAt?: number;
  17. };
  18. type DocgenContent = Record<string, Record<string, DeclarationType>>;
  19. /** CONSTANTS */
  20. const packagesDir = path.join(__dirname, "./../..", "./packages");
  21. const sourceDir = "./src";
  22. const excludedFilePatterns = [
  23. "node_modules",
  24. "tsup.config.ts",
  25. ".test.",
  26. ".spec.",
  27. ];
  28. const excludedValueDeclarationPatterns = ["node_modules/antd/lib/list/"];
  29. const excludePropPatterns = [/^__.*/];
  30. const excludedProps = [
  31. "className",
  32. "classNames",
  33. "styles",
  34. "unstyled",
  35. "component",
  36. "key",
  37. "ref",
  38. "style",
  39. "sx",
  40. "m",
  41. "mx",
  42. "my",
  43. "mt",
  44. "ml",
  45. "mr",
  46. "mb",
  47. "p",
  48. "px",
  49. "py",
  50. "pt",
  51. "pl",
  52. "pr",
  53. "pb",
  54. ];
  55. const replacementProps: Record<string, string> = {
  56. // "null | string | number | false | true | ReactElement<any, string | JSXElementConstructor<any>> | ReactFragment | ReactPortal": "ReactNode",
  57. ReactElement:
  58. "ReactElement<any, string | ((props: any) => ReactElement<any, any>) | (new (props: any) => Component<any, any, any>)>",
  59. "ReactNode | (value: number) => ReactNode":
  60. "string | number | boolean | {} | ReactElement<any, string | ((props: any) => ReactElement<any, any>) | (new (props: any) => Component<any, any, any>)> | ReactNodeArray | ReactPortal | ((value: number) => ReactNode)",
  61. ActionButtonRenderer:
  62. "ReactNode | ({ defaultButtons: ReactNode }) => ReactNode",
  63. "DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>":
  64. "DetailedHTMLProps<HTMLDivElement>",
  65. "false | OpenNotificationParams | ((data?: unknown, values?: unknown, resource?: string) => OpenNotificationParams)":
  66. "false | OpenNotificationParams | (data, values, resource) => OpenNotificationParams",
  67. "false | OpenNotificationParams | ((error?: unknown, values?: unknown, resource?: string) => OpenNotificationParams)":
  68. "false | OpenNotificationParams | (error, values, resource) => OpenNotificationParams",
  69. 'SvgIconProps<"svg", {}>': "SvgIconProps",
  70. SpaceProps: "[`SpaceProps`](https://styled-system.com/api#space)",
  71. "((value: DeleteOneResponse<BaseRecord>) => void)":
  72. "(value: DeleteOneResponse) => void",
  73. "{ [key: string]: any; ids?: BaseKey[]; }":
  74. "{ [key]: any; ids?: BaseKey[]; }",
  75. "BaseKey | BaseKey[]":
  76. "[BaseKey](/docs/core/interface-references/#basekey) | [BaseKey[]](/docs/core/interface-references/#basekey)",
  77. BaseKey: "[BaseKey](/docs/core/interface-references/#basekey)",
  78. MetaDataQuery:
  79. "[MetaDataQuery](/docs/core/interface-references/#metadataquery)",
  80. CrudFilters: "[CrudFilters](/docs/core/interface-references/#crudfilters)",
  81. CrudSorting: "[CrudSorting](/docs/core/interface-references/#crudsorting)",
  82. };
  83. const spinner = ora("Generating Refine declarations...");
  84. /** HELPERS */
  85. const getPackageNamePathMap = async (directory: string) => {
  86. const packages = await fs.readdir(directory);
  87. const packageNamePathMap: Record<string, string> = {};
  88. const includedPackages = process.env.INCLUDED_PACKAGES?.split(",") || [];
  89. await Promise.all(
  90. packages.map(async (packageName) => {
  91. const packagePath = path.join(
  92. directory,
  93. packageName,
  94. "package.json",
  95. );
  96. if (fs.existsSync(packagePath)) {
  97. const packageJson = await fs.readJSON(packagePath);
  98. if (
  99. includedPackages.length == 0 ||
  100. includedPackages.some((p) => packageName.includes(p))
  101. ) {
  102. packageNamePathMap[packageJson.name] = path.join(
  103. packagePath,
  104. "..",
  105. );
  106. }
  107. }
  108. return packageName;
  109. }),
  110. );
  111. return packageNamePathMap;
  112. };
  113. const getPaths = async (packageDir: string, excludedPatterns: string[]) => {
  114. const dir = await fs.readdir(packageDir);
  115. const filtered: string[] = [];
  116. await Promise.all(
  117. dir.map(async (file) => {
  118. const result = await fs.pathExists(path.join(packageDir, file));
  119. if (result) {
  120. filtered.push(file);
  121. }
  122. }),
  123. );
  124. return filtered
  125. .map((p) => path.join(packageDir, p))
  126. .filter(
  127. (p) => !excludedPatterns.some((pattern) => p.includes(pattern)),
  128. );
  129. };
  130. const _getPrefixFromDeclarationPath = async (path: string) => {
  131. const map = await getPackageNamePathMap(packagesDir);
  132. const packageName = Object.keys(map).find((key) => path.includes(map[key]));
  133. return packageName;
  134. };
  135. const getComponentName = (name: string, _fileName: string) => {
  136. return name;
  137. // return `${getPrefixFromDeclarationPath(fileName)}#${name}`;
  138. };
  139. const getOutputName = (packageName: string) => {
  140. return packageName;
  141. };
  142. const declarationFilter = (declaration: ParentType) => {
  143. return (
  144. !declaration.fileName.includes("node_modules") ||
  145. declaration.fileName.includes("@refinedev")
  146. );
  147. };
  148. const valueDeclarationFilter = (tsDeclaration?: ts.Declaration) => {
  149. // excludedValueDeclarationPatterns includes fileNames of source files to be ignored (partially)
  150. const sourceFileName = tsDeclaration?.getSourceFile().fileName;
  151. // if sourceFileName includes any of the excludedValueDeclarationPatterns then ignore it
  152. const isIgnored = excludedValueDeclarationPatterns.some((pattern) =>
  153. sourceFileName?.includes(pattern),
  154. );
  155. return !isIgnored;
  156. };
  157. const createParser = (configPath: string) => {
  158. const docgenParser = withCustomConfig(path.join(configPath), {
  159. savePropValueAsString: true,
  160. shouldExtractLiteralValuesFromEnum: true,
  161. shouldRemoveUndefinedFromOptional: true,
  162. shouldIncludePropTagMap: true,
  163. componentNameResolver: (exp, source) => {
  164. const name = getComponentName(exp.getName(), source.fileName);
  165. if (valueDeclarationFilter(exp.valueDeclaration)) {
  166. return name;
  167. }
  168. return `IGNORED_${name}`;
  169. },
  170. propFilter: (prop: PropItem) => {
  171. const isExcluded =
  172. excludedProps.includes(prop.name) ||
  173. excludePropPatterns.some((pattern) => pattern.test(prop.name));
  174. const isExternal =
  175. prop.declarations &&
  176. prop.declarations.length > 0 &&
  177. !Boolean(prop.declarations.find(declarationFilter));
  178. const isUnknown = typeof prop.declarations === "undefined";
  179. if (isExcluded || isExternal || isUnknown) {
  180. return false;
  181. }
  182. return true;
  183. },
  184. });
  185. return docgenParser;
  186. };
  187. const normalizeMarkdownLinks = (value: string) => {
  188. return value.replace(/\[(.*?)\]\s{1}\((.*?)\)/g, (_, p1, p2) => {
  189. return `[${p1}](${p2})`;
  190. });
  191. };
  192. const prepareDeclaration = (declaration: ComponentDoc) => {
  193. const data: DeclarationType = { ...declaration };
  194. delete data.methods;
  195. delete data.tags;
  196. data.generatedAt = Date.now();
  197. Object.keys(data.props).forEach((prop) => {
  198. data.props[prop].type.name = normalizeMarkdownLinks(
  199. data.props[prop].type.name,
  200. );
  201. delete data.props[prop].parent;
  202. delete data.props[prop].declarations;
  203. if (data.props[prop].type.raw === "ReactNode") {
  204. data.props[prop].type.name = "ReactNode";
  205. }
  206. if (data.props[prop].type.name in replacementProps) {
  207. data.props[prop].type.name =
  208. replacementProps[data.props[prop].type.name];
  209. }
  210. if (data.props[prop].type.name === "enum") {
  211. data.props[prop].type.name = data.props[prop].type.value
  212. .map((val: { value: string }) => val.value)
  213. .join(" | ");
  214. }
  215. });
  216. const ordered = Object.keys(data.props)
  217. // .sort()
  218. .reduce((obj, key) => {
  219. obj[key] = data.props[key];
  220. return obj;
  221. }, {} as Props);
  222. data.props = ordered;
  223. return data;
  224. };
  225. const transposeDeclarations = (declarations: DeclarationType[]) => {
  226. const transposed: Record<string, DeclarationType> = {};
  227. declarations.forEach((declaration) => {
  228. transposed[declaration.displayName] = declaration;
  229. });
  230. return transposed;
  231. };
  232. const generateDeclarations = async (packagePaths: [string, string][]) => {
  233. const generated: Record<string, Record<string, DeclarationType>> = {};
  234. await Promise.all(
  235. packagePaths.map(async ([packageName, packagePath]) => {
  236. const parser = createParser(
  237. path.join(packagePath, "./tsconfig.json"),
  238. );
  239. const sourcePath = path.join(packagePath, sourceDir);
  240. if (!(await fs.pathExists(sourcePath))) {
  241. spinner.fail("Component path does not exist", sourcePath);
  242. process.exit(1);
  243. }
  244. const declarationPaths = await getPaths(
  245. sourcePath,
  246. excludedFilePatterns,
  247. );
  248. const parsed = parser
  249. .parse(declarationPaths)
  250. .map(prepareDeclaration);
  251. const transposed = transposeDeclarations(parsed);
  252. const outputName = getOutputName(packageName);
  253. generated[outputName] = transposed;
  254. spinner.stop();
  255. spinner.start(`- Generated declarations - ${packageName}`);
  256. return [packageName, packagePath];
  257. }),
  258. );
  259. return generated;
  260. };
  261. /** DOCGEN */
  262. const handleDocgen = async () => {
  263. const packagePathMap = await getPackageNamePathMap(packagesDir);
  264. const packagePathMapArray = Object.entries(packagePathMap);
  265. spinner.stop();
  266. spinner.start(`- Found ${packagePathMapArray.length} packages`);
  267. const res = await generateDeclarations(packagePathMapArray);
  268. spinner.succeed("Generated declarations");
  269. return res;
  270. };
  271. export default function plugin(): Plugin<DocgenContent> {
  272. return {
  273. name: "docusaurus-plugin-refine-docgen",
  274. getPathsToWatch: function () {
  275. return [packagesDir];
  276. },
  277. async loadContent() {
  278. if (!process.env.DISABLE_DOCGEN) {
  279. spinner.start();
  280. return await handleDocgen();
  281. }
  282. return {};
  283. },
  284. configureWebpack(config) {
  285. return {
  286. resolve: {
  287. alias: {
  288. "@docgen": path.join(
  289. config.resolve?.alias?.["@generated"],
  290. "docusaurus-plugin-refine-docgen",
  291. "default",
  292. ),
  293. },
  294. },
  295. };
  296. },
  297. async contentLoaded({ content, actions }): Promise<void> {
  298. if (!process.env.DISABLE_DOCGEN) {
  299. ora("Creating Refine declaration files...").succeed();
  300. const { createData } = actions;
  301. const data: Promise<string>[] = [];
  302. Object.entries(content).forEach(
  303. ([packageName, packageDeclarations]) => {
  304. Object.entries(packageDeclarations).forEach(
  305. ([componentName, declaration]) => {
  306. data.push(
  307. createData(
  308. `${packageName}/${componentName}.json`,
  309. JSON.stringify(declaration),
  310. ),
  311. );
  312. },
  313. );
  314. },
  315. );
  316. await Promise.all(data);
  317. }
  318. },
  319. };
  320. }