diff --git a/.eslintrc.js b/.eslintrc.js index 77ad026..66b700f 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -25,5 +25,9 @@ module.exports = { 'import/no-extraneous-dependencies': 'off', 'unicorn/prefer-ternary': 'off', 'unicorn/no-abusive-eslint-disable': 'off', + 'no-plusplus': 'off', + // this is definitely code smell but we have a lot of it in the python transforming script + 'no-param-reassign': 'off', + complexity: 'off', }, }; diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0b6a03d..2daa077 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,59 +1,61 @@ name: Release @latest env: - YARN_IGNORE_NODE: 1 - RETRY_TESTS: 1 + YARN_IGNORE_NODE: 1 + RETRY_TESTS: 1 on: - workflow_dispatch: + workflow_dispatch: jobs: - release: - name: "Release" - if: (!contains(github.event.head_commit.message, '[skip ci]') && !contains(github.event.head_commit.message, 'docs:')) - runs-on: ubuntu-latest - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - token: ${{ secrets.APIFY_SERVICE_ACCOUNT_GITHUB_TOKEN }} - fetch-depth: 0 - - - name: Use Node.js 20 - uses: actions/setup-node@v4 - with: - node-version: 20 - - - name: Enable corepack - run: | - corepack enable - corepack prepare yarn@stable --activate - - - name: Activate cache for Node.js 20 - uses: actions/setup-node@v4 - with: - cache: 'yarn' - - - name: Install Dependencies - run: yarn - - - name: Build - run: yarn build:deploy - - - name: Setup git user and npm - run: | - git config --global user.name "Apify Release Bot" - git config --global user.email "noreply@apify.com" - - echo "access=public" > ~/.npmrc - echo "//registry.npmjs.org/:_authToken=${{ secrets.APIFY_SERVICE_ACCOUNT_NPM_TOKEN }}" >> ~/.npmrc - - - name: Publish to npm - run: | - cd ./packages/plugin - npm publish --access public - env: - NPM_TOKEN: ${{ secrets.APIFY_SERVICE_ACCOUNT_NPM_TOKEN }} - GIT_USER: 'noreply@apify.com:${{ secrets.APIFY_SERVICE_ACCOUNT_GITHUB_TOKEN }}' - GH_TOKEN: ${{ secrets.APIFY_SERVICE_ACCOUNT_GITHUB_TOKEN }} \ No newline at end of file + release: + name: 'Release' + if: + (!contains(github.event.head_commit.message, '[skip ci]') && + !contains(github.event.head_commit.message, 'docs:')) + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + token: ${{ secrets.APIFY_SERVICE_ACCOUNT_GITHUB_TOKEN }} + fetch-depth: 0 + + - name: Use Node.js 20 + uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Enable corepack + run: | + corepack enable + corepack prepare yarn@stable --activate + + - name: Activate cache for Node.js 20 + uses: actions/setup-node@v4 + with: + cache: 'yarn' + + - name: Install Dependencies + run: yarn + + - name: Build + run: yarn build:deploy + + - name: Setup git user and npm + run: | + git config --global user.name "Apify Release Bot" + git config --global user.email "noreply@apify.com" + + echo "access=public" > ~/.npmrc + echo "//registry.npmjs.org/:_authToken=${{ secrets.APIFY_SERVICE_ACCOUNT_NPM_TOKEN }}" >> ~/.npmrc + + - name: Publish to npm + run: | + cd ./packages/plugin + npm publish --access public + env: + NPM_TOKEN: ${{ secrets.APIFY_SERVICE_ACCOUNT_NPM_TOKEN }} + GIT_USER: 'noreply@apify.com:${{ secrets.APIFY_SERVICE_ACCOUNT_GITHUB_TOKEN }}' + GH_TOKEN: ${{ secrets.APIFY_SERVICE_ACCOUNT_GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index ea9c6ce..cff90f6 100644 --- a/.gitignore +++ b/.gitignore @@ -37,4 +37,10 @@ tmp/ !.yarn/sdks !.yarn/versions .pnp.* -*.tgz \ No newline at end of file +*.tgz + +# Python tooling +pydoc-markdown-dump.json +__pycache__/ +typedoc-types.raw +typedoc-types-*.json \ No newline at end of file diff --git a/README.md b/README.md index 4ca02ca..0933568 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,8 @@ # Apify's fork of `docusaurus-plugin-typedoc-api` -This is a fork of [docusaurus-plugin-typedoc-api](https://github.com/milesj/docusaurus-plugin-typedoc-api) -adjusted for our usecases with rendering library documentation in [Apify Docs](https://docs.apify.com). +This is a fork of +[docusaurus-plugin-typedoc-api](https://github.com/milesj/docusaurus-plugin-typedoc-api) adjusted +for our usecases with rendering library documentation in [Apify Docs](https://docs.apify.com). ## Publishing diff --git a/packages/plugin/src/components/ApiItem.tsx b/packages/plugin/src/components/ApiItem.tsx index 2ca0366..8d5d357 100644 --- a/packages/plugin/src/components/ApiItem.tsx +++ b/packages/plugin/src/components/ApiItem.tsx @@ -55,19 +55,25 @@ export interface ApiItemProps extends Pick { export const ApiOptionsContext = createContext({ hideInherited: false, - setHideInherited: (hideInherited: boolean) => {} + setHideInherited: (hideInherited: boolean) => {}, }); export default function ApiItem({ readme: Readme, route }: ApiItemProps) { const [hideInherited, setHideInherited] = useState(false); - const apiOptions = useMemo(() => ({ - hideInherited, - setHideInherited, - }), [hideInherited, setHideInherited]); + const apiOptions = useMemo( + () => ({ + hideInherited, + setHideInherited, + }), + [hideInherited, setHideInherited], + ); const item = useRequiredReflection((route as unknown as { id: number }).id); const reflections = useReflectionMap(); - const toc = useMemo(() => extractTOC(item, reflections, hideInherited), [item, reflections, hideInherited]); + const toc = useMemo( + () => extractTOC(item, reflections, hideInherited), + [item, reflections, hideInherited], + ); // Pagination const prevItem = useReflection(item.previousId); @@ -117,7 +123,7 @@ export default function ApiItem({ readme: Readme, route }: ApiItemProps) { )} - + ); diff --git a/packages/plugin/src/components/ApiItemLayout.tsx b/packages/plugin/src/components/ApiItemLayout.tsx index 643ab99..d1c016f 100644 --- a/packages/plugin/src/components/ApiItemLayout.tsx +++ b/packages/plugin/src/components/ApiItemLayout.tsx @@ -14,7 +14,7 @@ import TOC from '@theme/TOC'; import TOCCollapsible from '@theme/TOCCollapsible'; import { useBreadcrumbs } from '../hooks/useBreadcrumbs'; import type { TOCItem } from '../types'; -import ApiOptionsLayout from './ApiOptionsLayout' +import ApiOptionsLayout from './ApiOptionsLayout'; import { VersionBanner } from './VersionBanner'; export interface ApiItemLayoutProps extends Pick { @@ -72,7 +72,9 @@ export default function ApiItemLayout({
{heading} - {module && {`${module}.${name}`}} + {module && ( + {`${module}.${name}`} + )}
{children} diff --git a/packages/plugin/src/components/ApiOptionsLayout.tsx b/packages/plugin/src/components/ApiOptionsLayout.tsx index ccf9ce3..f9fee21 100644 --- a/packages/plugin/src/components/ApiOptionsLayout.tsx +++ b/packages/plugin/src/components/ApiOptionsLayout.tsx @@ -2,21 +2,23 @@ import { useCallback, useContext } from 'react'; import { ApiOptionsContext } from './ApiItem'; export default function ApiOptionsLayout({ className }: { className: string }) { - const { hideInherited, setHideInherited } = useContext(ApiOptionsContext); - const handleHideInherited = useCallback(() => { - setHideInherited(!hideInherited); - }, [hideInherited, setHideInherited]); + const { hideInherited, setHideInherited } = useContext(ApiOptionsContext); + const handleHideInherited = useCallback(() => { + setHideInherited(!hideInherited); + }, [hideInherited, setHideInherited]); - return ( - <> -
-
Page Options
- -
-
- - ); + return ( + <> +
+
+ Page Options +
+ +
+
+ + ); } diff --git a/packages/plugin/src/components/ApiPage.tsx b/packages/plugin/src/components/ApiPage.tsx index 3b4f1b8..fbeac3c 100644 --- a/packages/plugin/src/components/ApiPage.tsx +++ b/packages/plugin/src/components/ApiPage.tsx @@ -1,5 +1,3 @@ -/* eslint-disable no-param-reassign */ - import '@vscode/codicons/dist/codicon.css'; import './styles.css'; import { useMemo } from 'react'; diff --git a/packages/plugin/src/components/DefaultValue.tsx b/packages/plugin/src/components/DefaultValue.tsx index 4c09738..cb4ab7a 100644 --- a/packages/plugin/src/components/DefaultValue.tsx +++ b/packages/plugin/src/components/DefaultValue.tsx @@ -34,7 +34,6 @@ export function DefaultValue({ comment, value, type }: DefaultValueProps) { defaultTag = decode(marked(defaultTag)); } - if (!defaultTag && !value) { return null; } diff --git a/packages/plugin/src/components/Flags.tsx b/packages/plugin/src/components/Flags.tsx index 2c4ffcc..5073565 100644 --- a/packages/plugin/src/components/Flags.tsx +++ b/packages/plugin/src/components/Flags.tsx @@ -16,7 +16,7 @@ export function Flags({ flags }: FlagsProps) { } return ( - + {Object.keys(flags) .map(removePrefix) .map((flag) => ( diff --git a/packages/plugin/src/components/Markdown.tsx b/packages/plugin/src/components/Markdown.tsx index e669138..9cf9ae2 100644 --- a/packages/plugin/src/components/Markdown.tsx +++ b/packages/plugin/src/components/Markdown.tsx @@ -111,7 +111,6 @@ function convertAstToElements(ast: TokensList): React.ReactNode[] | undefined { const elements: React.ReactNode[] = []; let counter = 0; - // eslint-disable-next-line complexity ast.forEach((token) => { // Nested tokens aren't typed for some reason... const children = (token as unknown as { tokens: TokensList }).tokens ?? []; diff --git a/packages/plugin/src/components/Member.tsx b/packages/plugin/src/components/Member.tsx index d0721db..4f16243 100644 --- a/packages/plugin/src/components/Member.tsx +++ b/packages/plugin/src/components/Member.tsx @@ -46,24 +46,26 @@ export function Member({ id }: MemberProps) { } return ( - !shouldHideInherited &&
-

- - - - {escapeMdx(reflection.name)} - {isCommentWithModifiers(comment) && } -

+ !shouldHideInherited && ( +
+

+ + + + {escapeMdx(reflection.name)} + {isCommentWithModifiers(comment) && } +

- {content} + {content} - {reflection.groups?.map((group) => ( - - {group.children?.map((child) => - hasOwnDocument(child, reflections) ? null : , - )} - - ))} -
+ {reflection.groups?.map((group) => ( + + {group.children?.map((child) => + hasOwnDocument(child, reflections) ? null : , + )} + + ))} +
+ ) ); } diff --git a/packages/plugin/src/components/MemberGetterSetter.tsx b/packages/plugin/src/components/MemberGetterSetter.tsx index 73b6add..900b75a 100644 --- a/packages/plugin/src/components/MemberGetterSetter.tsx +++ b/packages/plugin/src/components/MemberGetterSetter.tsx @@ -13,7 +13,6 @@ export interface MemberGetterSetterProps { setter?: TSDDeclarationReflection['setSignature']; } -// eslint-disable-next-line complexity export function MemberGetterSetter({ inPanel, getter, setter }: MemberGetterSetterProps) { const minimal = useMinimalLayout(); diff --git a/packages/plugin/src/components/MemberSignatureBody.tsx b/packages/plugin/src/components/MemberSignatureBody.tsx index 863e2a1..0707d7f 100644 --- a/packages/plugin/src/components/MemberSignatureBody.tsx +++ b/packages/plugin/src/components/MemberSignatureBody.tsx @@ -1,6 +1,6 @@ // https://github.com/TypeStrong/typedoc-default-themes/blob/master/src/default/partials/member.signature.body.hbs -import { Fragment, useContext } from 'react' +import { Fragment, useContext } from 'react'; import type { JSONOutput, Models } from 'typedoc'; import { type GlobalData } from '@docusaurus/types'; import { usePluginData } from '@docusaurus/useGlobalData'; @@ -57,7 +57,6 @@ function intoReturnComment(comment?: JSONOutput.Comment): JSONOutput.Comment | u const HIDE_TAGS = ['@returns', '@param']; -// eslint-disable-next-line complexity export function MemberSignatureBody({ hideSources, sig }: MemberSignatureBodyProps) { const minimal = useMinimalLayout(); const showTypes = sig.typeParameter && sig.typeParameter.length > 0; @@ -67,39 +66,37 @@ export function MemberSignatureBody({ hideSources, sig }: MemberSignatureBodyPro const { reflections } = useContext(ApiDataContext); const { isPython } = usePluginData('docusaurus-plugin-typedoc-api') as GlobalData; - if (isPython) { - // eslint-disable-next-line sig.parameters = sig.parameters?.reduce((acc, param) => { // @ts-expect-error Silence ts errors switch (param.type?.name) { case 'Unpack': // @ts-expect-error Silence ts errors - // eslint-disable-next-line - acc.push(...reflections[param.type.typeArguments[0].target].children.map(x => ({...x, flags: {'keyword-only': true}}))); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + acc.push(...reflections[param.type.typeArguments[0].target].children.map((x) => ({...x, flags: { 'keyword-only': true } })), + ); break; default: acc.push(param); break; } - + return acc; }, []); - - // eslint-disable-next-line + sig.parameters = sig.parameters?.reduce((acc, param) => { // @ts-expect-error Silence ts errors switch (param.type?.name) { case 'NotRequired': // @ts-expect-error Silence ts errors // eslint-disable-next-line - acc.push({...param, type: param.type.typeArguments[0]}); + acc.push({ ...param, type: param.type.typeArguments[0] }); break; default: acc.push(param); break; } - + return acc; }, []); } @@ -152,7 +149,9 @@ export function MemberSignatureBody({ hideSources, sig }: MemberSignatureBodyPro
  • - {reflectionChild.flags?.isRest && ...} + {reflectionChild.flags?.isRest && ( + ... + )} {`${reflectionChild.name}: `}
    - +
  • ))} )} - {param.type?.type === 'union' && ( - ((param.type.types.filter( - (unionType) => unionType.type === 'reflection')) as unknown as Models.ReflectionType[]).map( - (unionReflectionType) => ( -
      - {unionReflectionType.declaration?.children?.map((unionChild) => ( -
    • -
      - - {unionChild.flags?.isRest && ...} - {`${unionChild.name}: `} - - -
      - - -
    • - ))} -
    - )) - )} + {param.type?.type === 'union' && + ( + param.type.types.filter( + (unionType) => unionType.type === 'reflection', + ) as unknown as Models.ReflectionType[] + ).map((unionReflectionType) => ( +
      + {unionReflectionType.declaration?.children?.map((unionChild) => ( +
    • +
      + + {unionChild.flags?.isRest && ( + ... + )} + {`${unionChild.name}: `} + + +
      + + +
    • + ))} +
    + ))} ))} diff --git a/packages/plugin/src/components/MemberSignatureTitle.tsx b/packages/plugin/src/components/MemberSignatureTitle.tsx index 0dd78d5..fb111b2 100644 --- a/packages/plugin/src/components/MemberSignatureTitle.tsx +++ b/packages/plugin/src/components/MemberSignatureTitle.tsx @@ -3,7 +3,7 @@ import { Fragment } from 'react'; import { usePluginData } from '@docusaurus/useGlobalData'; -import type { GlobalData,TSDSignatureReflection } from '../types'; +import type { GlobalData, TSDSignatureReflection } from '../types'; import { escapeMdx } from '../utils/helpers'; import { Type } from './Type'; import { TypeParametersGeneric } from './TypeParametersGeneric'; @@ -18,13 +18,15 @@ export function MemberSignatureTitle({ useArrow, hideName, sig }: MemberSignatur const { isPython } = usePluginData('docusaurus-plugin-typedoc-api') as GlobalData; // add `*` before the first keyword-only parameter const parametersCopy = sig.parameters?.slice() ?? []; - const firstKeywordOnlyIndex = parametersCopy.findIndex((param) => Object.keys(param.flags).includes('keyword-only')); + const firstKeywordOnlyIndex = parametersCopy.findIndex((param) => + Object.keys(param.flags).includes('keyword-only'), + ); if (firstKeywordOnlyIndex >= 0) { parametersCopy.splice(firstKeywordOnlyIndex, 0, { id: 999_999, name: '*', kind: 32_768, - flags: { }, + flags: {}, variant: 'param', }); } @@ -32,7 +34,10 @@ export function MemberSignatureTitle({ useArrow, hideName, sig }: MemberSignatur return ( <> {!hideName && sig.name !== '__type' ? ( - {sig.modifiers ? `${sig.modifiers.join(' ')} ` : ''}{escapeMdx(sig.name)} + + {sig.modifiers ? `${sig.modifiers.join(' ')} ` : ''} + {escapeMdx(sig.name)} + ) : // Constructor signature sig.kind === 16_384 ? ( <> @@ -52,17 +57,15 @@ export function MemberSignatureTitle({ useArrow, hideName, sig }: MemberSignatur {param.flags?.isRest && ...} {escapeMdx(param.name)} - { - !isPython && ( - <> - - {(param.flags?.isOptional || 'defaultValue' in param) && '?'} - {': '} - - - - ) - } + {!isPython && ( + <> + + {(param.flags?.isOptional || 'defaultValue' in param) && '?'} + {': '} + + + + )} ))} diff --git a/packages/plugin/src/components/Reflection.tsx b/packages/plugin/src/components/Reflection.tsx index d3d1f51..4150696 100644 --- a/packages/plugin/src/components/Reflection.tsx +++ b/packages/plugin/src/components/Reflection.tsx @@ -17,7 +17,7 @@ import { TypeParameters } from './TypeParameters'; export interface ReflectionProps { reflection: TSDDeclarationReflection | TSDReflection | TSDSignatureReflection; } -// eslint-disable-next-line complexity + export function Reflection({ reflection }: ReflectionProps) { const hierarchy = useMemo(() => createHierarchy(reflection), [reflection]); diff --git a/packages/plugin/src/components/Type.tsx b/packages/plugin/src/components/Type.tsx index dc88821..2cbf3dd 100644 --- a/packages/plugin/src/components/Type.tsx +++ b/packages/plugin/src/components/Type.tsx @@ -39,7 +39,6 @@ export interface TypeProps { type?: { type: string; value?: unknown }; } -// eslint-disable-next-line complexity export function Type({ needsParens = false, type: base }: TypeProps) { const reflections = useReflectionMap(); const { isPython } = usePluginData('docusaurus-plugin-typedoc-api') as GlobalData; @@ -238,22 +237,14 @@ export function Type({ needsParens = false, type: base }: TypeProps) { )} {type.typeArguments && type.typeArguments.length > 0 && ( <> - - { - isPython ? '[' : '<' - } - + {isPython ? '[' : '<'} {type.typeArguments.map((t, i) => ( {i > 0 && , } ))} - - { - isPython ? ']' : '>' - } - + {isPython ? ']' : '>'} )} diff --git a/packages/plugin/src/components/VersionBanner.tsx b/packages/plugin/src/components/VersionBanner.tsx index 809e1e5..2b2e1c2 100644 --- a/packages/plugin/src/components/VersionBanner.tsx +++ b/packages/plugin/src/components/VersionBanner.tsx @@ -1,6 +1,10 @@ import { useCallback } from 'react'; import Link from '@docusaurus/Link'; -import { useDocsPreferredVersion, useDocsVersion, useDocVersionSuggestions } from '@docusaurus/plugin-content-docs/client'; +import { + useDocsPreferredVersion, + useDocsVersion, + useDocVersionSuggestions, +} from '@docusaurus/plugin-content-docs/client'; import { ThemeClassNames } from '@docusaurus/theme-common'; export function VersionBanner(): JSX.Element | null { diff --git a/packages/plugin/src/index.ts b/packages/plugin/src/index.ts index d509201..c2afbbd 100644 --- a/packages/plugin/src/index.ts +++ b/packages/plugin/src/index.ts @@ -13,6 +13,7 @@ import { generateJson, loadPackageJsonAndDocs, } from './plugin/data'; +import { processPythonDocs } from './plugin/python'; import { extractSidebar } from './plugin/sidebar'; import { getVersionedDocsDirPath, readVersionsMetadata } from './plugin/version'; import type { @@ -57,6 +58,7 @@ const DEFAULT_OPTIONS: Required = { rehypePlugins: [], versions: {}, python: false, + pythonOptions: {}, }; async function importFile(file: string): Promise { @@ -167,9 +169,8 @@ export default function typedocApiPlugin( options.changelogName, ); - // eslint-disable-next-line no-param-reassign cfg.packageName = packageJson.name; - // eslint-disable-next-line no-param-reassign + cfg.packageVersion = packageJson.version; }); @@ -196,10 +197,23 @@ export default function typedocApiPlugin( const outFile = path.join(context.generatedFilesDir, `api-typedoc-${pluginId}.json`); if (options.pathToCurrentVersionTypedocJSON) { - if (!fs.existsSync(context.generatedFilesDir)){ + if (!fs.existsSync(context.generatedFilesDir)) { fs.mkdirSync(context.generatedFilesDir, { recursive: true }); } fs.copyFileSync(options.pathToCurrentVersionTypedocJSON, outFile); + } else if (Object.keys(options.pythonOptions).length > 0) { + if ( + !options.pythonOptions.pythonModulePath || + !options.pythonOptions.moduleShortcutsPath + ) { + throw new Error('Python options are missing required fields'); + } + + processPythonDocs({ + pythonModulePath: options.pythonOptions.pythonModulePath, + moduleShortcutsPath: options.pythonOptions.moduleShortcutsPath, + outPath: outFile, + }); } else { await generateJson(projectRoot, entryPoints, outFile, options); } @@ -266,7 +280,7 @@ export default function typedocApiPlugin( } actions.setGlobalData({ - isPython: !!options.python, + isPython: !!(options.python || options.pythonOptions), } as GlobalData); const docs: PropVersionDocs = {}; @@ -330,7 +344,10 @@ export default function typedocApiPlugin( return { path: info.permalink, exact: true, - component: path.join(__dirname, `./components/ApiItem.${process.env.TYPEDOC_PLUGIN_DEV ? 'tsx' : 'js'}`), + component: path.join( + __dirname, + `./components/ApiItem.${process.env.TYPEDOC_PLUGIN_DEV ? 'tsx' : 'js'}`, + ), modules, sidebar: 'api', // Map the ID here instead of creating a JSON data file, @@ -399,7 +416,10 @@ export default function typedocApiPlugin( { path: indexPermalink, exact: false, - component: path.join(__dirname, `./components/ApiPage.${process.env.TYPEDOC_PLUGIN_DEV ? 'tsx' : 'js'}`), + component: path.join( + __dirname, + `./components/ApiPage.${process.env.TYPEDOC_PLUGIN_DEV ? 'tsx' : 'js'}`, + ), routes, modules: { options: optionsData, @@ -463,7 +483,10 @@ export default function typedocApiPlugin( remarkPlugins: options.remarkPlugins, rehypePlugins: options.rehypePlugins, siteDir: context.siteDir, - staticDirs: [...context.siteConfig.staticDirectories, path.join(context.siteDir, 'static')], + staticDirs: [ + ...context.siteConfig.staticDirectories, + path.join(context.siteDir, 'static'), + ], // Since this isn't a doc/blog page, we can get // away with it being a partial! isMDXPartial: () => true, diff --git a/packages/plugin/src/plugin/data.ts b/packages/plugin/src/plugin/data.ts index 8519449..8959ca8 100644 --- a/packages/plugin/src/plugin/data.ts +++ b/packages/plugin/src/plugin/data.ts @@ -1,7 +1,7 @@ import fs from 'fs'; import path from 'path'; import * as TypeDoc from 'typedoc'; -import { type InlineTagDisplayPart, type JSONOutput, ReflectionKind } from 'typedoc' +import { type InlineTagDisplayPart, type JSONOutput, ReflectionKind } from 'typedoc'; import ts from 'typescript'; import { normalizeUrl } from '@docusaurus/utils'; import type { @@ -110,7 +110,6 @@ export function createReflectionMap( ): TSDDeclarationReflectionMap { const map: TSDDeclarationReflectionMap = {}; - // eslint-disable-next-line complexity items.forEach((item) => { // Add @reference categories to reflection. const referenceCategories: Record = {}; @@ -133,7 +132,6 @@ export function createReflectionMap( // Update categories with reference categories. if (!item.categories) { - // eslint-disable-next-line no-param-reassign item.categories = []; } for (const category of Object.values(referenceCategories)) { @@ -170,7 +168,7 @@ export function loadPackageJsonAndDocs( } } - if(!found) { + if (!found) { // TODO: load the actual package information from pyproject.toml or similar return { packageJson: { @@ -179,7 +177,7 @@ export function loadPackageJsonAndDocs( }, readmePath: '', changelogPath: '', - } + }; } const readmePath = path.join(currentDir, readmeFileName); @@ -203,7 +201,6 @@ export function addMetadataToReflections( const permalink = `/${joinUrl(urlPrefix, packageSlug)}`; if (project.children) { - // eslint-disable-next-line no-param-reassign project.children = project.children.map((child) => { migrateToVersion0230(child); @@ -213,7 +210,6 @@ export function addMetadataToReflections( // We need to go another level deeper and only use fragments if (child.kind === ReflectionKind.Namespace && child.children) { - // eslint-disable-next-line no-param-reassign child.children = child.children.map((grandChild) => ({ ...grandChild, permalink: normalizeUrl([`${childPermalink}#${grandChild.name}`]), @@ -251,7 +247,7 @@ function mergeReflections(base: TSDDeclarationReflection, next: TSDDeclarationRe }); // We can remove refs since were merging all reflections into one - // eslint-disable-next-line no-param-reassign + base.groups = base.groups.filter((group) => group.title !== 'References'); } } @@ -390,14 +386,13 @@ function buildSourceFileNameMap( const map: Record = {}; const cwd = process.cwd(); - if(project.symbolIdMap) { + if (project.symbolIdMap) { Object.values(project.symbolIdMap).forEach((symbol) => { // absolute map[path.normalize(path.join(cwd, symbol.sourceFileName))] = true; }); } - modChildren.forEach((child) => { child.sources?.forEach((sf) => { // relative @@ -458,9 +453,8 @@ export function flattenAndGroupPackages( changelogPath, }; - // eslint-disable-next-line no-param-reassign cfg.packageName = packages[cfg.packagePath].packageName; - // eslint-disable-next-line no-param-reassign + cfg.packageVersion = packages[cfg.packagePath].packageVersion; } diff --git a/packages/plugin/src/plugin/python/consts.ts b/packages/plugin/src/plugin/python/consts.ts new file mode 100644 index 0000000..2d59316 --- /dev/null +++ b/packages/plugin/src/plugin/python/consts.ts @@ -0,0 +1,50 @@ +export const REPO_ROOT_PLACEHOLDER = 'REPO_ROOT_PLACEHOLDER'; + +export const APIFY_CLIENT_REPO_URL = 'https://github.com/apify/apify-client-python'; +export const APIFY_SDK_REPO_URL = 'https://github.com/apify/apify-sdk-python'; +export const APIFY_SHARED_REPO_URL = 'https://github.com/apify/apify-shared-python'; +export const CRAWLEE_PYTHON_REPO_URL = 'https://github.com/apify/crawlee-python'; + +export const REPO_URL_PER_PACKAGE = { + apify: APIFY_SDK_REPO_URL, + apify_client: APIFY_CLIENT_REPO_URL, + apify_shared: APIFY_SHARED_REPO_URL, + crawlee: CRAWLEE_PYTHON_REPO_URL, +}; + +// Taken from https://github.com/TypeStrong/typedoc/blob/v0.23.24/src/lib/models/reflections/kind.ts, modified +export const TYPEDOC_KINDS = { + class: { + kind: 128, + kindString: 'Class', + }, + data: { + kind: 1024, + kindString: 'Property', + }, + enum: { + kind: 8, + kindString: 'Enumeration', + }, + enumValue: { + kind: 16, + kindString: 'Enumeration Member', + }, + function: { + kind: 2048, + kindString: 'Method', + }, +}; + +export const GROUP_ORDER = [ + 'Classes', + 'Abstract classes', + 'Data structures', + 'Errors', + 'Functions', + 'Constructors', + 'Methods', + 'Properties', + 'Constants', + 'Enumeration Members', +]; diff --git a/packages/plugin/src/plugin/python/docspec-gen/__init__.py b/packages/plugin/src/plugin/python/docspec-gen/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/packages/plugin/src/plugin/python/docspec-gen/generate_ast.py b/packages/plugin/src/plugin/python/docspec-gen/generate_ast.py new file mode 100755 index 0000000..f1e06a3 --- /dev/null +++ b/packages/plugin/src/plugin/python/docspec-gen/generate_ast.py @@ -0,0 +1,73 @@ +""" +Replaces the default pydoc-markdown shell script with a custom Python script calling the pydoc-markdown API directly. + +This script generates an AST from the Python source code in the `src` directory and prints it as a JSON object. +""" + +from pydoc_markdown.interfaces import Context +from pydoc_markdown.contrib.loaders.python import PythonLoader +from pydoc_markdown.contrib.processors.filter import FilterProcessor +from pydoc_markdown.contrib.processors.crossref import CrossrefProcessor +from google_docstring_processor import ApifyGoogleProcessor +from docspec import dump_module + +import argparse + +import json +import os + +def search_for_git_root(path): + if os.path.exists(os.path.join(path, '.git')): + return path + else: + parent = os.path.dirname(path) + if parent == path: + return None + return search_for_git_root(parent) + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("-i", "--input", help = "Path to the Python module to generate the AST from.", required=True) + parser.add_argument("output", help = "Path to store the generated AST as a JSON file in.") + + args = parser.parse_args() + project_path = os.path.abspath(args.input) + + repo_root_path = search_for_git_root(project_path) + if not repo_root_path: + raise Exception("Could not find git root directory. Are you sure the Python module is in a git repository?") + + context = Context(directory='.') + loader = PythonLoader(search_path=[project_path]) + filter = FilterProcessor( + documented_only=False, + skip_empty_modules=False, + ) + crossref = CrossrefProcessor() + google = ApifyGoogleProcessor() + + loader.init(context) + filter.init(context) + google.init(context) + crossref.init(context) + + processors = [filter, google, crossref] + + dump = [] + + modules = list(loader.load()) + + for processor in processors: + processor.process(modules, None) + + for module in modules: + dump.append(dump_module(module)) + + with open(args.output, 'w') as f: + f.write(json.dumps(dump, indent=4).replace( + repo_root_path, + 'REPO_ROOT_PLACEHOLDER' + )) + +if __name__ == "__main__": + main() diff --git a/packages/plugin/src/plugin/python/docspec-gen/google_docstring_processor.py b/packages/plugin/src/plugin/python/docspec-gen/google_docstring_processor.py new file mode 100644 index 0000000..154462c --- /dev/null +++ b/packages/plugin/src/plugin/python/docspec-gen/google_docstring_processor.py @@ -0,0 +1,185 @@ +# -*- coding: utf8 -*- +# Copyright (c) 2019 Niklas Rosenstein +# !!! Modified 2024 Jindřich Bär +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to +# deal in the Software without restriction, including without limitation the +# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +# sell copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +# IN THE SOFTWARE. + +import dataclasses +import re +import typing as t + +import docspec + +from pydoc_markdown.contrib.processors.sphinx import generate_sections_markdown +from pydoc_markdown.interfaces import Processor, Resolver + +import json + + +@dataclasses.dataclass +class ApifyGoogleProcessor(Processor): + """ + This class implements the preprocessor for Google and PEP 257 docstrings. It converts + docstrings formatted in the Google docstyle to Markdown syntax. + + References: + + * https://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_google.html + * https://www.python.org/dev/peps/pep-0257/ + + Example: + + ``` + Attributes: + module_level_variable1 (int): Module level variables may be documented in + either the ``Attributes`` section of the module docstring, or in an + inline docstring immediately following the variable. + + Either form is acceptable, but the two should not be mixed. Choose + one convention to document module level variables and be consistent + with it. + + Todo: + * For module TODOs + * You have to also use ``sphinx.ext.todo`` extension + ``` + + Renders as: + + Attributes: + module_level_variable1 (int): Module level variables may be documented in + either the ``Attributes`` section of the module docstring, or in an + inline docstring immediately following the variable. + + Either form is acceptable, but the two should not be mixed. Choose + one convention to document module level variables and be consistent + with it. + + Todo: + * For module TODOs + * You have to also use ``sphinx.ext.todo`` extension + + @doc:fmt:google + """ + + _param_res = [ + re.compile(r"^(?P\S+):\s+(?P.+)$"), + re.compile(r"^(?P\S+)\s+\((?P[^)]+)\):\s+(?P.+)$"), + re.compile(r"^(?P\S+)\s+--\s+(?P.+)$"), + re.compile(r"^(?P\S+)\s+\{\[(?P\S+)\]\}\s+--\s+(?P.+)$"), + re.compile(r"^(?P\S+)\s+\{(?P\S+)\}\s+--\s+(?P.+)$"), + ] + + _keywords_map = { + "Args:": "Arguments", + "Arguments:": "Arguments", + "Attributes:": "Attributes", + "Example:": "Example", + "Examples:": "Examples", + "Keyword Args:": "Arguments", + "Keyword Arguments:": "Arguments", + "Methods:": "Methods", + "Note:": "Notes", + "Notes:": "Notes", + "Other Parameters:": "Arguments", + "Parameters:": "Arguments", + "Return:": "Returns", + "Returns:": "Returns", + "Raises:": "Raises", + "References:": "References", + "See Also:": "See Also", + "Todo:": "Todo", + "Warning:": "Warnings", + "Warnings:": "Warnings", + "Warns:": "Warns", + "Yield:": "Yields", + "Yields:": "Yields", + } + + def check_docstring_format(self, docstring: str) -> bool: + for section_name in self._keywords_map: + if section_name in docstring: + return True + return False + + def process(self, modules: t.List[docspec.Module], resolver: t.Optional[Resolver]) -> None: + docspec.visit(modules, self._process) + + def _process(self, node: docspec.ApiObject): + if not node.docstring: + return + + lines = [] + sections = [] + current_lines: t.List[str] = [] + in_codeblock = False + keyword = None + multiline_argument_offset = -1 + + def _commit(): + if keyword: + sections.append({keyword: list(current_lines)}) + else: + lines.extend(current_lines) + current_lines.clear() + + for line in node.docstring.content.split("\n"): + multiline_argument_offset += 1 + if line.lstrip().startswith("```"): + in_codeblock = not in_codeblock + current_lines.append(line) + if not in_codeblock: + _commit() + continue + + if in_codeblock: + current_lines.append(line) + continue + + line = line.strip() + if line in self._keywords_map: + _commit() + keyword = self._keywords_map[line] + continue + + if keyword is None: + lines.append(line) + continue + + for param_re in self._param_res: + param_match = param_re.match(line) + if param_match: + current_lines.append(param_match.groupdict()) + multiline_argument_offset = 0 + break + + if not param_match: + if multiline_argument_offset == 1: + current_lines[-1]["desc"] += "\n" + line + multiline_argument_offset = 0 + else: + current_lines.append(line) + + _commit() + node.docstring.content = json.dumps({ + "text": "\n".join(lines), + "sections": sections, + }, indent=None) + + diff --git a/packages/plugin/src/plugin/python/index.ts b/packages/plugin/src/plugin/python/index.ts new file mode 100644 index 0000000..415c997 --- /dev/null +++ b/packages/plugin/src/plugin/python/index.ts @@ -0,0 +1,47 @@ +import childProcess from 'child_process'; +import fs from 'fs'; +import path from 'path'; +import { DocspecTransformer } from './transformation'; +import type { DocspecObject } from './types'; + +export { groupSort } from './utils'; + +/** + * Processes the Python documentation generated by `pydoc-markdown` and transforms it into a format + * accepted by the TypeDoc JSON generator. + */ +export function processPythonDocs({ + pythonModulePath, + moduleShortcutsPath, + outPath, +}: { + pythonModulePath: string; + moduleShortcutsPath: string; + outPath: string; +}) { + const pydocMarkdownDumpPath = path.join(__dirname, './pydoc-markdown-dump.json'); + + childProcess.spawnSync('python', [ + path.join(__dirname, './docspec-gen/generate_ast.py'), + '-i', + pythonModulePath, + pydocMarkdownDumpPath, + ]); + + const moduleShortcuts = JSON.parse(fs.readFileSync(moduleShortcutsPath, 'utf8')) as Record< + string, + string + >; + + const pydocMarkdownDump = JSON.parse( + fs.readFileSync(pydocMarkdownDumpPath, 'utf8'), + ) as DocspecObject[]; + + const docspecTransformer = new DocspecTransformer({ + moduleShortcuts, + }); + + const typedocApiReference = docspecTransformer.transform(pydocMarkdownDump); + + fs.writeFileSync(outPath, JSON.stringify(typedocApiReference, null, 4)); +} diff --git a/packages/plugin/src/plugin/python/inheritance.ts b/packages/plugin/src/plugin/python/inheritance.ts new file mode 100644 index 0000000..737f372 --- /dev/null +++ b/packages/plugin/src/plugin/python/inheritance.ts @@ -0,0 +1,80 @@ +import type { TypeDocObject } from './types'; +import { getGroupName, getOID } from './utils'; + +/** + * Given an ancestor and a descendant objects, injects the children of the ancestor into the descendant. + * + * Sets the `extendedTypes` / `extendedBy` properties. + * @param ancestor + * @param descendant + */ +export function resolveInheritedSymbols(ancestor: TypeDocObject, descendant: TypeDocObject) { + descendant.children ??= []; + + descendant.extendedTypes = [ + ...(descendant.extendedTypes ?? []), + { + name: ancestor.name, + target: ancestor.id, + type: 'reference', + }, + ]; + + ancestor.extendedBy = [ + ...(ancestor.extendedBy ?? []), + { + name: descendant.name, + target: descendant.id, + type: 'reference', + }, + ]; + + for (const inheritedChild of ancestor.children ?? []) { + const ownChild = descendant.children?.find((x) => x.name === inheritedChild.name); + + if (!ownChild) { + const childId = getOID(); + + const { groupName } = getGroupName(inheritedChild); + if (!groupName) { + throw new Error( + `Couldn't resolve the group name for ${inheritedChild.name} (inherited child of ${ancestor.name})`, + ); + } + + const group = descendant.groups?.find((g) => g.title === groupName); + + if (group) { + group.children.push(inheritedChild.id); + } else { + descendant.groups?.push({ + children: [inheritedChild.id], + title: groupName, + }); + } + + descendant.children.push({ + ...inheritedChild, + id: childId, + inheritedFrom: { + name: `${ancestor.name}.${inheritedChild.name}`, + target: inheritedChild.id, + type: 'reference', + }, + }); + } else if (!ownChild.comment?.summary?.[0]?.text) { + ownChild.inheritedFrom = { + name: `${ancestor.name}.${inheritedChild.name}`, + target: inheritedChild.id, + type: 'reference', + }; + + for (const key of Object.keys(inheritedChild)) { + if (key !== 'id' && key !== 'inheritedFrom') { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + ownChild[key as keyof typeof ownChild] = inheritedChild[key as keyof typeof inheritedChild]; + } + } + } + } +} diff --git a/packages/plugin/src/plugin/python/packageVersions.ts b/packages/plugin/src/plugin/python/packageVersions.ts new file mode 100644 index 0000000..6fb6aed --- /dev/null +++ b/packages/plugin/src/plugin/python/packageVersions.ts @@ -0,0 +1,43 @@ +import childProcess from 'child_process'; +import fs from 'fs'; +import path from 'path'; + +/** + * Looks for the installed versions of the given packages and returns them as a dictionary. + */ +export function getPackageGitHubTags(packageNames: string[]): Record { + // For each package, get the installed version, and set the tag to the corresponding version + const packageTags: Record = {}; + + for (const pkg of packageNames) { + const spawnResult = childProcess.spawnSync('python', [ + '-c', + `import ${pkg}; print(${pkg}.__version__)`, + ]); + if (spawnResult.status === 0) { + packageTags[pkg] = `v${spawnResult.stdout.toString().trim()}`; + } + } + + return packageTags; +} + +export function findNearestInParent(currentPath: string, filename: string) { + let parentPath = currentPath; + while (parentPath !== '/') { + parentPath = path.dirname(parentPath); + if (fs.existsSync(path.join(parentPath, filename))) { + return path.join(parentPath, filename); + } + } + + throw new Error(`No ${filename} found in any parent directory`); +} + +export function getCurrentPackageName(pyprojectTomlPath?: string) { + const currentPath = path.dirname(__dirname); + pyprojectTomlPath ??= findNearestInParent(currentPath, 'pyproject.toml'); + const pyprojectToml = fs.readFileSync(pyprojectTomlPath, 'utf8'); + + return pyprojectToml.match(/^name = "(.+)"$/m)?.[1]; +} diff --git a/packages/plugin/src/plugin/python/transformation.ts b/packages/plugin/src/plugin/python/transformation.ts new file mode 100644 index 0000000..e17d9df --- /dev/null +++ b/packages/plugin/src/plugin/python/transformation.ts @@ -0,0 +1,444 @@ +import { REPO_ROOT_PLACEHOLDER, TYPEDOC_KINDS } from './consts'; +import { resolveInheritedSymbols } from './inheritance'; +import { PythonTypeResolver } from './type-parsing'; +import type { + DocspecDocstring, + DocspecObject, + TypeDocDocstring, + TypeDocObject, + TypeDocType, +} from './types'; +import { getGroupName, getOID, groupSort, isHidden, projectUsesDocsGroupDecorator } from './utils'; + +interface TransformObjectOptions { + /** + * The current docspec (`pydoc-markdown`) object to transform. + */ + currentDocspecNode: DocspecObject; + /** + * The already (partially) transformed parent Typedoc object. + */ + parentTypeDoc: TypeDocObject; + /** + * The full name of the module the current object is in. + */ + moduleName: string; +} + +interface DocspecTransformerOptions { + /** + * A map of module shortcuts, where the key is the full name of the module, and the value is the shortened name. + */ + moduleShortcuts?: Record; +} + +export class DocspecTransformer { + private pythonTypeResolver: PythonTypeResolver; + + private symbolIdMap: Record = {}; + + private namesToIds: Record = {}; + + private moduleShortcuts: Record; + + /** + * Maps the name of the class to the list of Typedoc objects representing the classes that extend it. + * + * This is used for resolving the references to the base classes - in case the base class is encountered after the class that extends it. + */ + private forwardAncestorRefs = new Map(); + + /** + * Maps the name of the class to the reference to the Typedoc object representing the class. + * + * This is used to resolve the references to the base classes of a class using the name. + */ + private backwardAncestorRefs = new Map(); + + /** + * Stack of the docstrings of the current context. + * + * Used to read the class Google-style docstrings from the class' properties and methods. + */ + private contextStack: TypeDocDocstring[] = []; + + private settings: { useDocsGroup: boolean } = { useDocsGroup: false }; + + constructor({ moduleShortcuts }: DocspecTransformerOptions) { + this.pythonTypeResolver = new PythonTypeResolver(); + this.moduleShortcuts = moduleShortcuts ?? {}; + } + + transform(docspecModules: DocspecObject[]): TypeDocObject { + // Root object of the Typedoc structure, accumulator for the recursive walk + const typedocApiReference: TypeDocObject = { + children: [], + flags: {}, + groups: [], + id: 0, + kind: 1, + kindString: 'Project', + name: 'apify-client', + sources: [ + { + character: 0, + fileName: 'src/index.ts', + line: 1, + }, + ], + symbolIdMap: this.symbolIdMap, + }; + + this.settings.useDocsGroup = projectUsesDocsGroupDecorator( + docspecModules as unknown as { name: string }, + ); + + // Convert all the modules, store them in the root object + for (const module of docspecModules) { + this.walkAndTransform({ + currentDocspecNode: module, + moduleName: module.name, + parentTypeDoc: typedocApiReference, + }); + } + + this.pythonTypeResolver.resolveTypes(); + + this.namesToIds = Object.entries(this.symbolIdMap).reduce>( + (acc, [id, { qualifiedName }]) => { + acc[qualifiedName] = Number(id); + return acc; + }, + {}, + ); + + this.fixRefs(typedocApiReference); + this.sortChildren(typedocApiReference); + + return typedocApiReference; + } + + private getContext() { + return this.contextStack[this.contextStack.length - 1]; + } + + private popContext() { + this.contextStack.pop(); + } + + private newContext(context: TypeDocDocstring) { + this.contextStack.push(context); + } + + /** + * Recursively traverse the Typedoc structure and fix the references to the named entities. + * + * Searches for the {@link TypeDocType} structure with the `type` property set to `reference`, and replaces the `target` property + * with the corresponding ID of the named entity. + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private fixRefs(obj: Record) { + for (const key of Object.keys(obj)) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + if (key === 'name' && obj?.type === 'reference' && this.namesToIds[obj?.name]) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + obj.target = this.namesToIds[obj?.name]; + } + if (typeof obj[key] === 'object' && obj[key] !== null) { + this.fixRefs(obj[key] as TypeDocObject); + } + } + } + + /** + * Given a docspec object outputted by `pydoc-markdown`, transforms this object into the Typedoc structure, + * and appends it as a child of the `parentTypeDoc` Typedoc object (which serves as an accumulator for the recursion). + * @param obj + * @param parent + * @param module + */ + private walkAndTransform({ + currentDocspecNode, + parentTypeDoc, + moduleName, + }: TransformObjectOptions) { + if (isHidden(currentDocspecNode)) { + for (const docspecMember of currentDocspecNode.members ?? []) { + this.walkAndTransform({ + currentDocspecNode: docspecMember, + moduleName, + // Skips the hidden member, i.e. its children will be appended to the parent of the hidden member + parentTypeDoc, + }); + } + + return; + } + + const { typedocType, typedocKind } = this.getTypedocType(currentDocspecNode, parentTypeDoc); + const { filePathInRepo } = this.getGitHubUrls(currentDocspecNode); + + const docstring = this.parseDocstring(currentDocspecNode); + const currentId = getOID(); + + this.symbolIdMap[currentId] = { + qualifiedName: currentDocspecNode.name, + sourceFileName: filePathInRepo, + }; + + // Get the module name of the member, and check if it has a shortcut (reexport from an ancestor module) + const fullName = `${moduleName}.${currentDocspecNode.name}`; + if (fullName in this.moduleShortcuts) { + moduleName = this.moduleShortcuts[fullName].replace(`.${currentDocspecNode.name}`, ''); + } + + currentDocspecNode.name = + currentDocspecNode.decorations?.find((d) => d.name === 'docs_name')?.args.slice(2, -2) ?? + currentDocspecNode.name; + + // Create the Typedoc member object + const currentTypedocNode: TypeDocObject = { + ...typedocKind, + children: [], + comment: docstring + ? { + summary: [ + { + kind: 'text', + text: docstring.text, + }, + ], + } + : undefined, + decorations: currentDocspecNode.decorations?.map(({ name, args }) => ({ args, name })), + flags: {}, + groups: [], + id: currentId, + module: moduleName, // This is an extension to the original Typedoc structure, to support showing where the member is exported from + name: currentDocspecNode.name, + sources: [ + { + character: 1, + fileName: filePathInRepo, + line: currentDocspecNode.location.lineno, + }, + ], + type: typedocType, + }; + + if (currentTypedocNode.kindString === 'Method') { + currentTypedocNode.signatures = [ + { + comment: docstring.text + ? { + blockTags: docstring?.returns + ? [{ content: [{ kind: 'text', text: docstring.returns }], tag: '@returns' }] + : undefined, + summary: [ + { + kind: 'text', + text: docstring?.text, + }, + ], + } + : undefined, + flags: {}, + id: getOID(), + kind: 4096, + kindString: 'Call signature', + modifiers: currentDocspecNode.modifiers ?? [], + name: currentDocspecNode.name, + parameters: currentDocspecNode.args + ?.filter((arg) => arg.name !== 'self' && arg.name !== 'cls') + .map((arg) => ({ + comment: docstring.args?.[arg.name] + ? { + summary: [ + { + kind: 'text', + text: docstring.args[arg.name], + }, + ], + } + : undefined, + defaultValue: arg.default_value as string, + flags: { + isOptional: arg.datatype?.includes('Optional'), + 'keyword-only': arg.type === 'KEYWORD_ONLY', + }, + id: getOID(), + kind: 32_768, + kindString: 'Parameter', + name: arg.name, + type: this.pythonTypeResolver.registerType(arg.datatype), + })), + type: this.pythonTypeResolver.registerType(currentDocspecNode.return_type), + }, + ]; + } + + if (currentTypedocNode.kindString === 'Class') { + this.newContext(docstring); + + this.backwardAncestorRefs.set(currentDocspecNode.name, currentTypedocNode); + + if (currentDocspecNode.bases && currentDocspecNode.bases.length > 0) { + for (const base of currentDocspecNode.bases) { + const canonicalAncestorType = this.pythonTypeResolver.getBaseType(base); + + const baseTypedocMember = this.backwardAncestorRefs.get(canonicalAncestorType); + if (baseTypedocMember) { + resolveInheritedSymbols(baseTypedocMember, currentTypedocNode); + } else { + this.forwardAncestorRefs.set(canonicalAncestorType, [ + ...(this.forwardAncestorRefs.get(canonicalAncestorType) ?? []), + currentTypedocNode, + ]); + } + } + } + } + + for (const docspecMember of currentDocspecNode.members ?? []) { + this.walkAndTransform({ + currentDocspecNode: docspecMember, + moduleName, + parentTypeDoc: currentTypedocNode, + }); + } + + if (currentTypedocNode.kindString === 'Class') { + this.popContext(); + } + + const { groupName, source: groupSource } = getGroupName(currentTypedocNode); + + if ( + groupName && // If the group comes from a decorator, use it always; otherwise check if the symbol isn't top-level + (!this.settings.useDocsGroup || + groupSource === 'decorator' || + parentTypeDoc.kindString !== 'Project') + ) { + const group = parentTypeDoc.groups?.find((g) => g.title === groupName); + if (group) { + group.children.push(currentTypedocNode.id); + } else { + parentTypeDoc.groups?.push({ + children: [currentTypedocNode.id], + title: groupName, + }); + } + } + + parentTypeDoc.children?.push(currentTypedocNode); + + this.sortChildren(currentTypedocNode); + + if (currentTypedocNode.kindString === 'Class') { + for (const descendant of this.forwardAncestorRefs.get(currentTypedocNode.name) ?? []) { + resolveInheritedSymbols(currentTypedocNode, descendant); + + this.sortChildren(descendant); + } + } + } + + // Get the URL of the member in GitHub + private getGitHubUrls(docspecMember: DocspecObject): { filePathInRepo: string } { + const filePathInRepo = docspecMember.location.filename.replace(REPO_ROOT_PLACEHOLDER, ''); + + return { filePathInRepo }; + } + + /** + * Sorts the `groups` of `typedocMember` using {@link groupSort} and sorts the children of each group alphabetically. + */ + private sortChildren(typedocMember: TypeDocObject) { + if (!typedocMember.groups) return; + + for (const group of typedocMember.groups) { + group.children.sort((a, b) => { + const firstName = + typedocMember.children?.find((x) => x.id === a || x.inheritedFrom?.target === a)?.name ?? + 'a'; + const secondName = + typedocMember.children?.find((x) => x.id === b || x.inheritedFrom?.target === b)?.name ?? + 'b'; + return firstName.localeCompare(secondName); + }); + } + typedocMember.groups?.sort((a, b) => groupSort(a.title, b.title)); + } + + /** + * If possible, parses the `.docstring` property of the passed object. If the docstring is a stringified JSON object, + * it extracts the `args` and `returns` sections and adds them to the returned object. + * + * TODO + * This structure is created in the `google` docstring format, which is a JSON object with the following structure: + */ + private parseDocstring(docspecMember: DocspecObject): TypeDocDocstring { + const docstring: TypeDocDocstring = { text: docspecMember.docstring?.content ?? '' }; + + try { + const parsedDocstring = JSON.parse(docstring.text) as DocspecDocstring; + + docstring.text = parsedDocstring.text; + const parsedArguments = (parsedDocstring.sections?.find( + (section) => Object.keys(section)[0] === 'Arguments', + )?.Arguments ?? []) as DocspecDocstring['args']; + + docstring.args = + parsedArguments?.reduce>((acc, arg) => { + acc[arg.param] = arg.desc; + return acc; + }, {}) ?? {}; + + const returnTypes = + docstring.sections?.find((section) => Object.keys(section)[0] === 'Returns')?.Returns ?? []; + + docstring.returns = returnTypes.join('\n'); + } catch { + // Do nothing + } + + if (!docstring.text) { + docstring.text = this.getContext()?.args?.[docspecMember.name] ?? ''; + } + + return docstring; + } + + /** + * Given the current Docspec object and the parent Typedoc object, returns the Typedoc type and kind of the current object. + */ + private getTypedocType( + docspecMember: DocspecObject, + parentTypeDoc: TypeDocObject, + ): { typedocType: TypeDocType; typedocKind: (typeof TYPEDOC_KINDS)[keyof typeof TYPEDOC_KINDS] } { + let typedocKind = TYPEDOC_KINDS[docspecMember.type]; + + if (docspecMember.bases?.includes('Enum')) { + typedocKind = TYPEDOC_KINDS.enum; + } + + let typedocType = this.pythonTypeResolver.registerType(docspecMember.datatype); + + if (docspecMember.decorations?.some((d) => ['property', 'dualproperty'].includes(d.name))) { + typedocKind = TYPEDOC_KINDS.data; + typedocType = this.pythonTypeResolver.registerType( + docspecMember.return_type ?? docspecMember.datatype, + ); + } + + if (parentTypeDoc.kindString === 'Enumeration') { + typedocKind = TYPEDOC_KINDS.enumValue; + typedocType = { + type: 'literal', + value: docspecMember.value as string, + }; + } + + return { typedocKind, typedocType }; + } +} diff --git a/packages/plugin/src/plugin/python/type-parsing/index.ts b/packages/plugin/src/plugin/python/type-parsing/index.ts new file mode 100644 index 0000000..81bae24 --- /dev/null +++ b/packages/plugin/src/plugin/python/type-parsing/index.ts @@ -0,0 +1,88 @@ +import childProcess from 'child_process'; +import fs from 'fs'; +import path from 'path'; +import type { DocspecType, TypeDocType } from '../types'; + +const RAW_TYPES_JSON_FILEPATH = path.join(__dirname, 'typedoc-types.raw'); +const PARSED_TYPES_JSON_FILEPATH = path.join(__dirname, 'typedoc-types-parsed.json'); + +const PYTHON_SCRIPT_FILEPATH = path.join(__dirname, 'parse_types.py'); + +/** + * Keeps track of Typedoc type objects. When `resolveTypes` is called, it tries to parse + * the Python types using Python's `ast` module. + * + * The parsed types are then applied to the original registered Typedoc type objects. + */ +export class PythonTypeResolver { + private typedocTypes: TypeDocType[] = []; + + /** + * Register a new Python type to be resolved. + * + * Given a string representation of the type, returns a Typedoc type object. + */ + registerType(docspecType?: DocspecType): TypeDocType { + const newType: TypeDocType = { + name: docspecType?.replaceAll(/#.*/g, '').replaceAll('\n', '').trim() ?? 'Undefined', + type: 'reference', + }; + + this.typedocTypes.push(newType); + return newType; + } + + /** + * Parse the registered Python types using Python's ast module. + * For the actual Python implementation, see `parse_types.py`. + * + * Modifies the objects registered with `registerType` in-place. + * + * @param typedocTypes The "opaque" Python types to parse. + * @returns {void} Nothing. The registered types are mutated in-place. + */ + resolveTypes() { + fs.writeFileSync( + RAW_TYPES_JSON_FILEPATH, + JSON.stringify( + this.typedocTypes + .map((x) => { + if (x.type === 'reference') { + return x.name; + } + + return null; + }) + .filter(Boolean), + ), + ); + + childProcess.spawnSync('python', [PYTHON_SCRIPT_FILEPATH, RAW_TYPES_JSON_FILEPATH]); + + const parsedTypes = JSON.parse(fs.readFileSync(PARSED_TYPES_JSON_FILEPATH, 'utf8')) as Record< + string, + TypeDocType + >; + + for (const originalType of this.typedocTypes) { + if (originalType.type === 'reference') { + const parsedType = parsedTypes[originalType.name]; + + if (parsedType) { + for (const key of Object.keys(parsedType)) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + originalType[key] = parsedType[key as keyof TypeDocType]; + } + } + } + } + } + + /** + * Strips the Optional[] type from the type string, + * and replaces generic types with just the main type. + */ + getBaseType(type: string): string { + return type?.replace(/Optional\[(.*)]/g, '$1').split('[')[0]; + } +} diff --git a/packages/plugin/src/plugin/python/type-parsing/parse_types.py b/packages/plugin/src/plugin/python/type-parsing/parse_types.py new file mode 100644 index 0000000..e7e547a --- /dev/null +++ b/packages/plugin/src/plugin/python/type-parsing/parse_types.py @@ -0,0 +1,82 @@ +""" +Given a JSON file containing a list of expressions, this script will parse each expression and output a JSON file containing an object with the parsed expressions. +Called from transformDocs.js. + +Accepts one CLI argument: the path to the JSON file containing the expressions to parse. +""" + +import ast +import json +import sys +import os + +base_scalar_types = { + "str", + "int", + "float", + "bool", + "bytearray", + "timedelta", + "None", +} + + +def parse_expression(ast_node, full_expression): + """ + Turns the AST expression object into a typedoc-compliant dict + """ + + current_node_type = ast_node.__class__.__name__ + + if current_node_type == "BinOp" and ast_node.op.__class__.__name__ == "BitOr": + return { + "type": "union", + "types": [ + parse_expression(ast_node.left, full_expression), + parse_expression(ast_node.right, full_expression), + ], + } + + if current_node_type == "Tuple": + return [parse_expression(e, full_expression) for e in ast_node.elts] + + if current_node_type == "Subscript": + if "id" in ast_node.value._fields and ast_node.value.id == "Annotated": + return parse_expression(ast_node.slice.dims[0], full_expression) + + main_type = parse_expression(ast_node.value, full_expression) + type_argument = parse_expression(ast_node.slice, full_expression) + + main_type["typeArguments"] = ( + type_argument if isinstance(type_argument, list) else [type_argument] + ) + return main_type + + if current_node_type == "Constant": + return {"type": "literal", "value": ast_node.value} + + # If the expression is not one of the types above, we simply print the expression + return { + "type": "reference", + "name": full_expression[ast_node.col_offset : ast_node.end_col_offset], + } + + +typedoc_types_path = sys.argv[1] + +with open(typedoc_types_path, "r") as f: + typedoc_out = {} + expressions = json.load(f) + + for expression in expressions: + try: + if typedoc_out.get(expression) is None: + typedoc_out[expression] = parse_expression( + ast.parse(expression).body[0].value, expression + ) + except Exception as e: + print(f"Invalid expression encountered while parsing: {expression}") + print(f"Error: {e}") + + with open(f"{os.path.splitext(typedoc_types_path)[0]}-parsed.json", "w") as f: + f.write(json.dumps(typedoc_out, indent=4)) diff --git a/packages/plugin/src/plugin/python/types.ts b/packages/plugin/src/plugin/python/types.ts new file mode 100644 index 0000000..3b1b68b --- /dev/null +++ b/packages/plugin/src/plugin/python/types.ts @@ -0,0 +1,83 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { TYPEDOC_KINDS } from './consts'; + +export type OID = number; + +export interface TypeDocObject { + [key: string]: any; + id: OID; + name: string; + kind: number; + kindString: string; + decorations?: { name: string; args: string }[]; + children?: TypeDocObject[]; + groups?: { title: string; children: OID[] }[]; + flags: Record; + module?: string; + inheritedFrom?: { + type: string; + target: OID; + name: string; + }; + comment?: { + summary: { text: string; kind: 'text' }[]; + blockTags?: { tag: string; content: any[] }[]; + }; + signatures?: TypeDocObject[]; + sources?: { + fileName: string; + line: number; + character: number; + }[]; + type?: TypeDocType; + symbolIdMap?: Record; + extendedTypes?: TypeDocType[]; + extendedBy?: TypeDocType[]; + modifiers?: any[]; + parameters?: TypeDocObject[]; +} + +export interface DocspecObject { + members: DocspecObject[]; + name: string; + type: keyof typeof TYPEDOC_KINDS; + location: { + filename: string; + lineno: number; + }; + decorations?: { name: string; args: string }[]; + bases?: DocspecType[]; + datatype?: DocspecType; + return_type?: DocspecType; + value?: any; + docstring?: { content: string }; + modifiers?: DocspecType[]; + args?: { name: string; type: DocspecType; default_value: any; datatype: DocspecType }[]; +} + +export interface TypeDocDocstring { + text: string; + returns?: string; + args?: Record; + sections?: Record[]; +} + +export interface DocspecDocstring { + text: string; + returns?: string; + args?: { param: string; desc: string }[]; + sections?: Record[]; +} + +export type TypeDocType = + { + [key: string]: any; + type: 'reference'; + name: string; + target?: number; + } | { + type: 'literal'; + value: any; + }; + +export type DocspecType = string; diff --git a/packages/plugin/src/plugin/python/utils.ts b/packages/plugin/src/plugin/python/utils.ts new file mode 100644 index 0000000..d10d0e6 --- /dev/null +++ b/packages/plugin/src/plugin/python/utils.ts @@ -0,0 +1,123 @@ +/* eslint-disable sort-keys */ +import { GROUP_ORDER, TYPEDOC_KINDS } from './consts'; +import type { DocspecObject, OID, TypeDocObject } from './types'; + +function* generateOID() { + let id = 1; + while (true) { + yield id++; + } +} + +const oidGenerator = generateOID(); + +/** + * Returns automatically incrementing OID. Every call to this function will return a new unique OID. + * @returns {number} The OID. + */ +export function getOID(): OID { + return oidGenerator.next().value as OID; +} + +/** + * Given a TypeDoc object, returns the name of the group this object belongs to. + * @param object The TypeDoc object. + * @returns The group name and the source of the group name (either 'decorator' or 'predicate'). + */ +export function getGroupName(object: TypeDocObject): { + groupName: string | undefined; + source: 'decorator' | 'predicate'; +} { + if (object.decorations?.some((d) => d.name === 'docs_group')) { + const parsedGroupName = object.decorations + .find((d) => d.name === 'docs_group') + ?.args.slice(2, -2); + + if (parsedGroupName) { + return { + groupName: parsedGroupName, + source: 'decorator', + }; + } + } + + const groupPredicates: Record boolean> = { + 'Scrapy integration': (x) => + [ + 'ApifyScheduler', + 'ActorDatasetPushPipeline', + 'ApifyHttpProxyMiddleware', + 'apply_apify_settings', + ].includes(x.name), + 'Data structures': (x) => + Boolean(['BaseModel', 'TypedDict'].some((base) => + (x?.bases as { includes: (x: string) => boolean })?.includes(base), + ) || x?.decorations?.some((d) => d.name === 'dataclass')), + Errors: (x) => x.name.toLowerCase().includes('error'), + Classes: (x) => x.kindString === 'Class', + 'Main Clients': (x) => ['ApifyClient', 'ApifyClientAsync'].includes(x.name), + 'Async Resource Clients': (x) => x.name.toLowerCase().includes('async'), + 'Resource Clients': (x) => x.kindString === 'Class' && x.name.toLowerCase().includes('client'), + Methods: (x) => x.kindString === 'Method', + Constructors: (x) => x.kindString === 'Constructor', + Properties: (x) => x.kindString === 'Property', + Constants: (x) => x.kindString === 'Enumeration', + 'Enumeration members': (x) => x.kindString === 'Enumeration Member', + }; + + const groupName = Object.entries(groupPredicates).find(([_, predicate]) => + predicate(object), + )?.[0]; + + return { groupName, source: 'predicate' }; +} + +/** + * Recursively search arbitrary JS object for property `name: 'docs_group'`. + * @param object + */ +export function projectUsesDocsGroupDecorator(object: { name: string }): boolean { + if (object instanceof Object) { + if (object.name === 'docs_group') { + return true; + } + + for (const key in object) { + if (projectUsesDocsGroupDecorator(object[key as keyof typeof object] as unknown as { name: string })) { + return true; + } + } + } + + return false; +} + +/** + * Returns true if the given member should be hidden from the documentation. + * + * A member should be hidden if: + * - It has a `ignore_docs` decoration. + * + * @param member The member to check. + */ +export function isHidden(member: DocspecObject): boolean { + return ( + !(member.type in TYPEDOC_KINDS) || + member.decorations?.some((d) => d.name === 'ignore_docs') || + member.name === 'ignore_docs' + ); +} + +/** + * Comparator for enforcing the documentation groups order (examples of groups in {@link GROUP_ORDER}). + * + * The groups are sorted by the order in which they appear in {@link GROUP_ORDER}. + * + * This is compatible with the `Array.prototype.sort` method. + */ +export function groupSort(g1: string, g2: string) { + if (GROUP_ORDER.includes(g1) && GROUP_ORDER.includes(g2)) { + return GROUP_ORDER.indexOf(g1) - GROUP_ORDER.indexOf(g2); + } + return g1.localeCompare(g2); +} diff --git a/packages/plugin/src/plugin/structure/0.23.ts b/packages/plugin/src/plugin/structure/0.23.ts index 73f272e..69ee268 100644 --- a/packages/plugin/src/plugin/structure/0.23.ts +++ b/packages/plugin/src/plugin/structure/0.23.ts @@ -1,5 +1,3 @@ -/* eslint-disable no-param-reassign */ - import { JSONOutput } from 'typedoc'; interface OldComment { diff --git a/packages/plugin/src/plugin/version.ts b/packages/plugin/src/plugin/version.ts index 205794f..da90cd3 100644 --- a/packages/plugin/src/plugin/version.ts +++ b/packages/plugin/src/plugin/version.ts @@ -46,9 +46,9 @@ function createVersionMetadata({ const isLast = versionName === lastVersionName; const versionOptions = options.versions[versionName] ?? {}; const versionLabel = - versionOptions.label ?? versionName === CURRENT_VERSION_NAME ? 'Next' : versionName; + (versionOptions.label ?? versionName === CURRENT_VERSION_NAME) ? 'Next' : versionName; let versionPathPart = - versionOptions.path ?? versionName === CURRENT_VERSION_NAME ? 'next' : versionName; + (versionOptions.path ?? versionName === CURRENT_VERSION_NAME) ? 'next' : versionName; if (isLast) { versionPathPart = ''; diff --git a/packages/plugin/src/types.ts b/packages/plugin/src/types.ts index 0c3edbf..8f9d1fb 100644 --- a/packages/plugin/src/types.ts +++ b/packages/plugin/src/types.ts @@ -21,6 +21,9 @@ export interface DocusaurusPluginTypeDocApiOptions minimal?: boolean; packageJsonName?: string; packages: (PackageConfig | string)[]; + /** + * @deprecated Use `pythonOptions` and the bundled transformation script instead. + */ pathToCurrentVersionTypedocJSON?: string; projectRoot: string; readmeName?: string; @@ -33,8 +36,13 @@ export interface DocusaurusPluginTypeDocApiOptions /** * Enables the Python-specific rendering patches. + * If `pythonOptions` is specified, this is automatically set to `true`. */ python: boolean; + pythonOptions: { + moduleShortcutsPath?: string; + pythonModulePath?: string; + }; remarkPlugins: MDXPlugin[]; rehypePlugins: MDXPlugin[]; diff --git a/packages/plugin/src/utils/icons.ts b/packages/plugin/src/utils/icons.ts index 48b5f4b..15cc8a0 100644 --- a/packages/plugin/src/utils/icons.ts +++ b/packages/plugin/src/utils/icons.ts @@ -28,7 +28,7 @@ const KIND_ICONS: Record = { 1_048_576: 'symbol-field', // SetSignature 2_097_152: 'symbol-parameter', // TypeAlias 4_194_304: 'references', // Reference - 8_388_608: 'references' // a Non-TS document (new in TypeDoc `0.26.0`, unused by `docusaurus-plugin-typedoc-api`) + 8_388_608: 'references', // a Non-TS document (new in TypeDoc `0.26.0`, unused by `docusaurus-plugin-typedoc-api`) }; export function getKindIcon(kind: ReflectionKind, name: string): string { @@ -42,7 +42,6 @@ export function getKindIcon(kind: ReflectionKind, name: string): string { return icon; } -// eslint-disable-next-line complexity export function getKindIconColor(kind: ReflectionKind): string { switch (kind) { // Function diff --git a/packages/plugin/tsconfig.json b/packages/plugin/tsconfig.json index 79a90f8..b8d1a2d 100644 --- a/packages/plugin/tsconfig.json +++ b/packages/plugin/tsconfig.json @@ -5,6 +5,8 @@ "types/**/*" ], "compilerOptions": { - "lib": ["es2021"] + "lib": [ + "es2021" + ] } } diff --git a/playground/js/README.md b/playground/js/README.md index 4a21194..14f718c 100644 --- a/playground/js/README.md +++ b/playground/js/README.md @@ -1,3 +1,5 @@ -# Sample JavaScript project +# Sample JavaScript project -This directory contains a sample JavaScript project that demonstrates how to use the `@apify/docusaurus-plugin-typedoc-api` plugin to generate API documentation for a JavaScript project. \ No newline at end of file +This directory contains a sample JavaScript project that demonstrates how to use the +`@apify/docusaurus-plugin-typedoc-api` plugin to generate API documentation for a JavaScript +project. diff --git a/playground/js/src/bar.ts b/playground/js/src/bar.ts index b0b8274..a511c6a 100644 --- a/playground/js/src/bar.ts +++ b/playground/js/src/bar.ts @@ -1,45 +1,45 @@ export interface BarOptions { - /** - * The name of the `Bar` instance. - * - * @default 'Karl' - */ - name: string; - /** - * The age of the `Bar` instance. - * - * @default 0 - */ - age: number; - /** - * Favourite numbers of the `Bar` instance. - * - * @default [1, 2, Infinity] - */ - numbers: number[]; - /** - * Favourite words of the `Bar` instance. - * - * @default ['foo', 'bar', 'jabberwocky'] - */ - strings: string[]; + /** + * The name of the `Bar` instance. + * + * @default 'Karl' + */ + name: string; + /** + * The age of the `Bar` instance. + * + * @default 0 + */ + age: number; + /** + * Favourite numbers of the `Bar` instance. + * + * @default [1, 2, Infinity] + */ + numbers: number[]; + /** + * Favourite words of the `Bar` instance. + * + * @default ['foo', 'bar', 'jabberwocky'] + */ + strings: string[]; } /** * This is a simple class called `Bar` */ export class Bar { - private constructor(options: BarOptions) { - // do nothing - } + private constructor(options: BarOptions) { + // do nothing + } - /** - * Create a new instance of the `Bar` class. - * - * @param {BarOptions} options - * @returns {Bar} - */ - static create(options: BarOptions = {} as BarOptions): Bar { - return new Bar(options); - } -} \ No newline at end of file + /** + * Create a new instance of the `Bar` class. + * + * @param {BarOptions} options + * @returns {Bar} + */ + static create(options: BarOptions = {} as BarOptions): Bar { + return new Bar(options); + } +} diff --git a/playground/js/src/foo.ts b/playground/js/src/foo.ts index d2a54e3..85200bd 100644 --- a/playground/js/src/foo.ts +++ b/playground/js/src/foo.ts @@ -2,40 +2,40 @@ * This is a simple class called Foo */ export class Foo { - private name: string; - /** - * Constructor of the `Foo` class. - */ - constructor() { - this.name = 'Foo'; - } + private name: string; + /** + * Constructor of the `Foo` class. + */ + constructor() { + this.name = 'Foo'; + } - /** - * Get the name of the class. - * @returns {string} - */ - getName(): string { - return this.name; - } + /** + * Get the name of the class. + * @returns {string} + */ + getName(): string { + return this.name; + } - /** - * Set the name of the class. - * @param {string} name - */ - setName(name: string | Foo): void { - this.name = typeof name === 'string' ? name : name.getName(); - } + /** + * Set the name of the class. + * @param {string} name + */ + setName(name: string | Foo): void { + this.name = typeof name === 'string' ? name : name.getName(); + } - /** - * A static method of the class. - * - * @returns {string} - * @static - * @memberof Foo - * @example - * Foo.staticMethod(); - */ - static staticMethod(): string { - return 'static method'; - } -} \ No newline at end of file + /** + * A static method of the class. + * + * @returns {string} + * @static + * @memberof Foo + * @example + * Foo.staticMethod(); + */ + static staticMethod(): string { + return 'static method'; + } +} diff --git a/playground/js/src/index.ts b/playground/js/src/index.ts index ce5fe6d..4af197d 100644 --- a/playground/js/src/index.ts +++ b/playground/js/src/index.ts @@ -1,4 +1,4 @@ import { Foo } from './foo'; import { Bar, BarOptions } from './bar'; -export { Foo, Bar, BarOptions }; \ No newline at end of file +export { Foo, Bar, BarOptions }; diff --git a/playground/python/README.md b/playground/python/README.md index 54861da..fdda8d3 100644 --- a/playground/python/README.md +++ b/playground/python/README.md @@ -1,3 +1,4 @@ -# Sample Python project +# Sample Python project -This directory contains a sample Python project that demonstrates how to use the `@apify/docusaurus-plugin-typedoc-api` plugin to generate API documentation for a Python project. \ No newline at end of file +This directory contains a sample Python project that demonstrates how to use the +`@apify/docusaurus-plugin-typedoc-api` plugin to generate API documentation for a Python project. diff --git a/playground/python/module_shortcuts.json b/playground/python/module_shortcuts.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/playground/python/module_shortcuts.json @@ -0,0 +1 @@ +{} diff --git a/playground/python/src/__init__.py b/playground/python/src/__init__.py index 10578d2..e3fa48d 100644 --- a/playground/python/src/__init__.py +++ b/playground/python/src/__init__.py @@ -1,6 +1,6 @@ from foo import Foo - +@docs_group('Classes') class Bar: """ The bar class is a simple @@ -15,7 +15,7 @@ def __init__(self): """ print("Bar") - def foo(self): + def foo(self) -> Foo: """ The foo method of the bar class, prints "foo". """ diff --git a/playground/python/src/foo.py b/playground/python/src/foo.py index 3423019..46830c7 100644 --- a/playground/python/src/foo.py +++ b/playground/python/src/foo.py @@ -1,3 +1,4 @@ +@docs_group('Classes') class Foo: """ The foo class is a simple class that prints "Foo" when it is initialized and "bar" when the bar method is called. diff --git a/playground/tsconfig.json b/playground/tsconfig.json index d0bc17e..f28faf3 100644 --- a/playground/tsconfig.json +++ b/playground/tsconfig.json @@ -1,3 +1,5 @@ { - "include": ["js/src/**/*.ts"], + "include": [ + "js/src/**/*.ts" + ] } diff --git a/playground/website/README.md b/playground/website/README.md index 0c6c2c2..d94d05c 100644 --- a/playground/website/README.md +++ b/playground/website/README.md @@ -14,7 +14,8 @@ $ yarn $ yarn start ``` -This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server. +This command starts a local development server and opens up a browser window. Most changes are +reflected live without having to restart the server. ### Build @@ -22,7 +23,8 @@ This command starts a local development server and opens up a browser window. Mo $ yarn build ``` -This command generates static content into the `build` directory and can be served using any static contents hosting service. +This command generates static content into the `build` directory and can be served using any static +contents hosting service. ### Deployment @@ -38,4 +40,5 @@ Not using SSH: $ GIT_USER= yarn deploy ``` -If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch. +If you are using GitHub pages for hosting, this command is a convenient way to build the website and +push to the `gh-pages` branch. diff --git a/playground/website/babel.config.js b/playground/website/babel.config.js index e00595d..6b5e0c8 100644 --- a/playground/website/babel.config.js +++ b/playground/website/babel.config.js @@ -1,3 +1,3 @@ module.exports = { - presets: [require.resolve('@docusaurus/core/lib/babel/preset')], + presets: [require.resolve('@docusaurus/core/lib/babel/preset')], }; diff --git a/playground/website/docs/intro.md b/playground/website/docs/intro.md index 6dcd8bf..5652c2e 100644 --- a/playground/website/docs/intro.md +++ b/playground/website/docs/intro.md @@ -1,3 +1,4 @@ # Filler page -The Docusaurus classic theme doesn't seem to work without the `docs` plugin - or at least we didn't figure it out. This is a filler page to make the sidebar work. \ No newline at end of file +The Docusaurus classic theme doesn't seem to work without the `docs` plugin - or at least we didn't +figure it out. This is a filler page to make the sidebar work. diff --git a/playground/website/docusaurus.config.ts b/playground/website/docusaurus.config.ts index 577ae67..437ff32 100644 --- a/playground/website/docusaurus.config.ts +++ b/playground/website/docusaurus.config.ts @@ -1,82 +1,85 @@ -import {themes as prismThemes} from 'prism-react-renderer'; -import type {Config} from '@docusaurus/types'; +import { themes as prismThemes } from 'prism-react-renderer'; +import type { Config } from '@docusaurus/types'; import type * as Preset from '@docusaurus/preset-classic'; import typedocApiPlugin from '../../packages/plugin/src/index'; const config: Config = { - title: '@apify/docusaurus-plugin-typedoc-api', + title: '@apify/docusaurus-plugin-typedoc-api', - // Set the production url of your site here - url: 'https://nonexistent.apify.com', - // Set the // pathname under which your site is served - // For GitHub pages deployment, it is often '//' - baseUrl: '/', + // Set the production url of your site here + url: 'https://nonexistent.apify.com', + // Set the // pathname under which your site is served + // For GitHub pages deployment, it is often '//' + baseUrl: '/', - // GitHub pages deployment config. - // If you aren't using GitHub pages, you don't need these. - organizationName: 'apify', // Usually your GitHub org/user name. - projectName: '@apify/docusaurus-plugin-typedoc-api', // Usually your repo name. + // GitHub pages deployment config. + // If you aren't using GitHub pages, you don't need these. + organizationName: 'apify', // Usually your GitHub org/user name. + projectName: 'docusaurus-plugin-typedoc-api', // Usually your repo name. + githubHost: 'github.com', - onBrokenLinks: 'throw', - onBrokenMarkdownLinks: 'warn', + onBrokenLinks: 'throw', + onBrokenMarkdownLinks: 'warn', - // Even if you don't use internationalization, you can use this field to set - // useful metadata like html lang. For example, if your site is Chinese, you - // may want to replace "en" with "zh-Hans". - i18n: { - defaultLocale: 'en', - locales: ['en'], - }, + // Even if you don't use internationalization, you can use this field to set + // useful metadata like html lang. For example, if your site is Chinese, you + // may want to replace "en" with "zh-Hans". + i18n: { + defaultLocale: 'en', + locales: ['en'], + }, - presets: [ - [ - '@docusaurus/preset-classic', - { - docs: { - sidebarPath: './sidebars.ts', - }, - blog: false, - theme: { - customCss: './src/css/custom.css', - }, - } satisfies Preset.Options, - ], - ], + presets: [ + [ + '@docusaurus/preset-classic', + { + docs: { + sidebarPath: './sidebars.ts', + }, + blog: false, + theme: { + customCss: './src/css/custom.css', + }, + } satisfies Preset.Options, + ], + ], - plugins: [ - (context, options) => typedocApiPlugin( - context, - { - ...options as any, - projectRoot: __dirname + '/../', - packages: ['/js'], - }, - ), - ], + plugins: [ + (context, options) => + typedocApiPlugin(context, { + ...(options as any), + projectRoot: '.', + packages: [{ path: '.' }], + pythonOptions: { + moduleShortcutsPath: __dirname + '/../python/module_shortcuts.json', + pythonModulePath: __dirname + '/../python/src', + }, + }), + ], - themeConfig: { - // Replace with your project's social card - navbar: { - title: '@apify/docusaurus-plugin-typedoc-api', - items: [ - { - type: 'docSidebar', - sidebarId: 'tutorialSidebar', - position: 'left', - label: 'Docs', - }, - { - to: '/api', - label: 'API', - position: 'left', - }, - ], - }, - prism: { - theme: prismThemes.github, - darkTheme: prismThemes.dracula, - }, - } satisfies Preset.ThemeConfig, + themeConfig: { + // Replace with your project's social card + navbar: { + title: '@apify/docusaurus-plugin-typedoc-api', + items: [ + { + type: 'docSidebar', + sidebarId: 'tutorialSidebar', + position: 'left', + label: 'Docs', + }, + { + to: '/api', + label: 'API', + position: 'left', + }, + ], + }, + prism: { + theme: prismThemes.github, + darkTheme: prismThemes.dracula, + }, + } satisfies Preset.ThemeConfig, }; export default config; diff --git a/playground/website/sidebars.ts b/playground/website/sidebars.ts index acc7685..1dded3c 100644 --- a/playground/website/sidebars.ts +++ b/playground/website/sidebars.ts @@ -1,4 +1,4 @@ -import type {SidebarsConfig} from '@docusaurus/plugin-content-docs'; +import type { SidebarsConfig } from '@docusaurus/plugin-content-docs'; /** * Creating a sidebar enables you to: @@ -11,11 +11,11 @@ import type {SidebarsConfig} from '@docusaurus/plugin-content-docs'; Create as many sidebars as you want. */ const sidebars: SidebarsConfig = { - // By default, Docusaurus generates a sidebar from the docs folder structure - tutorialSidebar: [{type: 'autogenerated', dirName: '.'}], + // By default, Docusaurus generates a sidebar from the docs folder structure + tutorialSidebar: [{ type: 'autogenerated', dirName: '.' }], - // But you can create a sidebar manually - /* + // But you can create a sidebar manually + /* tutorialSidebar: [ 'intro', 'hello', diff --git a/playground/website/src/css/custom.css b/playground/website/src/css/custom.css index 2bc6a4c..044310b 100644 --- a/playground/website/src/css/custom.css +++ b/playground/website/src/css/custom.css @@ -6,25 +6,25 @@ /* You can override the default Infima variables here. */ :root { - --ifm-color-primary: #2e8555; - --ifm-color-primary-dark: #29784c; - --ifm-color-primary-darker: #277148; - --ifm-color-primary-darkest: #205d3b; - --ifm-color-primary-light: #33925d; - --ifm-color-primary-lighter: #359962; - --ifm-color-primary-lightest: #3cad6e; - --ifm-code-font-size: 95%; - --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1); + --ifm-color-primary: #2e8555; + --ifm-color-primary-dark: #29784c; + --ifm-color-primary-darker: #277148; + --ifm-color-primary-darkest: #205d3b; + --ifm-color-primary-light: #33925d; + --ifm-color-primary-lighter: #359962; + --ifm-color-primary-lightest: #3cad6e; + --ifm-code-font-size: 95%; + --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1); } /* For readability concerns, you should choose a lighter palette in dark mode. */ [data-theme='dark'] { - --ifm-color-primary: #25c2a0; - --ifm-color-primary-dark: #21af90; - --ifm-color-primary-darker: #1fa588; - --ifm-color-primary-darkest: #1a8870; - --ifm-color-primary-light: #29d5b0; - --ifm-color-primary-lighter: #32d8b4; - --ifm-color-primary-lightest: #4fddbf; - --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3); + --ifm-color-primary: #25c2a0; + --ifm-color-primary-dark: #21af90; + --ifm-color-primary-darker: #1fa588; + --ifm-color-primary-darkest: #1a8870; + --ifm-color-primary-light: #29d5b0; + --ifm-color-primary-lighter: #32d8b4; + --ifm-color-primary-lightest: #4fddbf; + --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3); } diff --git a/playground/website/src/pages/index.module.css b/playground/website/src/pages/index.module.css index 9f71a5d..d7160c1 100644 --- a/playground/website/src/pages/index.module.css +++ b/playground/website/src/pages/index.module.css @@ -4,20 +4,20 @@ */ .heroBanner { - padding: 4rem 0; - text-align: center; - position: relative; - overflow: hidden; + padding: 4rem 0; + text-align: center; + position: relative; + overflow: hidden; } @media screen and (max-width: 996px) { - .heroBanner { - padding: 2rem; - } + .heroBanner { + padding: 2rem; + } } .buttons { - display: flex; - align-items: center; - justify-content: center; + display: flex; + align-items: center; + justify-content: center; } diff --git a/playground/website/src/pages/index.tsx b/playground/website/src/pages/index.tsx index 70e33ad..a21d97c 100644 --- a/playground/website/src/pages/index.tsx +++ b/playground/website/src/pages/index.tsx @@ -7,33 +7,33 @@ import styles from './index.module.css'; import Link from '@docusaurus/Link'; function HomepageHeader() { - const {siteConfig} = useDocusaurusContext(); - return ( -
    -
    - - {siteConfig.title} - -
    -
    - ); + const { siteConfig } = useDocusaurusContext(); + return ( +
    +
    + + {siteConfig.title} + +
    +
    + ); } export default function Home(): JSX.Element { - const {siteConfig} = useDocusaurusContext(); - return ( - - -
    - Show generated API documentation -
    -
    - ); + const { siteConfig } = useDocusaurusContext(); + return ( + + +
    + Show generated API documentation +
    +
    + ); }