diff options
| author | ST-DDT <[email protected]> | 2024-04-01 10:21:18 +0200 |
|---|---|---|
| committer | GitHub <[email protected]> | 2024-04-01 10:21:18 +0200 |
| commit | 6191a5d883048b694404dbf42527caba395828ea (patch) | |
| tree | d0f18f17789cb0bbdb5d6087f1a95772438dfe27 | |
| parent | 7dae52bfcd93c41ec9d2c4dd4d96a07f31c3dfc1 (diff) | |
| download | faker-6191a5d883048b694404dbf42527caba395828ea.tar.xz faker-6191a5d883048b694404dbf42527caba395828ea.zip | |
docs: rewrite api-docs generation using ts-morph (#2628)
58 files changed, 4012 insertions, 2927 deletions
diff --git a/.github/renovate.json5 b/.github/renovate.json5 index 3421abe7..ac590676 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -55,12 +55,7 @@ }, { "groupName": "doc-dependencies", - "matchPackageNames": [ - "@algolia/client-search", - "typedoc", - "typedoc-plugin-missing-exports", - "vitepress" - ] + "matchPackageNames": ["@algolia/client-search", "ts-morph", "vitepress"] } ], "vulnerabilityAlerts": { diff --git a/.prettierignore b/.prettierignore index 8997b865..9c80acc2 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,7 +1,7 @@ .pnpm-store/ coverage/ dist/ -test/scripts/apidoc/temp/ +test/scripts/apidocs/temp/ CHANGELOG.md CHANGELOG_old.md pnpm-lock.yaml diff --git a/docs/.vitepress/components/api-docs/method-parameters.vue b/docs/.vitepress/components/api-docs/method-parameters.vue index fbc28635..7b14133d 100644 --- a/docs/.vitepress/components/api-docs/method-parameters.vue +++ b/docs/.vitepress/components/api-docs/method-parameters.vue @@ -1,7 +1,7 @@ <script setup lang="ts"> -import type { MethodParameter } from './method'; +import type { ApiDocsMethodParameter } from './method'; -const props = defineProps<{ parameters: MethodParameter[] }>(); +const props = defineProps<{ parameters: ApiDocsMethodParameter[] }>(); </script> <template> diff --git a/docs/.vitepress/components/api-docs/method.ts b/docs/.vitepress/components/api-docs/method.ts index 3c1b9b95..90310ed0 100644 --- a/docs/.vitepress/components/api-docs/method.ts +++ b/docs/.vitepress/components/api-docs/method.ts @@ -1,19 +1,19 @@ -export interface Method { +export interface ApiDocsMethod { readonly name: string; + readonly deprecated: string | undefined; // HTML readonly description: string; // HTML - readonly parameters: MethodParameter[]; + readonly since: string; + readonly parameters: ApiDocsMethodParameter[]; readonly returns: string; + readonly throws: string | undefined; // HTML readonly examples: string; // HTML - readonly deprecated?: string; // HTML - readonly since: string; - readonly sourcePath: string; // URL-Suffix readonly seeAlsos: string[]; - readonly throws?: string; // HTML + readonly sourcePath: string; // URL-Suffix } -export interface MethodParameter { +export interface ApiDocsMethodParameter { readonly name: string; - readonly type?: string; - readonly default?: string; + readonly type: string | undefined; + readonly default: string | undefined; readonly description: string; // HTML } diff --git a/docs/.vitepress/components/api-docs/method.vue b/docs/.vitepress/components/api-docs/method.vue index 2b3f0c5b..8e25e8f8 100644 --- a/docs/.vitepress/components/api-docs/method.vue +++ b/docs/.vitepress/components/api-docs/method.vue @@ -1,10 +1,10 @@ <script setup lang="ts"> -import type { Method } from './method'; +import type { ApiDocsMethod } from './method'; import MethodParameters from './method-parameters.vue'; import { slugify } from '../../shared/utils/slugify'; import { sourceBaseUrl } from '../../../api/source-base-url'; -const props = defineProps<{ method: Method }>(); +const props = defineProps<{ method: ApiDocsMethod }>(); function seeAlsoToUrl(see: string): string { const [, module, method] = see.replace(/\(.*/, '').split('\.'); diff --git a/package.json b/package.json index 8abe25a1..05770786 100644 --- a/package.json +++ b/package.json @@ -65,13 +65,12 @@ "build:types": "tsc --project tsconfig.build.json", "build": "run-s build:clean build:code build:types", "generate": "run-s generate:locales generate:api-docs", - "generate:api-docs": "tsx ./scripts/apidoc.ts", + "generate:api-docs": "tsx ./scripts/apidocs.ts", "generate:locales": "tsx ./scripts/generate-locales.ts", - "docs:build": "run-s docs:prepare docs:build:run", + "docs:build": "run-s generate:api-docs docs:build:run", "docs:build:run": "vitepress build docs", "docs:build:ci": "run-s build docs:build", - "docs:prepare": "run-s generate:api-docs", - "docs:dev": "run-s docs:prepare docs:dev:run", + "docs:dev": "run-s generate:api-docs docs:dev:run", "docs:dev:run": "vitepress dev docs", "docs:serve": "vitepress serve docs --port 5173", "docs:diff": "tsx ./scripts/diff.ts", @@ -126,9 +125,9 @@ "sanitize-html": "2.13.0", "semver": "7.6.0", "standard-version": "9.5.0", + "ts-morph": "22.0.0", "tsup": "8.0.2", "tsx": "4.7.1", - "typedoc": "0.25.12", "typescript": "5.4.3", "validator": "13.11.0", "vite": "5.2.6", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 40b2565e..c79ae455 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -113,15 +113,15 @@ devDependencies: standard-version: specifier: 9.5.0 version: 9.5.0 + ts-morph: + specifier: 22.0.0 + version: 22.0.0 tsup: specifier: 8.0.2 version: 8.0.2([email protected]) tsx: specifier: 4.7.1 version: 4.7.1 - typedoc: - specifier: 0.25.12 - version: 0.25.12([email protected]) typescript: specifier: 5.4.3 version: 5.4.3 @@ -1297,6 +1297,15 @@ packages: resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} dev: true + /@ts-morph/[email protected]: + resolution: {integrity: sha512-m7Lllj9n/S6sOkCkRftpM7L24uvmfXQFedlW/4hENcuJH1HHm9u5EgxZb9uVjQSCGrbBWBkOGgcTxNg36r6ywA==} + dependencies: + fast-glob: 3.3.2 + minimatch: 9.0.3 + mkdirp: 3.0.1 + path-browserify: 1.0.1 + dev: true + /@types/[email protected]: resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} dev: true @@ -1986,10 +1995,6 @@ packages: engines: {node: '>=12'} dev: true - resolution: {integrity: sha512-vJXt3yiaUL4UU546s3rPXlsry/RnM730G1+HkpKE012AN0sx1eOrxSu95oKDIonskeLTijMgqWZ3uDEe3NFvyg==} - dev: true - resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} engines: {node: '>=4'} @@ -2383,6 +2388,10 @@ packages: wrap-ansi: 7.0.0 dev: true + resolution: {integrity: sha512-c5or4P6erEA69TxaxTNcHUNcIn+oyxSRTOWV+pSYF+z4epXqNvwvJ70XPGjPNgue83oAFAPBRQYwpAJ/Hpe/Sg==} + dev: true + resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} dependencies: @@ -4689,10 +4698,6 @@ packages: yallist: 4.0.0 dev: true - resolution: {integrity: sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==} - dev: true - resolution: {integrity: sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==} engines: {node: '>=12'} @@ -4729,12 +4734,6 @@ packages: resolution: {integrity: sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==} dev: true - resolution: {integrity: sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==} - engines: {node: '>= 12'} - hasBin: true - dev: true - resolution: {integrity: sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw==} engines: {node: '>= 0.10.0'} @@ -4845,6 +4844,12 @@ packages: resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} dev: true + resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==} + engines: {node: '>=10'} + hasBin: true + dev: true + resolution: {integrity: sha512-vLgaHvaeunuOXHSmEbZ9izxPx3USsk8KCQ8iC+aTlp5sKRSoZvwhHh5L9VbKSaVC6sJDqbyohIS76E2VmHIPAA==} dependencies: @@ -5152,6 +5157,10 @@ packages: resolution: {integrity: sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==} dev: true + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + dev: true + resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==} engines: {node: '>=4'} @@ -5738,15 +5747,6 @@ packages: resolution: {integrity: sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==} dev: true - resolution: {integrity: sha512-dNPAPrxSc87ua2sKJ3H5dQ/6ZaY8RNnaAqK+t0eG7p0Soi2ydiqbGOTaZCqaYvA/uZYfS1LJnemt3Q+mSfcPCg==} - dependencies: - ansi-sequence-parser: 1.1.1 - jsonc-parser: 3.2.1 - vscode-oniguruma: 1.7.0 - vscode-textmate: 8.0.0 - dev: true - resolution: {integrity: sha512-u+XW6o0vCkUNlneZb914dLO+AayEIwK5tI62WeS//R5HIXBFiYaj/Hc5xcq27Yh83Grr4JbNtUBV8W6zyK4hWg==} dependencies: @@ -6226,6 +6226,13 @@ packages: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} dev: true + resolution: {integrity: sha512-M9MqFGZREyeb5fTl6gNHKZLqBQA0TjA1lea+CR48R8EBTDuWrNqW6ccC5QvjNR4s6wDumD3LTCjOFSp9iwlzaw==} + dependencies: + '@ts-morph/common': 0.23.0 + code-block-writer: 13.0.1 + dev: true + resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} dev: true @@ -6404,20 +6411,6 @@ packages: resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} dev: true - resolution: {integrity: sha512-F+qhkK2VoTweDXd1c42GS/By2DvI2uDF4/EpG424dTexSHdtCH52C6IcAvMA6jR3DzAWZjHpUOW+E02kyPNUNw==} - engines: {node: '>= 16'} - hasBin: true - peerDependencies: - typescript: 4.6.x || 4.7.x || 4.8.x || 4.9.x || 5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x - dependencies: - lunr: 2.3.9 - marked: 4.3.0 - minimatch: 9.0.3 - shiki: 0.14.7 - typescript: 5.4.3 - dev: true - resolution: {integrity: sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg==} engines: {node: '>=14.17'} @@ -6707,14 +6700,6 @@ packages: - terser dev: true - resolution: {integrity: sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==} - dev: true - - resolution: {integrity: sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==} - dev: true - resolution: {integrity: sha512-EOG8KXDQNwkJILkx/gPcoL/7vH+hORoBaKgGe+6W7VFMvCYJfmF2dGbvgDroVnI8LU7/kTu8mbjRZGBU1z9NTA==} engines: {node: '>=12'} diff --git a/scripts/apidoc.ts b/scripts/apidoc.ts deleted file mode 100644 index 42eee5c3..00000000 --- a/scripts/apidoc.ts +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env node - -import { generate } from './apidoc/generate'; -import { initMarkdownRenderer } from './apidoc/markdown'; - -await initMarkdownRenderer(); -await generate(); diff --git a/scripts/apidoc/faker-class.ts b/scripts/apidoc/faker-class.ts deleted file mode 100644 index a9acad3c..00000000 --- a/scripts/apidoc/faker-class.ts +++ /dev/null @@ -1,86 +0,0 @@ -import type { DeclarationReflection, ProjectReflection } from 'typedoc'; -import { ReflectionKind } from 'typedoc'; -import type { Method } from '../../docs/.vitepress/components/api-docs/method'; -import { analyzeModule, processModuleMethods } from './module-methods'; -import { analyzeSignature } from './signature'; -import { extractModuleFieldName, selectApiSignature } from './typedoc'; -import type { ModuleSummary } from './utils'; -import { writeApiDocsModule } from './writer'; - -export async function processFakerClasses( - project: ProjectReflection -): Promise<ModuleSummary[]> { - const fakerClasses = project - .getChildrenByKind(ReflectionKind.Class) - .filter((clazz) => clazz.name === 'Faker' || clazz.name === 'SimpleFaker'); - - if (fakerClasses.length !== 2) { - throw new Error('Faker classes not found'); - } - - return Promise.all(fakerClasses.map(processClass)); -} - -export async function processFakerRandomizer( - project: ProjectReflection -): Promise<ModuleSummary> { - const randomizerClass = project - .getChildrenByKind(ReflectionKind.Interface) - .find((clazz) => clazz.name === 'Randomizer'); - - if (randomizerClass == null) { - throw new Error('Randomizer class not found'); - } - - return processClass(randomizerClass); -} - -async function processClass( - clazz: DeclarationReflection -): Promise<ModuleSummary> { - const { name } = clazz; - const moduleFieldName = extractModuleFieldName(clazz); - - console.log(`Processing ${name} class`); - - const { comment, deprecated, examples } = analyzeModule(clazz); - const methods: Method[] = []; - - if (hasConstructor(clazz)) { - console.debug(`- constructor`); - methods.push(await processConstructor(clazz)); - } - - methods.push(...(await processModuleMethods(clazz, `${moduleFieldName}.`))); - - return writeApiDocsModule( - name, - moduleFieldName, - comment, - examples, - deprecated, - methods, - '' - ); -} - -function hasConstructor(clazz: DeclarationReflection): boolean { - return clazz - .getChildrenByKind(ReflectionKind.Constructor) - .some((constructor) => (constructor.signatures?.length ?? 0) > 0); -} - -async function processConstructor( - clazz: DeclarationReflection -): Promise<Method> { - const constructor = clazz.getChildrenByKind(ReflectionKind.Constructor)[0]; - - const signature = selectApiSignature(constructor); - - const method = await analyzeSignature(signature, '', `new ${clazz.name}`); - - return { - ...method, - name: 'constructor', - }; -} diff --git a/scripts/apidoc/faker-utilities.ts b/scripts/apidoc/faker-utilities.ts deleted file mode 100644 index ad1f74c3..00000000 --- a/scripts/apidoc/faker-utilities.ts +++ /dev/null @@ -1,40 +0,0 @@ -import type { DeclarationReflection, ProjectReflection } from 'typedoc'; -import { ReflectionKind } from 'typedoc'; -import type { Method } from '../../docs/.vitepress/components/api-docs/method'; -import { processMethods } from './module-methods'; -import { selectApiSignature } from './typedoc'; -import type { ModuleSummary } from './utils'; -import { writeApiDocsModule } from './writer'; - -export async function processFakerUtilities( - project: ProjectReflection -): Promise<ModuleSummary> { - const fakerUtilities = project - .getChildrenByKind(ReflectionKind.Function) - .filter((method) => !method.flags.isPrivate); - - return processUtilities(fakerUtilities); -} - -async function processUtilities( - fakerUtilities: DeclarationReflection[] -): Promise<ModuleSummary> { - console.log(`Processing Faker Utilities`); - const comment = 'A list of all the utilities available in Faker.js.'; - - const methods: Method[] = await processMethods( - Object.fromEntries( - fakerUtilities.map((method) => [method.name, selectApiSignature(method)]) - ) - ); - - return writeApiDocsModule( - 'Utilities', - 'utils', - comment, - undefined, - undefined, - methods, - '' - ); -} diff --git a/scripts/apidoc/generate.ts b/scripts/apidoc/generate.ts deleted file mode 100644 index eb00cea4..00000000 --- a/scripts/apidoc/generate.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { resolve } from 'node:path'; -import { processFakerClasses, processFakerRandomizer } from './faker-class'; -import { processFakerUtilities } from './faker-utilities'; -import { processModules } from './module-methods'; -import { loadProject } from './typedoc'; -import { pathOutputDir } from './utils'; -import { - writeApiDiffIndex, - writeApiPagesIndex, - writeApiSearchIndex, - writeSourceBaseUrl, -} from './writer'; - -const pathOutputJson = resolve(pathOutputDir, 'typedoc.json'); - -/** - * Generates the API documentation. - */ -export async function generate(): Promise<void> { - const [app, project] = await loadProject(); - - // Useful for manually analyzing the content - await app.generateJson(project, pathOutputJson); - - const pages = [ - ...(await processFakerClasses(project)), - await processFakerRandomizer(project), - await processFakerUtilities(project), - ...(await processModules(project)).sort((a, b) => - a.text.localeCompare(b.text) - ), - ]; - await writeApiPagesIndex( - pages.map(({ text, link, category }) => ({ text, link, category })) - ); - writeApiDiffIndex( - Object.fromEntries(pages.map(({ text, diff }) => [text, diff])) - ); - writeApiSearchIndex(pages); - - await writeSourceBaseUrl(project); -} diff --git a/scripts/apidoc/module-methods.ts b/scripts/apidoc/module-methods.ts deleted file mode 100644 index c21ccf97..00000000 --- a/scripts/apidoc/module-methods.ts +++ /dev/null @@ -1,122 +0,0 @@ -import type { - DeclarationReflection, - ProjectReflection, - SignatureReflection, -} from 'typedoc'; -import type { Method } from '../../docs/.vitepress/components/api-docs/method'; -import { codeToHtml } from './markdown'; -import { analyzeSignature } from './signature'; -import { - extractDeprecated, - extractDescription, - extractJoinedRawExamples, - extractModuleFieldName, - extractModuleName, - selectApiMethodSignatures, - selectApiModules, -} from './typedoc'; -import type { ModuleSummary } from './utils'; -import { adjustUrls } from './utils'; -import { writeApiDocsModule } from './writer'; - -/** - * Analyzes and writes the documentation for modules and their methods such as `faker.animal.cat()`. - * - * @param project The project used to extract the modules. - * - * @returns The generated pages. - */ -export async function processModules( - project: ProjectReflection -): Promise<ModuleSummary[]> { - return Promise.all(selectApiModules(project).map(processModule)); -} - -/** - * Analyzes and writes the documentation for a module and its methods such as `faker.animal.cat()`. - * - * @param module The module to process. - * - * @returns The generated pages. - */ -async function processModule( - module: DeclarationReflection -): Promise<ModuleSummary> { - const moduleName = extractModuleName(module); - console.log(`Processing Module ${moduleName}`); - const moduleFieldName = extractModuleFieldName(module); - const { comment, deprecated, examples } = analyzeModule(module); - const methods = await processModuleMethods( - module, - `faker.${moduleFieldName}.` - ); - - return writeApiDocsModule( - moduleName, - moduleFieldName, - comment, - examples, - deprecated, - methods, - 'Modules' - ); -} - -/** - * Analyzes the documentation for a class. - * - * @param module The class to process. - * - * @returns The class information. - */ -export function analyzeModule(module: DeclarationReflection): { - comment: string; - deprecated: string | undefined; - examples: string | undefined; -} { - const examplesRaw = extractJoinedRawExamples(module); - const examples = examplesRaw ? codeToHtml(examplesRaw) : undefined; - - return { - comment: adjustUrls(extractDescription(module)), - deprecated: extractDeprecated(module), - examples, - }; -} - -/** - * Processes all api methods of the given class. This does not include the constructor. - * - * @param module The module to process. - * @param accessor The code used to access the methods within the module. - * - * @returns A list containing the documentation for the api methods in the given module. - */ -export async function processModuleMethods( - module: DeclarationReflection, - accessor: string -): Promise<Method[]> { - return processMethods(selectApiMethodSignatures(module), accessor); -} - -/** - * Processes all api methods. - * - * @param signatures The signatures to process. - * @param accessor The code used to access the methods. - * - * @returns A list containing the documentation for the api methods. - */ -export async function processMethods( - signatures: Record<string, SignatureReflection>, - accessor: string = '' -): Promise<Method[]> { - const methods: Method[] = []; - - for (const [methodName, signature] of Object.entries(signatures)) { - console.debug(`- ${methodName}`); - methods.push(await analyzeSignature(signature, accessor, methodName)); - } - - return methods; -} diff --git a/scripts/apidoc/parameter-defaults.ts b/scripts/apidoc/parameter-defaults.ts deleted file mode 100644 index a7f53c2b..00000000 --- a/scripts/apidoc/parameter-defaults.ts +++ /dev/null @@ -1,136 +0,0 @@ -import type { - Context, - DeclarationReflection, - EventCallback, - JSONOutput, - ProjectReflection, - SerializerComponent, - SignatureReflection, -} from 'typedoc'; -import { Reflection, ReflectionKind, TypeScript } from 'typedoc'; - -const reflectionKindFunctionOrMethod = - ReflectionKind.Function | ReflectionKind.Method; - -interface ParameterDefaultsAware extends Reflection { - implementationDefaultParameters: Array<string | undefined>; -} - -/** - * TypeDoc EventCallback for EVENT_CREATE_DECLARATION events that reads the default parameters from the implementation. - * - * @param context The converter context. - * @param reflection The reflection to read the default parameters from. - */ -export const parameterDefaultReader: EventCallback = ( - context: Context, - reflection: Reflection -): void => { - const symbol = context.project.getSymbolFromReflection(reflection); - if (!symbol) return; - - if ( - reflection.kindOf(reflectionKindFunctionOrMethod) && - symbol.declarations?.length - ) { - const lastDeclaration = symbol.declarations.at(-1); - if (TypeScript.isFunctionLike(lastDeclaration)) { - (reflection as ParameterDefaultsAware).implementationDefaultParameters = - lastDeclaration.parameters.map((param) => - cleanParameterDefault(param.initializer?.getText()) - ); - } - } -}; - -/** - * Removes compile expressions that don't add any value for readers. - * - * @param value The default value to clean. - * - * @returns The cleaned default value. - */ -function cleanParameterDefault(value: string): string; -function cleanParameterDefault(value?: string): string | undefined; -function cleanParameterDefault(value?: string): string | undefined { - if (value == null) { - return undefined; - } - - // Strip type casts: "'foobar' as unknown as T" => "'foobar'" - return value.replace(/( as unknown)? as [A-Za-z<>]+/, ''); -} - -/** - * Serializer that adds the `implementationDefaultParameters` to the JSON output. - */ -export class DefaultParameterAwareSerializer - implements SerializerComponent<Reflection> -{ - readonly priority = 0; - - supports(item: unknown): item is Reflection { - return item instanceof Reflection; - } - - toObject( - item: Reflection, - obj: Partial<JSONOutput.Reflection> - ): Partial<JSONOutput.Reflection> { - (obj as unknown as ParameterDefaultsAware).implementationDefaultParameters = - (item as ParameterDefaultsAware).implementationDefaultParameters; - return obj; - } -} - -/** - * Replaces all methods' last signature's parameter's default value with the default value read from the implementation. - * - * @param project The project to patch. - */ -export function patchProjectParameterDefaults( - project: ProjectReflection -): void { - const functionOrMethods = project.getReflectionsByKind( - reflectionKindFunctionOrMethod - ) as DeclarationReflection[]; - for (const functionOrMethod of functionOrMethods) { - patchMethodParameterDefaults(functionOrMethod); - } -} - -/** - * Replaces the last signature's parameter's default value with the default value read from the implementation. - * - * @param method The method to patch. - */ -function patchMethodParameterDefaults(method: DeclarationReflection): void { - const signatures = method.signatures; - const signature = signatures?.[signatures.length - 1]; - const parameterDefaults = (method as unknown as ParameterDefaultsAware) - .implementationDefaultParameters; - if (signature && parameterDefaults) { - patchSignatureParameterDefaults(signature, parameterDefaults); - } -} - -/** - * Replaces the given signature's parameter's default value with the given default values. - * - * @param signature The signature to patch. - * @param parameterDefaults The defaults to add. - */ -function patchSignatureParameterDefaults( - signature: SignatureReflection, - parameterDefaults: Array<string | undefined> -): void { - const signatureParameters = - signature.parameters ?? Array.from({ length: parameterDefaults.length }); - if (signatureParameters.length !== parameterDefaults.length) { - throw new Error('Unexpected parameter length mismatch'); - } - - for (const [index, param] of signatureParameters.entries()) { - param.defaultValue = parameterDefaults[index] || param.defaultValue; - } -} diff --git a/scripts/apidoc/signature.ts b/scripts/apidoc/signature.ts deleted file mode 100644 index 64910534..00000000 --- a/scripts/apidoc/signature.ts +++ /dev/null @@ -1,365 +0,0 @@ -import type { - Comment, - DeclarationReflection, - ParameterReflection, - Reflection, - ReflectionType, - SignatureReflection, - SomeType, - Type, -} from 'typedoc'; -import { ReflectionFlag, ReflectionKind } from 'typedoc'; -import type { - Method, - MethodParameter, -} from '../../docs/.vitepress/components/api-docs/method'; -import { formatTypescript } from './format'; -import { codeToHtml, mdToHtml } from './markdown'; -import { - extractDeprecated, - extractDescription, - extractJoinedRawExamples, - extractRawDefault, - extractSeeAlsos, - extractSince, - extractSourcePath, - extractSummaryDefault, - extractThrows, - toBlock, -} from './typedoc'; - -export async function analyzeSignature( - signature: SignatureReflection, - accessor: string, - methodName: string -): Promise<Method> { - const parameters: MethodParameter[] = []; - - // Collect Type Parameters - const typeParameters = signature.typeParameters || []; - const signatureTypeParameters: string[] = []; - for (const parameter of typeParameters) { - signatureTypeParameters.push(parameter.name); - parameters.push({ - name: `<${parameter.name}>`, - type: parameter.type ? await typeToText(parameter.type) : undefined, - description: mdToHtml(extractDescription(parameter)), - }); - } - - // Collect Parameters - const signatureParameters: string[] = []; - for ( - let index = 0; - signature.parameters && index < signature.parameters.length; - index++ - ) { - const parameter = signature.parameters[index]; - - const aParam = await analyzeParameter(parameter); - signatureParameters.push(aParam.signature); - parameters.push(...aParam.parameters); - } - - // Generate usage section - - let signatureTypeParametersString = ''; - if (signatureTypeParameters.length > 0) { - signatureTypeParametersString = `<${signatureTypeParameters.join(', ')}>`; - } - - const signatureParametersString = signatureParameters.join(', '); - - let examples = `${accessor}${methodName}${signatureTypeParametersString}(${signatureParametersString}): ${signature.type?.toString()}\n`; - - const exampleTags = extractJoinedRawExamples(signature); - if (exampleTags) { - examples += exampleTags; - } - - const seeAlsos = extractSeeAlsos(signature).map((seeAlso) => - mdToHtml(seeAlso, true) - ); - const deprecatedMessage = extractDeprecated(signature); - const deprecated = deprecatedMessage - ? mdToHtml(deprecatedMessage) - : undefined; - const throwsMessage = extractThrows(signature); - const throws = throwsMessage ? mdToHtml(throwsMessage, true) : undefined; - - return { - name: methodName, - description: mdToHtml(extractDescription(signature)), - parameters: parameters, - since: extractSince(signature), - sourcePath: extractSourcePath(signature), - throws, - returns: await typeToText(signature.type), - examples: codeToHtml(examples), - deprecated, - seeAlsos, - }; -} - -async function analyzeParameter(parameter: ParameterReflection): Promise<{ - parameters: MethodParameter[]; - signature: string; -}> { - const name = parameter.name; - const declarationName = name + (isOptional(parameter) ? '?' : ''); - const type = parameter.type; - const defaultValue = extractDefaultFromParameter(parameter); - - let signatureText = ''; - if (defaultValue) { - signatureText = ` = ${defaultValue}`; - } - - const signature = `${declarationName}: ${await typeToText( - type - )}${signatureText}`; - - const parameters: MethodParameter[] = [ - { - name: declarationName, - type: await typeToText(type, true), - default: defaultValue, - description: mdToHtml(extractDescription(parameter)), - }, - ]; - parameters.push(...(await analyzeParameterOptions(name, type))); - - return { - parameters, - signature, - }; -} - -// keep in sync with assertNestedParameterDefault -async function analyzeParameterOptions( - name: string, - parameterType?: SomeType -): Promise<MethodParameter[]> { - if (!parameterType) { - return []; - } - - switch (parameterType.type) { - case 'array': { - return analyzeParameterOptions(`${name}[]`, parameterType.elementType); - } - - case 'union': { - return Promise.all( - parameterType.types.map((type) => analyzeParameterOptions(name, type)) - ).then((options) => options.flat()); - } - - case 'reflection': { - const properties = parameterType.declaration.children ?? []; - return Promise.all( - properties.map(async (property) => { - const reflection = property.comment - ? property - : (property.type as ReflectionType)?.declaration?.signatures?.[0]; - const comment = reflection?.comment; - const deprecated = extractDeprecated(reflection); - return { - name: `${name}.${property.name}${isOptional(property) ? '?' : ''}`, - type: await declarationTypeToText(property), - default: extractDefaultFromComment(comment), - description: mdToHtml( - toBlock(comment) + - (deprecated ? `\n\n**DEPRECATED:** ${deprecated}` : '') - ), - }; - }) - ); - } - - case 'typeOperator': { - return analyzeParameterOptions(name, parameterType.target); - } - - default: { - return []; - } - } -} - -function isOptional(parameter: Reflection): boolean { - return parameter.flags.hasFlag(ReflectionFlag.Optional); -} - -async function typeToText(type_?: Type, short = false): Promise<string> { - if (!type_) { - return '?'; - } - - const type = type_ as SomeType; - switch (type.type) { - case 'array': { - const text = await typeToText(type.elementType, short); - const isComplexType = text.includes('|') || text.includes('{'); - return isComplexType ? `Array<${text}>` : `${text}[]`; - } - - case 'union': { - return (await Promise.all(type.types.map((t) => typeToText(t, short)))) - .map((t) => (t.includes('=>') ? `(${t})` : t)) - .sort() - .join(' | '); - } - - case 'reference': { - if (!type.typeArguments || type.typeArguments.length === 0) { - const reflection = type.reflection as DeclarationReflection | undefined; - const reflectionType = reflection?.type; - if ( - (reflectionType?.type === 'literal' || - reflectionType?.type === 'union') && - !type.name.endsWith('Char') - ) { - return typeToText(reflectionType, short); - } - - return type.name; - } else if (type.name === 'LiteralUnion') { - return [ - await typeToText(type.typeArguments[0], short), - await typeToText(type.typeArguments[1], short), - ].join(' | '); - } - - return `${type.name}<${( - await Promise.all(type.typeArguments.map((t) => typeToText(t, short))) - ).join(', ')}>`; - } - - case 'reflection': { - return declarationTypeToText(type.declaration, short); - } - - case 'indexedAccess': { - return `${await typeToText(type.objectType, short)}[${await typeToText( - type.indexType, - short - )}]`; - } - - case 'literal': { - return (await formatTypescript(type.toString())).replace(/;\n$/, ''); - } - - case 'typeOperator': { - const text = await typeToText(type.target, short); - if (short && type.operator === 'readonly') { - return text; - } - - return `${type.operator} ${text}`; - } - - default: { - return type.toString(); - } - } -} - -async function declarationTypeToText( - declaration: DeclarationReflection, - short = false -): Promise<string> { - switch (declaration.kind) { - case ReflectionKind.Method: { - return signatureTypeToText(declaration.signatures?.[0]); - } - - case ReflectionKind.Property: { - return typeToText(declaration.type); - } - - case ReflectionKind.TypeLiteral: { - if (declaration.children?.length) { - if (short) { - // This is too long for the parameter table, thus we abbreviate this. - return '{ ... }'; - } - - const list = ( - await Promise.all( - declaration.children.map( - async (c) => ` ${c.name}: ${await declarationTypeToText(c)}` - ) - ) - ).join(',\n'); - - return `{\n${list}\n}`; - } else if (declaration.signatures?.length) { - return signatureTypeToText(declaration.signatures[0]); - } - - return declaration.toString(); - } - - default: { - return declaration.toString(); - } - } -} - -async function signatureTypeToText( - signature?: SignatureReflection -): Promise<string> { - if (!signature) { - return '(???) => ?'; - } - - return `(${( - await Promise.all( - signature.parameters?.map( - async (p) => `${p.name}: ${await typeToText(p.type)}` - ) ?? [] - ) - ).join(', ')}) => ${await typeToText(signature.type)}`; -} - -/** - * Extracts and optionally removes the parameter default from the parameter. - * - * @param parameter The parameter to extract the default from. - * @param eraseDefault Whether to erase the default text from the parameter comment. - * - * @returns The extracted default value. - */ -function extractDefaultFromParameter( - parameter: ParameterReflection, - eraseDefault = true -): string | undefined { - const commentDefault = extractDefaultFromComment( - parameter.comment, - eraseDefault - ); - return parameter.defaultValue ?? commentDefault; -} - -/** - * Extracts and optionally removes the parameter default from the comments. - * - * @param comment The comment to extract the default from. - * @param eraseDefault Whether to erase the default text from the comment. - * - * @returns The extracted default value. - */ -function extractDefaultFromComment( - comment?: Comment, - eraseDefault = true -): string | undefined { - if (!comment) { - return; - } - - const tagDefault = extractRawDefault({ comment }); - const summaryDefault = extractSummaryDefault(comment, eraseDefault); - return tagDefault || summaryDefault; -} diff --git a/scripts/apidoc/typedoc.ts b/scripts/apidoc/typedoc.ts deleted file mode 100644 index 1cdcf3e1..00000000 --- a/scripts/apidoc/typedoc.ts +++ /dev/null @@ -1,421 +0,0 @@ -import type { - Comment, - CommentDisplayPart, - CommentTag, - DeclarationReflection, - ProjectReflection, - Reflection, - SignatureReflection, - TypeDocOptions, -} from 'typedoc'; -import { - Application, - Converter, - ReflectionKind, - TSConfigReader, -} from 'typedoc'; -import { faker } from '../../src'; -import { - DefaultParameterAwareSerializer, - parameterDefaultReader, - patchProjectParameterDefaults, -} from './parameter-defaults'; -import { mapByName } from './utils'; - -type CommentHolder = Pick<Reflection, 'comment'>; - -/** - * Loads the project using TypeDoc. - * - * @param options The options to use for the project. - * - * @returns The TypeDoc application and the project reflection. - */ -export async function loadProject( - options: Partial<TypeDocOptions> = { - entryPoints: ['src/index.ts'], - pretty: true, - cleanOutputDir: true, - tsconfig: 'tsconfig.build.json', - } -): Promise<[Application, ProjectReflection]> { - const app = await newTypeDocApp(options); - - const project = await app.convert(); - - if (!project) { - throw new Error('Failed to convert project'); - } - - patchProjectParameterDefaults(project); - - return [app, project]; -} - -/** - * Creates and configures a new typedoc application. - * - * @param options The options to use for the project. - */ -async function newTypeDocApp( - options?: Partial<TypeDocOptions> -): Promise<Application> { - const app = await Application.bootstrapWithPlugins(options, [ - new TSConfigReader(), - ]); - - // Read parameter defaults - app.converter.on(Converter.EVENT_CREATE_DECLARATION, parameterDefaultReader); - // Add to debug json output - app.serializer.addSerializer(new DefaultParameterAwareSerializer()); - - return app; -} - -/** - * Selects the modules from the project that needs to be documented. - * - * @param project The project to extract the modules from. - * @param includeTestModules Whether to include test modules. - * - * @returns The modules to document. - */ -export function selectApiModules( - project: ProjectReflection, - includeTestModules = false -): DeclarationReflection[] { - return project - .getChildrenByKind(ReflectionKind.Class) - .filter( - (module) => - faker[extractModuleFieldName(module) as keyof typeof faker] != null || - includeTestModules - ); -} - -/** - * Selects the methods from the module that needs to be documented. - * - * @param module The module to extract the methods from. - * - * @returns The methods to document. - */ -export function selectApiMethods( - module: DeclarationReflection -): DeclarationReflection[] { - return module - .getChildrenByKind(ReflectionKind.Method) - .filter((method) => !method.flags.isPrivate); -} - -/** - * Selects the signature from the method that needs to be documented. - * - * @param method The method to extract the signature from. - * - * @returns The signature to document. - */ -export function selectApiSignature( - method: DeclarationReflection -): SignatureReflection { - const signatures = method.signatures; - if (signatures == null || signatures.length === 0) { - throw new Error(`Method ${method.name} has no signature.`); - } - - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - return signatures.at(-1)!; -} - -/** - * Selects the method signatures from the module that needs to be documented. - * Method-Name -> Method-Signature - * - * @param module The module to extract the method signatures from. - * - * @returns The method signatures to document. - */ -export function selectApiMethodSignatures( - module: DeclarationReflection -): Record<string, SignatureReflection> { - return mapByName(selectApiMethods(module), selectApiSignature); -} - -export function extractModuleName(module: DeclarationReflection): string { - const { name } = module; - // TODO @ST-DDT 2022-10-16: Remove in v10. - // Typedoc prefers the name of the module that is exported first. - if (name === 'AddressModule') { - return 'Location'; - } else if (name === 'NameModule') { - return 'Person'; - } - - return name.replace(/Module$/, ''); -} - -export function extractModuleFieldName(module: DeclarationReflection): string { - const moduleName = extractModuleName(module); - return moduleName[0].toLowerCase() + moduleName.substring(1); -} - -export const MISSING_DESCRIPTION = 'Missing'; - -export function toBlock(comment?: Comment): string { - return joinTagParts(comment?.summary) || MISSING_DESCRIPTION; -} - -export function extractDescription(reflection: Reflection): string { - return toBlock(reflection.comment); -} - -/** - * Extracts the source url from the jsdocs. - * - * @param reflection The reflection instance to extract the source url from. - */ -function extractSourceUrl( - reflection: DeclarationReflection | SignatureReflection -): string { - const source = reflection.sources?.[0]; - return source?.url ?? ''; -} - -/** - * Extracts the source base url from the jsdocs. - * - * @param reflection The reflection instance to extract the source base url from. - */ -export function extractSourceBaseUrl( - reflection: DeclarationReflection | SignatureReflection -): string { - return extractSourceUrl(reflection).replace( - /^(.*\/blob\/[0-9a-f]+\/)(.*)$/, - '$1' - ); -} - -/** - * Extracts the relative source path from the jsdocs. - * - * @param reflection The reflection instance to extract the source path from. - */ -export function extractSourcePath( - reflection: DeclarationReflection | SignatureReflection -): string { - return extractSourceUrl(reflection).replace( - /^(.*\/blob\/[0-9a-f]+\/)(.*)$/, - '$2' - ); -} - -/** - * Extracts the text (md) from a jsdoc tag. - * - * @param tag The tag to extract the text from. - * @param reflection The reflection to extract the text from. - * @param tagProcessor The function used to extract the text from the tag. - */ -export function extractTagContent( - tag: `@${string}`, - reflection?: CommentHolder, - tagProcessor: (tag: CommentTag) => string[] = joinTagContent -): string[] { - const tags = - reflection?.comment - ?.getTags(tag) - .flatMap(tagProcessor) - .map((tag) => tag.trim()) ?? []; - if (tags.some((tag) => tag.length === 0)) { - throw new Error(`Expected non-empty ${tag} tag.`); - } - - return tags; -} - -/** - * Extracts the text (md) from a single jsdoc tag. - * - * @param tag The tag to extract the text from. - * @param reflection The reflection to extract the text from. - * @param tagProcessor The function used to extract the text from the tag. - * - * @throws If there are multiple tags of that type. - */ -function extractSingleTagContent( - tag: `@${string}`, - reflection?: CommentHolder, - tagProcessor: (tag: CommentTag) => string[] = joinTagContent -): string | undefined { - const tags = extractTagContent(tag, reflection, tagProcessor); - if (tags.length === 0) { - return undefined; - } else if (tags.length === 1) { - return tags[0]; - } - - throw new Error(`Expected 1 ${tag} tag, but got ${tags.length}.`); -} - -/** - * Extracts the raw code from the jsdocs without the surrounding md code block. - * - * @param tag The tag to extract the code from. - * @param reflection The reflection to extract the code from. - */ -function extractRawCode( - tag: `@${string}`, - reflection?: CommentHolder -): string[] { - return extractTagContent(tag, reflection).map((tag) => - tag.replace(/^```ts\n/, '').replace(/\n```$/, '') - ); -} - -/** - * Extracts the default from the jsdocs without the surrounding md code block. - * - * @param reflection The reflection to extract the examples from. - */ -export function extractRawDefault(reflection?: CommentHolder): string { - return extractRawCode('@default', reflection)[0] ?? ''; -} - -/** - * Extracts and optionally removes the default from the comment summary. - * - * @param comment The comment to extract the default from. - * @param eraseDefault Whether to erase the default text from the comment. - * - * @returns The extracted default value. - */ -export function extractSummaryDefault( - comment?: Comment, - eraseDefault = true -): string | undefined { - if (!comment) { - return; - } - - const summary = comment.summary; - const text = joinTagParts(summary).trim(); - if (!text) { - return; - } - - const result = /^(.*)[ \n]Defaults to `([^`]+)`\.(.*)$/s.exec(text); - if (!result) { - return; - } - - if (result[3].trim()) { - throw new Error(`Found description text after the default value:\n${text}`); - } - - if (eraseDefault) { - summary.splice(-2, 2); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const lastSummaryPart = summary.at(-1)!; - lastSummaryPart.text = lastSummaryPart.text.replace( - /[ \n]Defaults to $/, - '' - ); - } - - return result[2]; -} - -/** - * Extracts the examples from the jsdocs without the surrounding md code block. - * - * @param reflection The reflection to extract the examples from. - */ -function extractRawExamples(reflection?: CommentHolder): string[] { - return extractRawCode('@example', reflection); -} - -/** - * Extracts the examples from the jsdocs without the surrounding md code block, then joins them with newlines and trims. - * - * @param reflection The reflection to extract the examples from. - */ -export function extractJoinedRawExamples( - reflection?: CommentHolder -): string | undefined { - const examples = extractRawExamples(reflection); - return examples.length === 0 ? undefined : examples.join('\n').trim(); -} - -/** - * Extracts all the `@see` references from the jsdocs separately. - * - * @param reflection The reflection to extract the see also references from. - */ -export function extractSeeAlsos(reflection?: CommentHolder): string[] { - return extractTagContent('@see', reflection, (tag) => - // If the @see tag contains code in backticks, the content is split into multiple parts. - // So we join together, split on newlines and filter out empty tags. - joinTagParts(tag.content) - .split('\n') - .map((link) => { - link = link.trim(); - if (link.startsWith('-')) { - link = link.slice(1).trimStart(); - } - - return link; - }) - .filter((link) => link.length > 0) - ); -} - -/** - * Joins the parts of the given jsdocs tag. - * - * @param tag The tag to join the parts of. - */ -export function joinTagContent(tag: CommentTag): string[] { - return [joinTagParts(tag?.content)]; -} - -export function joinTagParts(parts: CommentDisplayPart[]): string; -export function joinTagParts(parts?: CommentDisplayPart[]): string | undefined; -export function joinTagParts(parts?: CommentDisplayPart[]): string | undefined { - return parts?.map((part) => part.text).join(''); -} - -/** - * Checks if the given reflection is deprecated. - * - * @param reflection The reflection to check. - * - * @returns The message explaining the deprecation if deprecated, otherwise `undefined`. - */ -export function extractDeprecated( - reflection?: CommentHolder -): string | undefined { - return extractSingleTagContent('@deprecated', reflection); -} - -/** - * Extracts the "throws" tag from the provided signature. - * - * @param reflection The reflection to check. - * - * @returns The message explaining the conditions when this method throws. Or `undefined` if it does not throw. - */ -export function extractThrows(reflection?: CommentHolder): string | undefined { - const content = extractTagContent('@throws', reflection).join('\n'); - return content.length === 0 ? undefined : content; -} - -/** - * Extracts the "since" tag from the provided signature. - * - * @param reflection The signature to check. - * - * @returns The contents of the `@since` tag. - */ -export function extractSince(reflection: CommentHolder): string { - return extractSingleTagContent('@since', reflection) || MISSING_DESCRIPTION; -} diff --git a/scripts/apidoc/utils.ts b/scripts/apidoc/utils.ts deleted file mode 100644 index b23f3568..00000000 --- a/scripts/apidoc/utils.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { createHash } from 'node:crypto'; -import { dirname, resolve } from 'node:path'; -import { fileURLToPath } from 'node:url'; -import type { Method } from '../../docs/.vitepress/components/api-docs/method'; - -// Types - -export type Page = { text: string; link: string; category: string }; - -export type ModuleSummary = Page & { - methods: Method[]; - diff: DocsApiDiff; -}; - -export interface DocsApiDiffIndex { - /** - * The methods in the module by name. - */ - [module: string]: DocsApiDiff; -} - -export interface DocsApiDiff { - /** - * The checksum of the entire module. - */ - moduleHash: string; - /** - * The checksum of the method by name. - */ - [method: string]: string; -} - -// Paths - -const pathRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..'); -export const pathDocsDir = resolve(pathRoot, 'docs'); -const pathPublicDir = resolve(pathDocsDir, 'public'); -export const nameDocsDiffIndexFile = 'api-diff-index.json'; -export const pathDocsDiffIndexFile = resolve( - pathPublicDir, - nameDocsDiffIndexFile -); -export const pathOutputDir = resolve(pathDocsDir, 'api'); - -// Functions - -export function adjustUrls(description: string): string { - return description.replaceAll(/https:\/\/(next.)?fakerjs.dev\//g, '/'); -} - -export function mapByName<TInput extends { name: string }, TValue>( - input: TInput[], - valueExtractor: (item: TInput) => TValue -): Record<string, TValue> { - return Object.fromEntries( - input.map((item) => [item.name, valueExtractor(item)]) - ); -} - -/** - * Creates a diff hash for the given method by removing the line number from the source path. - * - * @param method The method to create a hash for. - */ -export function methodDiffHash(method: Method): string { - return diffHash({ - ...method, - sourcePath: method.sourcePath.replaceAll(/#.*/g, ''), - }); -} - -/** - * Creates a diff hash for the given object. - * - * @param object The object to create a hash for. - */ -export function diffHash(object: unknown): string { - return createHash('md5').update(JSON.stringify(object)).digest('hex'); -} diff --git a/scripts/apidoc/writer.ts b/scripts/apidoc/writer.ts deleted file mode 100644 index b03cfe85..00000000 --- a/scripts/apidoc/writer.ts +++ /dev/null @@ -1,249 +0,0 @@ -import { writeFileSync } from 'node:fs'; -import { resolve } from 'node:path'; -import type { ProjectReflection } from 'typedoc'; -import { ReflectionKind } from 'typedoc'; -import type { DefaultTheme } from 'vitepress'; -import type { Method } from '../../docs/.vitepress/components/api-docs/method'; -import type { APIGroup } from '../../docs/api/api-types'; -import { groupBy } from '../../src/internal/group-by'; -import { formatMarkdown, formatTypescript } from './format'; -import { extractSourceBaseUrl } from './typedoc'; -import type { DocsApiDiffIndex, ModuleSummary, Page } from './utils'; -import { - diffHash, - methodDiffHash, - pathDocsDiffIndexFile, - pathDocsDir, - pathOutputDir, -} from './utils'; - -const pathDocsApiPages = resolve(pathDocsDir, '.vitepress', 'api-pages.ts'); -const pathDocsApiSearchIndex = resolve( - pathDocsDir, - 'api', - 'api-search-index.json' -); - -const scriptCommand = 'pnpm run generate:api-docs'; - -// Moved here because this must not be formatted by prettier -const vitePressInFileOptions = `--- -editLink: false ---- - -`; - -/** - * Writes the api docs for the given modules. - * - * @param moduleName The name of the module to write the docs for. - * @param lowerModuleName The lowercase name of the module. - * @param comment The module comments. - * @param examples The example code. - * @param deprecated The deprecation message. - * @param methods The methods of the module. - * @param category The category of the module. - */ -export async function writeApiDocsModule( - moduleName: string, - lowerModuleName: string, - comment: string, - examples: string | undefined, - deprecated: string | undefined, - methods: Method[], - category: string -): Promise<ModuleSummary> { - await writeApiDocsModulePage( - moduleName, - lowerModuleName, - comment, - examples, - deprecated, - methods - ); - writeApiDocsModuleData(lowerModuleName, methods); - - return { - text: moduleName, - link: `/api/${lowerModuleName}.html`, - methods, - category, - diff: { - moduleHash: diffHash({ - name: moduleName, - field: lowerModuleName, - deprecated, - comment, - }), - ...Object.fromEntries( - methods.map((method) => [method.name, methodDiffHash(method)]) - ), - }, - }; -} - -/** - * Writes the api page for the given module to the correct location. - * - * @param moduleName The name of the module to write the docs for. - * @param lowerModuleName The lowercase name of the module. - * @param comment The module comments. - * @param examples The example code. - * @param deprecated The deprecation message. - * @param methods The methods of the module. - */ -async function writeApiDocsModulePage( - moduleName: string, - lowerModuleName: string, - comment: string, - examples: string | undefined, - deprecated: string | undefined, - methods: Method[] -): Promise<void> { - // Write api docs page - let content = ` - <script setup> - import ApiDocsMethod from '../.vitepress/components/api-docs/method.vue'; - import ${lowerModuleName} from './${lowerModuleName}.json'; - </script> - - <!-- This file is automatically generated. --> - <!-- Run '${scriptCommand}' to update --> - - # ${moduleName} - - ::: v-pre - - ${ - deprecated == null - ? '' - : `<div class="warning custom-block"> - <p class="custom-block-title">Deprecated</p> - <p>This module is deprecated and will be removed in a future version.</p> - <span>${deprecated}</span> - </div>` - } - - ${comment} - - ${examples == null ? '' : `<div class="examples">${examples}</div>`} - - ::: - - ${methods - .map( - (method) => ` - ## ${method.name} - - <ApiDocsMethod :method="${lowerModuleName}.${method.name}" v-once /> - ` - ) - .join('')} - `.replaceAll(/\n +/g, '\n'); - - content = vitePressInFileOptions + (await formatMarkdown(content)); - - writeFileSync(resolve(pathOutputDir, `${lowerModuleName}.md`), content); -} - -/** - * Writes the api docs data to correct location. - * - * @param lowerModuleName The lowercase name of the module. - * @param methods The methods data to save. - */ -function writeApiDocsModuleData( - lowerModuleName: string, - methods: Method[] -): void { - const content = JSON.stringify( - Object.fromEntries(methods.map((method) => [method.name, method])) - ); - - writeFileSync(resolve(pathOutputDir, `${lowerModuleName}.json`), content); -} - -/** - * Writes the api docs index to correct location. - * - * @param pages The pages to write into the index. - */ -export async function writeApiPagesIndex(pages: Page[]): Promise<void> { - const pagesByCategory: Record<string, DefaultTheme.SidebarItem[]> = groupBy( - pages, - (page) => page.category, - ({ text, link }) => ({ text, link }) - ); - const pageTree = Object.entries(pagesByCategory).flatMap( - ([category, items]) => (category ? [{ text: category, items }] : items) - ); - - // Write api-pages.ts - console.log('Updating api-pages.ts'); - pageTree.splice(0, 0, { text: 'Overview', link: '/api/' }); - let apiPagesContent = ` - // This file is automatically generated. - // Run '${scriptCommand}' to update - export const apiPages = ${JSON.stringify(pageTree)}; - `.replace(/\n +/, '\n'); - - apiPagesContent = await formatTypescript(apiPagesContent); - - writeFileSync(pathDocsApiPages, apiPagesContent); -} - -/** - * Writes the api diff index to the correct location. - * - * @param diffIndex The diff index project to write. - */ -export function writeApiDiffIndex(diffIndex: DocsApiDiffIndex): void { - writeFileSync(pathDocsDiffIndexFile, JSON.stringify(diffIndex)); -} - -/** - * Writes the api search index to the correct location. - * - * @param pages The pages to write into the index. - */ -export function writeApiSearchIndex(pages: ModuleSummary[]): void { - const apiIndex: APIGroup[] = [ - { - text: 'Module API', - items: pages.map((module) => ({ - text: module.text, - link: module.link, - headers: module.methods.map((method) => ({ - anchor: method.name, - text: method.name, - deprecated: !!method.deprecated, - })), - })), - }, - ]; - - writeFileSync(pathDocsApiSearchIndex, JSON.stringify(apiIndex)); -} - -/** - * Writes the source base url to the correct location. - * - * @param project The typedoc project. - */ -export async function writeSourceBaseUrl( - project: ProjectReflection -): Promise<void> { - const baseUrl = extractSourceBaseUrl( - project.getChildrenByKind(ReflectionKind.Class)[0] - ); - - let content = ` - // This file is automatically generated. - // Run '${scriptCommand}' to update - export const sourceBaseUrl = '${baseUrl}'; - `.replace(/\n +/, '\n'); - - content = await formatTypescript(content); - - writeFileSync(resolve(pathOutputDir, 'source-base-url.ts'), content); -} diff --git a/scripts/apidocs.ts b/scripts/apidocs.ts new file mode 100644 index 00000000..4a109006 --- /dev/null +++ b/scripts/apidocs.ts @@ -0,0 +1,7 @@ +#!/usr/bin/env node + +import { generate } from './apidocs/generate'; +import { initMarkdownRenderer } from './apidocs/utils/markdown'; + +await initMarkdownRenderer(); +await generate(); diff --git a/scripts/apidoc/diff.ts b/scripts/apidocs/diff.ts index 101200cd..0ccdad6e 100644 --- a/scripts/apidoc/diff.ts +++ b/scripts/apidocs/diff.ts @@ -1,12 +1,15 @@ -import type { DocsApiDiffIndex } from './utils'; -import { nameDocsDiffIndexFile, pathDocsDiffIndexFile } from './utils'; +import type { ApiDiffHashes } from './output/diff-index'; +import { + FILE_NAME_DOCS_DIFF_INDEX, + FILE_PATH_DOCS_DIFF_INDEX, +} from './output/diff-index'; /** * Loads the diff index from the given source url. * * @param url The url to load the diff index from. */ -async function loadRemote(url: string): Promise<DocsApiDiffIndex> { +async function loadRemote(url: string): Promise<ApiDiffHashes> { return fetch(url).then((res) => { if (!res.ok) { throw new Error( @@ -14,7 +17,7 @@ async function loadRemote(url: string): Promise<DocsApiDiffIndex> { ); } - return res.json() as Promise<DocsApiDiffIndex>; + return res.json() as Promise<ApiDiffHashes>; }); } @@ -23,8 +26,8 @@ async function loadRemote(url: string): Promise<DocsApiDiffIndex> { * * @param path The path to load the diff index from. Should start with `file://` for cross platform compatibility. */ -async function loadLocal(path: string): Promise<DocsApiDiffIndex> { - return import(path).then((imp) => imp.default as DocsApiDiffIndex); +async function loadLocal(path: string): Promise<ApiDiffHashes> { + return import(path).then((imp) => imp.default as ApiDiffHashes); } /** @@ -34,7 +37,7 @@ async function loadLocal(path: string): Promise<DocsApiDiffIndex> { * * @param source The source to load the diff index from. */ -async function load(source: string): Promise<DocsApiDiffIndex> { +async function load(source: string): Promise<ApiDiffHashes> { return source.startsWith('https://') ? loadRemote(source) : loadLocal(source); } @@ -58,8 +61,8 @@ function allKeys( * @param sourceDiffIndex The path to the source (changed) index. Defaults to the local diff index. */ export async function diff( - targetDiffIndex = `https://next.fakerjs.dev/${nameDocsDiffIndexFile}`, - sourceDiffIndex = `file://${pathDocsDiffIndexFile}` + targetDiffIndex = `https://next.fakerjs.dev/${FILE_NAME_DOCS_DIFF_INDEX}`, + sourceDiffIndex = `file://${FILE_PATH_DOCS_DIFF_INDEX}` ): Promise<Record<string, ['ADDED'] | ['REMOVED'] | string[]>> { const target = await load(targetDiffIndex); const source = await load(sourceDiffIndex); diff --git a/scripts/apidocs/generate.ts b/scripts/apidocs/generate.ts new file mode 100644 index 00000000..81021c5f --- /dev/null +++ b/scripts/apidocs/generate.ts @@ -0,0 +1,45 @@ +import type { Project } from 'ts-morph'; +import { writeDiffIndex } from './output/diff-index'; +import { writePages } from './output/page'; +import { writePageIndex } from './output/page-index'; +import { writeSearchIndex } from './output/search-index'; +import { writeSourceBaseUrl } from './output/source-base-url'; +import type { RawApiDocsPage } from './processing/class'; +import { + processModuleClasses, + processProjectClasses, + processProjectInterfaces, + processProjectUtilities, +} from './processing/class'; +import { getProject } from './project'; + +export async function generate(): Promise<void> { + console.log('Reading project'); + const project = getProject(); + console.log('Processing components'); + const apiDocsPages = processComponents(project); + console.log('Writing files'); + await writeFiles(apiDocsPages); +} + +export function processComponents(project: Project): RawApiDocsPage[] { + return [ + ...processProjectClasses(project), + ...processProjectInterfaces(project), + processProjectUtilities(project), + ...processModuleClasses(project), + ]; +} + +async function writeFiles(apiDocsPages: RawApiDocsPage[]): Promise<void> { + console.log('- diff index'); + writeDiffIndex(apiDocsPages); + console.log('- page index'); + await writePageIndex(apiDocsPages); + console.log('- pages'); + await writePages(apiDocsPages); + console.log('- search index'); + writeSearchIndex(apiDocsPages); + console.log('- source base url'); + await writeSourceBaseUrl(); +} diff --git a/scripts/apidocs/output/constants.ts b/scripts/apidocs/output/constants.ts new file mode 100644 index 00000000..ec579e3c --- /dev/null +++ b/scripts/apidocs/output/constants.ts @@ -0,0 +1 @@ +export const SCRIPT_COMMAND = 'pnpm run generate:api-docs'; diff --git a/scripts/apidocs/output/diff-index.ts b/scripts/apidocs/output/diff-index.ts new file mode 100644 index 00000000..f32c5dfe --- /dev/null +++ b/scripts/apidocs/output/diff-index.ts @@ -0,0 +1,81 @@ +import { createHash } from 'node:crypto'; +import { writeFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import type { RawApiDocsPage } from '../processing/class'; +import type { RawApiDocsMethod } from '../processing/method'; +import { FILE_PATH_PUBLIC } from '../utils/paths'; + +export const FILE_NAME_DOCS_DIFF_INDEX = 'api-diff-index.json'; +export const FILE_PATH_DOCS_DIFF_INDEX = resolve( + FILE_PATH_PUBLIC, + FILE_NAME_DOCS_DIFF_INDEX +); + +/** + * The diff hashes for the entire api. + */ +export interface ApiDiffHashes { + /** + * The pages with their diff hashes. + */ + [pages: string]: ApiPageDiffHashes; +} + +/** + * The diff hashes for a single api doc page. + */ +export interface ApiPageDiffHashes { + /** + * The checksum of the entire page. + */ + pageHash: string; + /** + * The checksum of the method by name. + */ + [method: string]: string; +} + +/** + * Writes the api diff index to the correct location. + * + * @param pages The pages to write into the index. + */ +export function writeDiffIndex(pages: RawApiDocsPage[]): void { + const diffIndex: ApiDiffHashes = Object.fromEntries( + pages.map((page) => [page.title, pageDiffHashes(page)]) + ); + writeFileSync(FILE_PATH_DOCS_DIFF_INDEX, JSON.stringify(diffIndex)); +} + +function pageDiffHashes(page: RawApiDocsPage): ApiPageDiffHashes { + return { + pageHash: diffHash({ + ...page, + methods: undefined, + } satisfies Partial<RawApiDocsPage>), + ...Object.fromEntries( + page.methods.map((method) => [method.name, methodDiffHash(method)]) + ), + }; +} + +/** + * Creates a diff hash for the given method by removing the line number from the source path. + * + * @param method The method to create a hash for. + */ +function methodDiffHash(method: RawApiDocsMethod): string { + return diffHash({ + ...method, + source: method.source.filePath, + } satisfies Record<keyof RawApiDocsMethod, unknown>); +} + +/** + * Creates a diff hash for the given object. + * + * @param object The object to create a hash for. + */ +function diffHash(object: unknown): string { + return createHash('md5').update(JSON.stringify(object)).digest('hex'); +} diff --git a/scripts/apidocs/output/page-index.ts b/scripts/apidocs/output/page-index.ts new file mode 100644 index 00000000..b676ca24 --- /dev/null +++ b/scripts/apidocs/output/page-index.ts @@ -0,0 +1,38 @@ +import { writeFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import type { DefaultTheme } from 'vitepress'; +import { groupBy } from '../../../src/internal/group-by'; +import type { RawApiDocsPage } from '../processing/class'; +import { formatTypescript } from '../utils/format'; +import { FILE_PATH_DOCS } from '../utils/paths'; +import { SCRIPT_COMMAND } from './constants'; + +const pathDocsApiPages = resolve(FILE_PATH_DOCS, '.vitepress', 'api-pages.ts'); + +/** + * Writes the api docs index to correct location. + * + * @param pages The pages to write into the index. + */ +export async function writePageIndex(pages: RawApiDocsPage[]): Promise<void> { + const pagesByCategory: Record<string, DefaultTheme.SidebarItem[]> = groupBy( + pages, + (page) => page.category ?? '', + ({ title: text, camelTitle }) => ({ text, link: `/api/${camelTitle}.html` }) + ); + const pageTree = Object.entries(pagesByCategory).flatMap( + ([category, items]) => (category ? [{ text: category, items }] : items) + ); + + // Write api-pages.ts + pageTree.unshift({ text: 'Overview', link: '/api/' }); + let apiPagesContent = ` + // This file is automatically generated. + // Run '${SCRIPT_COMMAND}' to update + export const apiPages = ${JSON.stringify(pageTree)}; + `.replace(/\n +/, '\n'); + + apiPagesContent = await formatTypescript(apiPagesContent); + + writeFileSync(pathDocsApiPages, apiPagesContent); +} diff --git a/scripts/apidocs/output/page.ts b/scripts/apidocs/output/page.ts new file mode 100644 index 00000000..ae1c3c4f --- /dev/null +++ b/scripts/apidocs/output/page.ts @@ -0,0 +1,172 @@ +import { writeFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import type { ApiDocsMethod } from '../../../docs/.vitepress/components/api-docs/method'; +import type { RawApiDocsPage } from '../processing/class'; +import type { RawApiDocsMethod } from '../processing/method'; +import { formatMarkdown } from '../utils/format'; +import { adjustUrls, codeToHtml, mdToHtml } from '../utils/markdown'; +import { FILE_PATH_API_DOCS } from '../utils/paths'; +import { required } from '../utils/value-checks'; +import { SCRIPT_COMMAND } from './constants'; + +// Extracted to a constant because the contents must not be formatted by prettier +const vitePressInFileOptions = `--- +editLink: false +--- + +`; + +/** + * Writes the api docs page and data for the given modules to the correct location. + * + * @param pages The pages to write. + */ +export async function writePages(pages: RawApiDocsPage[]): Promise<void> { + await Promise.all(pages.map(writePage)); +} + +/** + * Writes the api docs page and data for the given module to the correct location. + * + * @param page The page to write. + */ +async function writePage(page: RawApiDocsPage): Promise<void> { + try { + await writePageMarkdown(page); + writePageJsonData(page); + } catch (error) { + throw new Error(`Error writing page ${page.title}`, { cause: error }); + } +} + +/** + * Writes the api docs page for the given module to the correct location. + * + * @param page The page to write. + */ +async function writePageMarkdown(page: RawApiDocsPage): Promise<void> { + const { title, camelTitle, deprecated, description, examples, methods } = + page; + // Write api docs page + let content = ` + <script setup> + import ApiDocsMethod from '../.vitepress/components/api-docs/method.vue'; + import ${camelTitle} from './${camelTitle}.json'; + </script> + + <!-- This file is automatically generated. --> + <!-- Run '${SCRIPT_COMMAND}' to update --> + + # ${title} + + ::: v-pre + + ${ + deprecated == null + ? '' + : `<div class="warning custom-block"> + <p class="custom-block-title">Deprecated</p> + <p>This module is deprecated and will be removed in a future version.</p> + <span>${deprecated}</span> + </div>` + } + + ${adjustUrls(description)} + + ${examples.length === 0 ? '' : `<div class="examples">${codeToHtml(examples.join('\n'))}</div>`} + + ::: + + ${methods + .map( + (method) => ` + ## ${method.name} + + <ApiDocsMethod :method="${camelTitle}.${method.name}" v-once /> + ` + ) + .join('')} + `.replaceAll(/\n +/g, '\n'); + + content = vitePressInFileOptions + (await formatMarkdown(content)); + + writeFileSync(resolve(FILE_PATH_API_DOCS, `${camelTitle}.md`), content); +} + +/** + * Writes the api docs data for the given module to correct location. + * + * @param page The page to write. + */ +function writePageJsonData(page: RawApiDocsPage): void { + const { camelTitle, methods } = page; + const pageData: Record<string, ApiDocsMethod> = Object.fromEntries( + methods.map((method) => [method.name, toMethodData(method)]) + ); + const content = JSON.stringify(pageData, null, 2); + + writeFileSync(resolve(FILE_PATH_API_DOCS, `${camelTitle}.json`), content); +} + +const defaultCommentRegex = /\s+Defaults to `([^`]+)`\..*/; + +function toMethodData(method: RawApiDocsMethod): ApiDocsMethod { + const { name, signatures, source } = method; + const signatureData = required(signatures.at(-1), 'method signature'); + const { + deprecated, + description, + since, + parameters, + returns, + throws, + signature, + examples, + seeAlsos, + } = signatureData; + const { filePath, line } = source; + + /* Target order, omitted to improve diff to old files + return { + name, + deprecated: mdToHtml(deprecated), + description: mdToHtml(description), + since, + parameters: parameters.map((param) => ({ + ...param, + type: param.type.text, + default: + param.default ?? defaultCommentRegex.exec(param.description)?.[1], + description: mdToHtml(param.description.replace(defaultCommentRegex, '')), + })), + returns: returns.text, + throws: throws.length === 0 ? undefined : mdToHtml(throws.join('\n'), true), + // signature: codeToHtml(signature), + examples: codeToHtml([signature, ...examples].join('\n')), + seeAlsos: seeAlsos.map((seeAlso) => mdToHtml(seeAlso, true)), + sourcePath: sourcePath.replace(/:(\d+):\d+/g, '#L$1'), + }; + */ + + return { + name, + description: mdToHtml(description), + parameters: parameters.map((param) => ({ + ...param, + type: param.type.text, + default: param.default ?? extractSummaryDefault(param.description), + description: mdToHtml(param.description.replace(defaultCommentRegex, '')), + })), + since, + sourcePath: `${filePath}#L${line}`, + throws: throws.length === 0 ? undefined : mdToHtml(throws.join('\n'), true), + returns: returns.text, + examples: codeToHtml([signature, ...examples].join('\n')), + deprecated: mdToHtml(deprecated), + seeAlsos: seeAlsos.map((seeAlso) => mdToHtml(seeAlso, true)), + }; +} + +export function extractSummaryDefault(description: string): string | undefined { + return defaultCommentRegex.exec(description)?.[1]; +} diff --git a/scripts/apidocs/output/search-index.ts b/scripts/apidocs/output/search-index.ts new file mode 100644 index 00000000..9bff4a4e --- /dev/null +++ b/scripts/apidocs/output/search-index.ts @@ -0,0 +1,34 @@ +import { writeFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import type { APIGroup } from '../../../docs/api/api-types'; +import type { RawApiDocsPage } from '../processing/class'; +import { FILE_PATH_API_DOCS } from '../utils/paths'; + +const pathDocsApiSearchIndex = resolve( + FILE_PATH_API_DOCS, + 'api-search-index.json' +); + +/** + * Writes the api search index to the correct location. + * + * @param pages The pages to write into the index. + */ +export function writeSearchIndex(pages: RawApiDocsPage[]): void { + const apiIndex: APIGroup[] = [ + { + text: 'Module API', + items: pages.map((page) => ({ + text: page.title, + link: `/api/${page.camelTitle}.html`, + headers: page.methods.map((method) => ({ + anchor: method.name, + text: method.name, + deprecated: method.signatures.every((s) => !!s.deprecated), + })), + })), + }, + ]; + + writeFileSync(pathDocsApiSearchIndex, JSON.stringify(apiIndex)); +} diff --git a/scripts/apidocs/output/source-base-url.ts b/scripts/apidocs/output/source-base-url.ts new file mode 100644 index 00000000..ca6df85a --- /dev/null +++ b/scripts/apidocs/output/source-base-url.ts @@ -0,0 +1,38 @@ +import { execSync } from 'node:child_process'; +import { writeFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { formatTypescript } from '../utils/format'; +import { FILE_PATH_API_DOCS } from '../utils/paths'; +import { SCRIPT_COMMAND } from './constants'; + +const pathSourceBaseUrlFile = resolve(FILE_PATH_API_DOCS, 'source-base-url.ts'); + +/** + * Writes the source base url to the correct location. + */ +export async function writeSourceBaseUrl(): Promise<void> { + const baseUrl = getSourceBaseUrl(); + + let content = ` + // This file is automatically generated. + // Run '${SCRIPT_COMMAND}' to update + export const sourceBaseUrl = '${baseUrl}'; + `.replace(/\n +/, '\n'); + + content = await formatTypescript(content); + + writeFileSync(pathSourceBaseUrlFile, content); +} + +function getSourceBaseUrl(): string { + return `https://github.com/faker-js/faker/blob/${getCommitHash() || 'next'}/`; +} + +function getCommitHash(): string | undefined { + try { + return execSync('git rev-parse --verify HEAD').toString('utf8').trim(); + } catch (error) { + console.warn('Failed to get commit hash', error); + return undefined; + } +} diff --git a/scripts/apidocs/processing/class.ts b/scripts/apidocs/processing/class.ts new file mode 100644 index 00000000..5af5e874 --- /dev/null +++ b/scripts/apidocs/processing/class.ts @@ -0,0 +1,223 @@ +import type { ClassDeclaration, InterfaceDeclaration, Project } from 'ts-morph'; +import { required, valuesForKeys } from '../utils/value-checks'; +import { newProcessingError } from './error'; +import type { JSDocableLikeNode } from './jsdocs'; +import { + getDeprecated, + getDescription, + getExamples, + getJsDocs, +} from './jsdocs'; +import type { RawApiDocsMethod } from './method'; +import { + processClassConstructors, + processClassMethods, + processInterfaceMethods, + processProjectFunctions, +} from './method'; + +/** + * Represents a raw page in the API docs. + */ +export interface RawApiDocsPage { + /** + * The title of the page as shown to users. + */ + title: string; + /** + * The title of the page in camel case as used in paths. + */ + camelTitle: string; + /** + * The category of the page, if it has one. + */ + category: string | undefined; + /** + * The deprecation notice of the page, if it has one. + */ + deprecated: string | undefined; + /** + * The description of the page. + */ + description: string; + /** + * The usage examples of the elements on the page. + */ + examples: string[]; + /** + * The api methods on the page. + */ + methods: RawApiDocsMethod[]; +} + +// Classes + +function getAllClasses( + project: Project, + filter: (name: string) => boolean = () => true +): Record<string, ClassDeclaration> { + return Object.fromEntries( + project + .getSourceFiles() + .flatMap((file) => file.getClasses()) + .map((clazz) => [clazz.getNameOrThrow(), clazz] as const) + .filter(([name]) => filter(name)) + ); +} + +export function processProjectClasses(project: Project): RawApiDocsPage[] { + return processClasses( + valuesForKeys(getAllClasses(project), ['Faker', 'SimpleFaker']) + ); +} + +function processClasses(classes: ClassDeclaration[]): RawApiDocsPage[] { + return classes.map((clazz) => { + try { + return processClass(clazz); + } catch (error) { + throw newProcessingError({ + type: 'class', + name: clazz.getNameOrThrow(), + source: clazz, + cause: error, + }); + } + }); +} + +export function processClass(clazz: ClassDeclaration): RawApiDocsPage { + const result = processModule(clazz); + result.methods.unshift(...processClassConstructors(clazz)); + return result; +} + +// Modules + +export function processModuleClasses(project: Project): RawApiDocsPage[] { + return processModules( + Object.values( + getAllClasses( + project, + (module: string): boolean => + module.endsWith('Module') && !module.startsWith('Simple') + ) + ).sort((a, b) => a.getNameOrThrow().localeCompare(b.getNameOrThrow())) + ); +} + +function processModules(modules: ClassDeclaration[]): RawApiDocsPage[] { + return modules.map((module) => { + try { + return processModule(module, 'Modules'); + } catch (error: unknown) { + throw newProcessingError({ + type: 'module', + name: getModuleName(module), + source: module, + cause: error, + }); + } + }); +} + +function processModule( + module: ClassDeclaration, + category: string | undefined = undefined +): RawApiDocsPage { + const title = getModuleName(module); + + return { + ...preparePage(module, title, category), + methods: processClassMethods(module), + }; +} + +function getModuleName(module: ClassDeclaration): string { + return required(module.getName(), 'module name').replace(/Module$/, ''); +} + +// Interfaces + +function getAllInterfaces( + project: Project +): Record<string, InterfaceDeclaration> { + return Object.fromEntries( + project + .getSourceFiles() + .flatMap((file) => file.getInterfaces()) + .map((iface) => [iface.getName(), iface] as const) + ); +} + +export function processProjectInterfaces(project: Project): RawApiDocsPage[] { + return processInterfaces( + valuesForKeys(getAllInterfaces(project), ['Randomizer']) + ); +} + +function processInterfaces( + interfaces: InterfaceDeclaration[] +): RawApiDocsPage[] { + return interfaces.map((iface) => { + try { + return processInterface(iface); + } catch (error) { + throw newProcessingError({ + type: 'interface', + name: iface.getName(), + source: iface, + cause: error, + }); + } + }); +} + +function processInterface(iface: InterfaceDeclaration): RawApiDocsPage { + return { + ...preparePage(iface, iface.getName()), + methods: processInterfaceMethods(iface), + }; +} + +// Utilities + +export function processProjectUtilities(project: Project): RawApiDocsPage { + console.log(`- Utilities`); + + return { + title: 'Utilities', + camelTitle: 'utils', + category: undefined, + deprecated: undefined, + description: 'A list of all the utilities available in Faker.js.', + examples: [], + methods: processProjectFunctions(project, 'mergeLocales'), + }; +} + +// Helpers + +function preparePage( + module: JSDocableLikeNode, + title: string, + category: string | undefined = undefined +): RawApiDocsPage { + console.log(`- ${title}`); + + const jsdocs = getJsDocs(module); + + return { + title, + camelTitle: toCamelCase(title), + category, + deprecated: getDeprecated(jsdocs), + description: getDescription(jsdocs), + examples: getExamples(jsdocs), + methods: [], + }; +} + +function toCamelCase(value: string): string { + return value.substring(0, 1).toLowerCase() + value.substring(1); +} diff --git a/scripts/apidocs/processing/error.ts b/scripts/apidocs/processing/error.ts new file mode 100644 index 00000000..f171d6b4 --- /dev/null +++ b/scripts/apidocs/processing/error.ts @@ -0,0 +1,40 @@ +import { FakerError } from '../../../src/errors/faker-error'; +import type { SourceableNode } from './source'; +import { getSourcePath } from './source'; + +export class FakerApiDocsProcessingError extends FakerError { + constructor(options: { + type: string; + name: string; + source: string | SourceableNode; + cause: unknown; + }) { + const { type, name, source, cause } = options; + const sourceText = + typeof source === 'string' ? source : getSourcePathText(source); + const causeText = cause instanceof Error ? cause.message : ''; + super(`Failed to process ${type} ${name} at ${sourceText} : ${causeText}`, { + cause, + }); + } +} + +export function newProcessingError(options: { + type: string; + name: string; + source: string | SourceableNode; + cause: unknown; +}): FakerApiDocsProcessingError { + const { cause } = options; + + if (cause instanceof FakerApiDocsProcessingError) { + return cause; + } + + return new FakerApiDocsProcessingError(options); +} + +function getSourcePathText(source: SourceableNode): string { + const { filePath, line, column } = getSourcePath(source); + return `${filePath}:${line}:${column}`; +} diff --git a/scripts/apidocs/processing/jsdocs.ts b/scripts/apidocs/processing/jsdocs.ts new file mode 100644 index 00000000..4a8e5b08 --- /dev/null +++ b/scripts/apidocs/processing/jsdocs.ts @@ -0,0 +1,91 @@ +import type { JSDoc, JSDocTag, JSDocableNode } from 'ts-morph'; +import { JSDocParameterTag, JSDocTemplateTag } from 'ts-morph'; +import { + allRequired, + exactlyOne, + optionalOne, + required, +} from '../utils/value-checks'; + +export type JSDocableLikeNode = Pick<JSDocableNode, 'getJsDocs'>; + +export function getJsDocs(node: JSDocableLikeNode): JSDoc { + return exactlyOne(node.getJsDocs(), 'jsdocs'); +} + +export function getDeprecated(jsdocs: JSDoc): string | undefined { + return getOptionalTagFromJSDoc(jsdocs, 'deprecated'); +} + +export function getDescription(jsdocs: JSDoc | JSDocTag): string { + return required(jsdocs.getCommentText(), 'jsdocs description'); +} + +export function getSince(jsdocs: JSDoc): string { + return getExactlyOneTagFromJSDoc(jsdocs, 'since'); +} + +export function getTypeParameterTags(jsdocs: JSDoc): Record<string, JSDocTag> { + return Object.fromEntries( + jsdocs + .getTags() + .filter((tag) => tag.getTagName() === 'template') + .filter((tag) => tag instanceof JSDocTemplateTag) + .map((tag) => tag as JSDocTemplateTag) + .map((tag) => [tag.getTypeParameters()[0].getName(), tag] as const) + ); +} + +export function getParameterTags(jsdocs: JSDoc): Record<string, JSDocTag> { + return Object.fromEntries( + jsdocs + .getTags() + .filter((tag) => tag.getTagName() === 'param') + .filter((tag) => tag instanceof JSDocParameterTag) + .map((tag) => tag as JSDocParameterTag) + .map((tag) => [tag.getName(), tag] as const) + ); +} + +export function getDefault(jsdocs: JSDoc): string | undefined { + return getOptionalTagFromJSDoc(jsdocs, `default`); +} + +export function getThrows(jsdocs: JSDoc): string[] { + return getTagsFromJSDoc(jsdocs, 'throws'); +} + +export function getExamples(jsdocs: JSDoc): string[] { + return getTagsFromJSDoc(jsdocs, 'example'); +} + +export function getSeeAlsos(jsdocs: JSDoc): string[] { + return getTagsFromJSDoc(jsdocs, 'see', true); +} + +function getOptionalTagFromJSDoc( + jsdocs: JSDoc, + type: string +): string | undefined { + return optionalOne(getTagsFromJSDoc(jsdocs, type), `@${type}`); +} + +function getExactlyOneTagFromJSDoc(jsdocs: JSDoc, type: string): string { + return exactlyOne(getTagsFromJSDoc(jsdocs, type), `@${type}`); +} + +function getTagsFromJSDoc( + jsdocs: JSDoc, + type: string, + full: boolean = false +): string[] { + return allRequired( + jsdocs + .getTags() + .filter((tag) => tag.getTagName() === type) + .map((tag) => + full ? tag.getStructure().text?.toString() : tag.getCommentText() + ), + `@${type}` + ); +} diff --git a/scripts/apidocs/processing/method.ts b/scripts/apidocs/processing/method.ts new file mode 100644 index 00000000..b9d1aa38 --- /dev/null +++ b/scripts/apidocs/processing/method.ts @@ -0,0 +1,197 @@ +import type { + ClassDeclaration, + FunctionDeclaration, + InterfaceDeclaration, + MethodSignature, + Project, +} from 'ts-morph'; +import { + SyntaxKind, + type ConstructorDeclaration, + type MethodDeclaration, +} from 'ts-morph'; +import { groupBy } from '../../../src/internal/group-by'; +import { valuesForKeys } from '../utils/value-checks'; +import { newProcessingError } from './error'; +import type { + RawApiDocsSignature, + SignatureLikeDeclaration, +} from './signature'; +import { processSignatures } from './signature'; +import type { RawApiDocsSource } from './source'; +import { getSourcePath as getSource } from './source'; + +/** + * Represents a method in the raw API docs. + */ +export interface RawApiDocsMethod { + /** + * The name of the method. + */ + name: string; + /** + * The signatures of the method. + */ + signatures: RawApiDocsSignature[]; + /** + * The source of the method. + */ + source: RawApiDocsSource; +} + +// Constructors + +export function processClassConstructors( + clazz: ClassDeclaration +): RawApiDocsMethod[] { + return processConstructors(clazz.getConstructors()); +} + +function processConstructors( + constructors: ConstructorDeclaration[] +): RawApiDocsMethod[] { + return processMethodLikes(constructors, () => 'constructor'); +} + +// Class Methods + +export function processClassMethods( + clazz: ClassDeclaration +): RawApiDocsMethod[] { + return processMethods(getAllMethods(clazz)); +} + +function getAllMethods(clazz: ClassDeclaration): MethodDeclaration[] { + const parents: ClassDeclaration[] = [clazz]; + let parent: ClassDeclaration | undefined = clazz; + while ((parent = parent.getBaseClass()) != null) { + parents.unshift(parent); + } + + const methods: Record<string, MethodDeclaration> = {}; + + for (const parent of parents) { + for (const method of parent.getMethods()) { + methods[method.getName()] = method; + } + } + + return Object.values(methods).sort((a, b) => + a.getName().localeCompare(b.getName()) + ); +} + +type NamedMethodLikeDeclaration = MethodLikeDeclaration & + Pick<MethodDeclaration, 'getName'>; + +function processMethods( + methods: NamedMethodLikeDeclaration[] +): RawApiDocsMethod[] { + return processMethodLikes(methods, (v) => v.getName()); +} + +// Interface Methods + +export function processInterfaceMethods( + iface: InterfaceDeclaration +): RawApiDocsMethod[] { + return processMethodSignatures(iface.getMethods()); +} + +function processMethodSignatures( + methods: MethodSignature[] +): RawApiDocsMethod[] { + const groupedSignatures = groupBy(methods, (v) => v.getName()); + + const methodLikes: NamedMethodLikeDeclaration[] = Object.values( + groupedSignatures + ).map((signatures) => { + const signature = signatures[0]; + + return { + getName: () => signature.getName(), + hasModifier: () => false, + getOverloads: () => signatures, + getTypeParameters: () => signature.getTypeParameters(), + getParameters: () => signature.getParameters(), + getReturnType: () => signature.getReturnType(), + getJsDocs: () => signature.getJsDocs(), + getSourceFile: () => signature.getSourceFile(), + getStart: () => signature.getStart(), + getText: () => signature.getText(), + }; + }); + + return processMethods(methodLikes); +} + +// Functions + +function getAllFunctions( + project: Project +): Record<string, FunctionDeclaration> { + return Object.fromEntries( + project + .getSourceFiles() + .flatMap((file) => file.getFunctions()) + .map((fn) => [fn.getNameOrThrow(), fn] as const) + ); +} + +export function processProjectFunctions( + project: Project, + ...names: string[] +): RawApiDocsMethod[] { + return processMethodLikes( + valuesForKeys(getAllFunctions(project), names), + (f) => f.getNameOrThrow() + ); +} + +// Method-likes + +type MethodLikeDeclaration = SignatureLikeDeclaration & + Pick<MethodDeclaration, 'hasModifier'> & { + getOverloads(): SignatureLikeDeclaration[]; + }; + +function processMethodLikes<T extends MethodLikeDeclaration>( + methods: T[], + nameResolver: (value: T) => string +): RawApiDocsMethod[] { + return methods + .filter((method) => !method.hasModifier(SyntaxKind.PrivateKeyword)) + .map((method) => { + const name = nameResolver(method); + try { + return processMethodLike(name, method); + } catch (error) { + throw newProcessingError({ + type: 'method', + name, + source: method, + cause: error, + }); + } + }) + .sort((a, b) => a.name.localeCompare(b.name)); +} + +export function processMethodLike( + name: string, + method: MethodLikeDeclaration +): RawApiDocsMethod { + console.log(` - ${name}`); + const overloads = method.getOverloads(); + const signatureData: SignatureLikeDeclaration[] = + overloads.length > 0 ? overloads : [method]; + + const signatures = processSignatures(name, signatureData, method); + const source = getSource(method); + + return { + name, + signatures, + source, + }; +} diff --git a/scripts/apidocs/processing/parameter.ts b/scripts/apidocs/processing/parameter.ts new file mode 100644 index 00000000..7a6b67da --- /dev/null +++ b/scripts/apidocs/processing/parameter.ts @@ -0,0 +1,203 @@ +import type { + PropertySignature, + Type, + TypeParameterDeclaration, +} from 'ts-morph'; +import { type JSDoc, type JSDocTag, type ParameterDeclaration } from 'ts-morph'; +import { exactlyOne, valueForKey } from '../utils/value-checks'; +import { newProcessingError } from './error'; +import { + getDefault, + getDeprecated, + getDescription, + getJsDocs, + getParameterTags, + getTypeParameterTags, +} from './jsdocs'; +import type { RawApiDocsType } from './type'; +import { getNameSuffix, getTypeText, isOptionsLikeType } from './type'; + +/** + * Represents a parameter in the raw API docs. + */ +export interface RawApiDocsParameter { + /** + * The name of the parameter. + */ + name: string; + /** + * The type of the parameter. + */ + type: RawApiDocsType; + /** + * The default value or expression of the parameter, if it has one. + */ + default: string | undefined; + /** + * The description of the parameter. + */ + description: string; +} + +export function processTypeParameters( + parameters: TypeParameterDeclaration[], + jsdocs: JSDoc +): RawApiDocsParameter[] { + const paramTags = getTypeParameterTags(jsdocs); + + return parameters.flatMap((parameter) => { + try { + return processTypeParameterEntry(parameter, paramTags); + } catch (error) { + throw newProcessingError({ + type: 'type parameter', + name: parameter.getName(), + source: parameter, + cause: error, + }); + } + }); +} + +function processTypeParameterEntry( + parameter: TypeParameterDeclaration, + paramTags: Record<string, JSDocTag> +): RawApiDocsParameter { + return { + name: `<${parameter.getName()}>`, + type: getTypeText(parameter.getType(), { resolveAliases: true }), + default: parameter.getDefault()?.getText(), + description: getDescription(valueForKey(paramTags, parameter.getName())), + }; +} + +export function processParameters( + signatureParameters: ParameterDeclaration[], + implParameters: ParameterDeclaration[], + jsdocs: JSDoc +): RawApiDocsParameter[] { + const paramTags = getParameterTags(jsdocs); + const implParameterDefaults = Object.fromEntries( + implParameters.map((parameter) => [ + parameter.getName(), + getDefaultValue(parameter), + ]) + ); + + return signatureParameters.flatMap((parameter) => { + try { + return processParameter( + parameter, + paramTags, + implParameterDefaults[parameter.getName()] + ); + } catch (error) { + throw newProcessingError({ + type: 'parameter', + name: parameter.getName(), + source: parameter, + cause: error, + }); + } + }); +} + +function processParameter( + parameter: ParameterDeclaration, + paramTags: Record<string, JSDocTag>, + implementationDefault: string | undefined +): RawApiDocsParameter[] { + const name = parameter.getName(); + return [ + processSimpleParameter( + parameter, + valueForKey(paramTags, name), + implementationDefault + ), + ...processComplexParameter(name, parameter.getType()), + ]; +} + +type ParameterLikeDeclaration = Pick< + ParameterDeclaration, + 'getName' | 'getType' +> & + Partial<Pick<ParameterDeclaration, 'getInitializer'>>; + +function processSimpleParameter( + parameter: ParameterLikeDeclaration, + jsdocTag: JSDocTag, + implementationDefault: string | undefined +): RawApiDocsParameter { + const name = parameter.getName(); + const type = parameter.getType(); + return { + name: `${name}${getNameSuffix(type)}`, + type: getTypeText(type, { + abbreviate: true, + stripUndefined: true, + }), + default: getDefaultValue(parameter) ?? implementationDefault, + description: getDescription(jsdocTag), + }; +} + +function getDefaultValue( + parameter: ParameterLikeDeclaration +): string | undefined { + return parameter + .getInitializer?.() + ?.getText() + .replace(/ as .+$/, ''); +} + +function processComplexParameter( + name: string, + type: Type +): RawApiDocsParameter[] { + if (type.isNullable()) { + return processComplexParameter(name, type.getNonNullableType()); + } else if (type.isUnion()) { + return type + .getUnionTypes() + .flatMap((unionType) => processComplexParameter(name, unionType)); + } else if (type.isArray()) { + return processComplexParameter( + `${name}[]`, + type.getArrayElementTypeOrThrow() + ); + } else if (type.isObject()) { + if (!isOptionsLikeType(type)) { + return []; + } + + return type + .getApparentProperties() + .flatMap((parameter) => { + const declaration = exactlyOne( + parameter.getDeclarations(), + 'property declaration' + ) as PropertySignature; + const propertyType = declaration.getType(); + const jsdocs = getJsDocs(declaration); + const deprecated = getDeprecated(jsdocs); + + return [ + { + name: `${name}.${parameter.getName()}${getNameSuffix(propertyType)}`, + type: getTypeText(propertyType, { + abbreviate: false, + stripUndefined: true, + }), + default: getDefault(jsdocs), + description: + getDescription(jsdocs) + + (deprecated ? `\n\n**DEPRECATED:** ${deprecated}` : ''), + }, + ]; + }) + .sort((a, b) => a.name.localeCompare(b.name)); + } + + return []; +} diff --git a/scripts/apidocs/processing/signature.ts b/scripts/apidocs/processing/signature.ts new file mode 100644 index 00000000..c7ae872b --- /dev/null +++ b/scripts/apidocs/processing/signature.ts @@ -0,0 +1,158 @@ +import type { MethodDeclaration } from 'ts-morph'; +import { getProject } from '../project'; +import { exactlyOne } from '../utils/value-checks'; +import { newProcessingError } from './error'; +import type { JSDocableLikeNode } from './jsdocs'; +import { + getDeprecated, + getDescription, + getExamples, + getJsDocs, + getSeeAlsos, + getSince, + getThrows, +} from './jsdocs'; +import type { RawApiDocsParameter } from './parameter'; +import { processParameters, processTypeParameters } from './parameter'; +import type { SourceableNode } from './source'; +import type { RawApiDocsType } from './type'; +import { getTypeText } from './type'; + +/** + * Represents a method signature in the raw API docs. + */ +export interface RawApiDocsSignature { + /** + * The deprecation notice of the signature, if it has one. + */ + deprecated: string | undefined; + /** + * The description of the signature. + */ + description: string; + /** + * The version when the signature was added. + */ + since: string; + /** + * The parameters of the signature. + */ + parameters: RawApiDocsParameter[]; + /** + * The return type of the signature. + */ + returns: RawApiDocsType; + /** + * The exceptions thrown by the signature. + */ + throws: string[]; + /** + * The full call signature as text. + */ + signature: string; + /** + * The usage examples of the signature. + */ + examples: string[]; + /** + * The see also links of the signature. + */ + seeAlsos: string[]; +} + +export type SignatureLikeDeclaration = Pick< + MethodDeclaration, + 'getTypeParameters' | 'getParameters' | 'getReturnType' | 'getText' +> & + JSDocableLikeNode & + SourceableNode; + +export function processSignatures( + name: string, + signatures: SignatureLikeDeclaration[], + implementation: SignatureLikeDeclaration +): RawApiDocsSignature[] { + return signatures.map((signature, i) => { + try { + return processSignature(signature, implementation); + } catch (error) { + throw newProcessingError({ + type: 'signature', + name: `${name}/${i}`, + source: signature, + cause: error, + }); + } + }); +} + +function processSignature( + signature: SignatureLikeDeclaration, + implementation: SignatureLikeDeclaration +): RawApiDocsSignature { + const jsdocs = getJsDocs(signature); + const parameters = [ + ...processTypeParameters(signature.getTypeParameters(), jsdocs), + ...processParameters( + signature.getParameters(), + implementation.getParameters(), + jsdocs + ), + ]; + const returns = getTypeText(signature.getReturnType()); + + try { + return { + deprecated: getDeprecated(jsdocs), + description: getDescription(jsdocs), + since: getSince(jsdocs), + parameters, + returns, + throws: getThrows(jsdocs), + signature: getSignatureText(signature), + examples: getExamples(jsdocs), + seeAlsos: getSeeAlsos(jsdocs), + }; + } catch (error) { + throw newProcessingError({ + type: 'jsdocs', + name: signature.getText(), + source: jsdocs, + cause: error, + }); + } +} + +// Cache the project for performance reasons +const signatureExtractionProject = getProject({ + skipAddingFilesFromTsConfig: true, +}); + +function getSignatureText(signature: SignatureLikeDeclaration): string { + const fullText = signature + .getText() + // Remove all jsdocs + .replaceAll(/ *\/\*\*[^\n]*\n(\s*\*[^\n]*\n)*\s*\*\/\n/g, '') + // Remove all empty lines + .replaceAll(/\n\n+/g, '\n') + // Remove the export function keyword for consistency with member methods + .replace(/^export function /, ''); + + // Is this already a signature + if (fullText.endsWith(';')) { + // Restore the function keyword + return `function ${fullText}`; + } + + // Create a copy of the signature to keep the line numbers unchanged + // and for performance reasons, as removing and re-adding the body is slow. + // We use a function here to avoid unnecessary boilerplate + const fn = exactlyOne( + signatureExtractionProject + .createSourceFile('temp.ts', `function ${fullText}`, { overwrite: true }) + .getFunctions(), + 'function signature' + ); + fn.removeBody(); + return fn.getText(); +} diff --git a/scripts/apidocs/processing/source.ts b/scripts/apidocs/processing/source.ts new file mode 100644 index 00000000..ac23c9fb --- /dev/null +++ b/scripts/apidocs/processing/source.ts @@ -0,0 +1,37 @@ +import type { Node } from 'ts-morph'; +import { FILE_PATH_PROJECT } from '../utils/paths'; + +/** + * Represents a source element in the raw API docs. + */ +export interface RawApiDocsSource { + /** + * The file path of the target element. + */ + filePath: string; + /** + * The line number of the target element. + */ + line: number; + /** + * The column number of the target element. + */ + column: number; +} + +export type SourceableNode = Pick<Node, 'getSourceFile' | 'getStart'>; + +export function getSourcePath(node: SourceableNode): RawApiDocsSource { + const sourceFile = node.getSourceFile(); + const filePath = sourceFile + .getFilePath() + .substring(FILE_PATH_PROJECT.length + 1); + const startPosition = node.getStart(); + const { line, column } = sourceFile.getLineAndColumnAtPos(startPosition); + + return { + filePath, + line, + column, + }; +} diff --git a/scripts/apidocs/processing/type.ts b/scripts/apidocs/processing/type.ts new file mode 100644 index 00000000..25ec30c9 --- /dev/null +++ b/scripts/apidocs/processing/type.ts @@ -0,0 +1,231 @@ +import { TypeFlags, type Type } from 'ts-morph'; +import { atLeastOneAndAllRequired, required } from '../utils/value-checks'; + +export type RawApiDocsType = + | RawApiDocsSimpleType + | RawApiDocsGenericType + | RawApiDocsUnionType + | RawApiDocsShadowType; + +interface RawApiDocsBaseType { + type: string; + text: string; +} + +export interface RawApiDocsSimpleType extends RawApiDocsBaseType { + type: 'simple'; +} + +export interface RawApiDocsGenericType extends RawApiDocsBaseType { + type: 'generic'; + typeParameters: RawApiDocsType[]; +} + +export interface RawApiDocsUnionType extends RawApiDocsBaseType { + type: 'union'; + types: RawApiDocsType[]; +} + +export interface RawApiDocsShadowType extends RawApiDocsBaseType { + type: 'shadow'; + resolvedType: RawApiDocsType; +} + +export function getNameSuffix(type: Type): string { + return type.isNullable() ? '?' : ''; +} + +export function getTypeText( + type: Type, + options: { + abbreviate?: boolean; + stripUndefined?: boolean; + resolveAliases?: boolean; + } = {} +): RawApiDocsType { + const { + abbreviate = false, + stripUndefined = false, + resolveAliases = false, + } = options; + + if ( + type.isAny() || + type.isUnknown() || + type.isBoolean() || + type.isBooleanLiteral() || + type.isNumber() || + type.isNumberLiteral() || + type.getFlags() & TypeFlags.BigInt || + type.getFlags() & TypeFlags.ESSymbol || + type.isString() || + type.isUndefined() || + type.isNull() || + type.isVoid() || + type.isNever() + ) { + return newSimpleType(type.getText()); + } else if (type.isStringLiteral()) { + return newSimpleType(type.getText().replace(/^"(.*)"$/, "'$1'")); + } else if (type.isArray()) { + return newArrayType( + getTypeText(type.getArrayElementTypeOrThrow(), options) + ); + } else if (stripUndefined && type.isNullable()) { + return getTypeText(type.getNonNullableType(), options); + } + + const symbol = type.getSymbol() ?? type.getAliasSymbol(); + if (!resolveAliases && symbol) { + const name = symbol.getName(); + if (name !== '__type') { + const typeArguments = [ + ...type.getTypeArguments(), + ...type.getAliasTypeArguments(), + ]; + + if (name === 'LiteralUnion') { + const displayType = getTypeText(typeArguments[0], options); + const baseType = typeArguments[1] + ? getTypeText(typeArguments[1], options) + : newSimpleType('string'); + + return newUnionType([displayType, baseType]); + } + + const typeParameters = typeArguments.map((t) => getTypeText(t, options)); + + if (typeParameters.length === 0) { + const resolvedType = getTypeText(type, { + ...options, + resolveAliases: true, + }); + + if (name === resolvedType.text) { + return newSimpleType(name); + } + + return newShadowType(name, resolvedType); + } + + return newGenericType(name, typeParameters); + } + } + + if (type.isUnion()) { + let unionTypes = type + .getUnionTypes() + .map((unionType) => getTypeText(unionType, options)) + .filter((unionType) => !stripUndefined || unionType.text !== 'undefined'); + + const trueIndex = unionTypes.findIndex( + (unionType) => unionType.text === 'true' + ); + if ( + trueIndex !== -1 && + unionTypes.some((unionType) => unionType.text === 'false') + ) { + unionTypes[trueIndex] = newSimpleType('boolean'); + unionTypes = unionTypes.filter( + (unionType) => unionType.text !== 'true' && unionType.text !== 'false' + ); + } + + if (unionTypes.length === 1) { + return unionTypes[0]; + } + + return newUnionType(unionTypes); + } + + if (abbreviate && isOptionsLikeType(type)) { + return newSimpleType('{ ... }'); + } + + if (resolveAliases && type.isTypeParameter()) { + const text = getTypeText(type.getApparentType(), { + ...options, + resolveAliases: true, + }); + + if (text.text === 'unknown') { + return newSimpleType('any'); + } + + return text; + } + + return newSimpleType(type.getText().replaceAll(/import\([^)]*\)\./g, '')); +} + +export function isOptionsLikeType(type: Type): boolean { + return ( + type.isObject() && + type.isAnonymous() && + type.getCallSignatures().length === 0 && + type.getTupleElements().length === 0 + ); +} + +function newSimpleType(name: string): RawApiDocsSimpleType { + required(name, 'name'); + return { type: 'simple', text: name }; +} + +function newArrayType(typeParameter: RawApiDocsType): RawApiDocsGenericType { + const { text } = required(typeParameter, 'array type'); + const useGeneric = text.includes('|') || text.includes('{'); + return { + type: 'generic', + typeParameters: [typeParameter], + text: useGeneric ? `Array<${text}>` : `${text}[]`, + }; +} + +function newGenericType( + name: string, + typeParameters: RawApiDocsType[] +): RawApiDocsType { + required(name, 'name'); + atLeastOneAndAllRequired(typeParameters, 'type parameters'); + return { + type: 'generic', + typeParameters, + text: `${name}<${typeParameters.map((t) => t.text).join(', ')}>`, + }; +} + +function newUnionType(types: RawApiDocsType[]): RawApiDocsUnionType { + atLeastOneAndAllRequired(types, 'unions'); + return { + type: 'union', + types, + text: types + .map((type) => type.text) + .map((text) => + // Remove LiteralUnion shadow types + text.endsWith(' & { zz_IGNORE_ME?: undefined; }') + ? text.slice(0, -32) + : text + ) + .map((text) => { + // () => T -> (() => T) + const isFunctionSignature = text.startsWith('('); + return isFunctionSignature ? `(${text})` : text; + }) + .join(' | '), + }; +} + +function newShadowType( + displayText: string, + resolvedType: RawApiDocsType +): RawApiDocsShadowType { + required(displayText, 'display text'); + required(resolvedType, 'resolved type'); + return { + type: 'shadow', + resolvedType, + text: displayText, + }; +} diff --git a/scripts/apidocs/project.ts b/scripts/apidocs/project.ts new file mode 100644 index 00000000..c63bad8e --- /dev/null +++ b/scripts/apidocs/project.ts @@ -0,0 +1,9 @@ +import type { ProjectOptions } from 'ts-morph'; +import { Project } from 'ts-morph'; + +export function getProject(options: Partial<ProjectOptions> = {}): Project { + return new Project({ + ...options, + tsConfigFilePath: options.tsConfigFilePath ?? 'tsconfig.build.json', + }); +} diff --git a/scripts/apidoc/format.ts b/scripts/apidocs/utils/format.ts index a8f63ac0..ab9f524d 100644 --- a/scripts/apidoc/format.ts +++ b/scripts/apidocs/utils/format.ts @@ -1,6 +1,6 @@ import type { Options } from 'prettier'; import { format } from 'prettier'; -import prettierConfig from '../../.prettierrc.js'; +import prettierConfig from '../../../.prettierrc.js'; /** * Formats markdown contents. diff --git a/scripts/apidoc/markdown.ts b/scripts/apidocs/utils/markdown.ts index 0b1e3b40..b0cc68be 100644 --- a/scripts/apidoc/markdown.ts +++ b/scripts/apidocs/utils/markdown.ts @@ -1,14 +1,14 @@ import sanitizeHtml from 'sanitize-html'; import type { MarkdownRenderer } from 'vitepress'; import { createMarkdownRenderer } from 'vitepress'; -import vitepressConfig from '../../docs/.vitepress/config'; -import { adjustUrls, pathOutputDir } from './utils'; +import vitepressConfig from '../../../docs/.vitepress/config'; +import { FILE_PATH_API_DOCS } from './paths'; let markdown: MarkdownRenderer; export async function initMarkdownRenderer(): Promise<void> { markdown = await createMarkdownRenderer( - pathOutputDir, + FILE_PATH_API_DOCS, vitepressConfig.markdown, '/' ); @@ -31,7 +31,7 @@ const htmlSanitizeOptions: sanitizeHtml.IOptions = { a: ['href', 'target', 'rel'], button: ['class', 'title'], div: ['class'], - pre: ['class', 'tabindex', 'v-pre'], + pre: ['class', 'v-pre'], span: ['class', 'style'], }, selfClosing: [], @@ -45,7 +45,6 @@ function comparableSanitizedHtml(html: string): string { .replaceAll('>', '>') .replaceAll('<', '<') .replaceAll('&', '&') - .replaceAll('"', '"') .replaceAll('=""', '') .replaceAll(' ', ''); } @@ -70,7 +69,27 @@ export function codeToHtml(code: string): string { * * @returns The converted HTML string. */ -export function mdToHtml(md: string, inline: boolean = false): string { +export function mdToHtml(md: string, inline?: boolean): string; +/** + * Converts Markdown to an HTML string and sanitizes it. + * + * @param md The markdown to convert. + * @param inline Whether to render the markdown as inline, without a wrapping `<p>` tag. Defaults to `false`. + * + * @returns The converted HTML string. + */ +export function mdToHtml( + md: string | undefined, + inline?: boolean +): string | undefined; +export function mdToHtml( + md: string | undefined, + inline: boolean = false +): string | undefined { + if (md == null) { + return undefined; + } + const rawHtml = inline ? markdown.renderInline(md) : markdown.render(md); const safeHtml: string = sanitizeHtml(rawHtml, htmlSanitizeOptions); @@ -79,9 +98,14 @@ export function mdToHtml(md: string, inline: boolean = false): string { return adjustUrls(safeHtml); } - console.debug('Rejected unsafe md:', md); - console.error('Rejected unsafe html:', rawHtml); - console.error('Rejected unsafe html:', comparableSanitizedHtml(rawHtml)); - console.error('Expected safe html:', comparableSanitizedHtml(safeHtml)); + console.debug('Rejected unsafe md:\n', md); + console.error('Rejected unsafe html:\n', rawHtml); + console.error('Clean unsafe html:\n', comparableSanitizedHtml(rawHtml)); + console.error('Clean safe html:\n', comparableSanitizedHtml(safeHtml)); + console.log('-'.repeat(80)); throw new Error('Found unsafe html'); } + +export function adjustUrls(description: string): string { + return description.replaceAll(/https:\/\/(next.)?fakerjs.dev\//g, '/'); +} diff --git a/scripts/apidocs/utils/paths.ts b/scripts/apidocs/utils/paths.ts new file mode 100644 index 00000000..8abca1ea --- /dev/null +++ b/scripts/apidocs/utils/paths.ts @@ -0,0 +1,24 @@ +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const FILE_PATH_THIS = dirname(fileURLToPath(import.meta.url)); +/** + * The path to the project directory. + */ +// Required for converting the source file paths to relative paths +export const FILE_PATH_PROJECT = resolve(FILE_PATH_THIS, '..', '..', '..'); +/** + * The path to the docs directory. + */ +// Required for writing the api page vitepress config +export const FILE_PATH_DOCS = resolve(FILE_PATH_PROJECT, 'docs'); +/** + * The path to the website's public directory. + */ +// Required for publishing the diff index +export const FILE_PATH_PUBLIC = resolve(FILE_PATH_DOCS, 'public'); +/** + * The path to the api docs directory. + */ +// Required for writing various api docs files +export const FILE_PATH_API_DOCS = resolve(FILE_PATH_DOCS, 'api'); diff --git a/scripts/apidocs/utils/value-checks.ts b/scripts/apidocs/utils/value-checks.ts new file mode 100644 index 00000000..f8578ccc --- /dev/null +++ b/scripts/apidocs/utils/value-checks.ts @@ -0,0 +1,69 @@ +export function exactlyOne<T>(input: ReadonlyArray<T>, property: string): T { + if (input.length !== 1) { + throw new Error( + `Expected exactly one element for ${property}, got ${input.length}` + ); + } + + return input[0]; +} + +export function optionalOne<T>( + input: ReadonlyArray<T>, + property: string +): T | undefined { + if (input.length > 1) { + throw new Error( + `Expected one optional element for ${property}, got ${input.length}` + ); + } + + return input[0]; +} + +export function required<T>( + input: T | undefined, + property: string +): NonNullable<T> { + if (input == null) { + throw new Error(`Expected a value for ${property}, got undefined`); + } + + return input; +} + +export function allRequired<T>( + input: ReadonlyArray<T | undefined>, + property: string +): Array<NonNullable<T>> { + return input.map((v, i) => required(v, `${property}[${i}]`)); +} + +export function atLeastOne<T>( + input: ReadonlyArray<T>, + property: string +): ReadonlyArray<T> { + if (input.length === 0) { + throw new Error(`Expected at least one element for ${property}`); + } + + return input; +} + +export function atLeastOneAndAllRequired<T>( + input: ReadonlyArray<T | undefined>, + property: string +): ReadonlyArray<NonNullable<T>> { + return atLeastOne(allRequired(input, property), property); +} + +export function valueForKey<T>(input: Record<string, T>, key: string): T { + return required(input[key], key); +} + +export function valuesForKeys<T>( + input: Record<string, T>, + keys: string[] +): T[] { + return keys.map((key) => valueForKey(input, key)); +} diff --git a/scripts/diff.ts b/scripts/diff.ts index 813154f2..f52076aa 100644 --- a/scripts/diff.ts +++ b/scripts/diff.ts @@ -2,14 +2,14 @@ import { existsSync } from 'node:fs'; import { argv } from 'node:process'; -import { diff } from './apidoc/diff'; -import { pathDocsDiffIndexFile } from './apidoc/utils'; +import { diff } from './apidocs/diff'; +import { FILE_PATH_DOCS_DIFF_INDEX } from './apidocs/output/diff-index'; const [target, source] = argv.slice(2); -if (!source && !existsSync(pathDocsDiffIndexFile)) { +if (!source && !existsSync(FILE_PATH_DOCS_DIFF_INDEX)) { throw new Error( - `Unable to find local diff index file at: ${pathDocsDiffIndexFile}\n + `Unable to find local diff index file at: ${FILE_PATH_DOCS_DIFF_INDEX}\n You can run \`pnpm run generate:api-docs\` to generate it.` ); } diff --git a/scripts/generate-locales.ts b/scripts/generate-locales.ts index 90ea9a34..cba70edc 100644 --- a/scripts/generate-locales.ts +++ b/scripts/generate-locales.ts @@ -25,7 +25,7 @@ import { dirname, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import type { LocaleDefinition, MetadataDefinition } from '../src/definitions'; import { keys } from '../src/internal/keys'; -import { formatMarkdown, formatTypescript } from './apidoc/format'; +import { formatMarkdown, formatTypescript } from './apidocs/utils/format'; // Constants diff --git a/test/scripts/apidoc/__snapshots__/module.spec.ts.snap b/test/scripts/apidoc/__snapshots__/module.spec.ts.snap deleted file mode 100644 index d4275860..00000000 --- a/test/scripts/apidoc/__snapshots__/module.spec.ts.snap +++ /dev/null @@ -1,54 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[`module > analyzeModule() > ModuleDeprecationTest 1`] = ` -{ - "comment": "This is a description for a module with a code example.", - "deprecated": "Well, this is deprecated.", - "examples": undefined, -} -`; - -exports[`module > analyzeModule() > ModuleExampleTest 1`] = ` -{ - "comment": "This is a description for a module with a code example.", - "deprecated": undefined, - "examples": "<div class="language-ts vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ts</span><pre class="shiki shiki-themes github-light github-dark vp-code" v-pre><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">new</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> ModuleExampleTest</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">()</span></span></code></pre> -</div>", -} -`; - -exports[`module > analyzeModule() > ModuleFakerJsLinkTest 1`] = ` -{ - "comment": "Description with a link to our [website](/) -and [api docs](/api/).", - "deprecated": undefined, - "examples": undefined, -} -`; - -exports[`module > analyzeModule() > ModuleNextFakerJsLinkTest 1`] = ` -{ - "comment": "Description with a link to our [website](/) -and [api docs](/api/).", - "deprecated": undefined, - "examples": undefined, -} -`; - -exports[`module > analyzeModule() > ModuleSimpleTest 1`] = ` -{ - "comment": "A simple module without anything special.", - "deprecated": undefined, - "examples": undefined, -} -`; - -exports[`module > analyzeModule() > expected and actual modules are equal 1`] = ` -[ - "ModuleDeprecationTest", - "ModuleExampleTest", - "ModuleFakerJsLinkTest", - "ModuleNextFakerJsLinkTest", - "ModuleSimpleTest", -] -`; diff --git a/test/scripts/apidoc/__snapshots__/signature.spec.ts.snap b/test/scripts/apidoc/__snapshots__/signature.spec.ts.snap deleted file mode 100644 index 37a9f754..00000000 --- a/test/scripts/apidoc/__snapshots__/signature.spec.ts.snap +++ /dev/null @@ -1,768 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[`signature > analyzeSignature() > complexArrayParameter 1`] = ` -{ - "deprecated": undefined, - "description": "<p>Complex array parameter.</p> -", - "examples": "<div class="language-ts vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ts</span><pre class="shiki shiki-themes github-light github-dark vp-code" v-pre><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">complexArrayParameter</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"><</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">T</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">>(array: readonly </span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">Array</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"><{</span></span> -<span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70"> value</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> T</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span> -<span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70"> weight</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> number</span></span> -<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}>): </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">T</span></span></code></pre> -</div>", - "name": "complexArrayParameter", - "parameters": [ - { - "description": "<p>The type of the entries to pick from.</p> -", - "name": "<T>", - "type": undefined, - }, - { - "default": undefined, - "description": "<p>Array to pick the value from.</p> -", - "name": "array", - "type": "Array<{ ... }>", - }, - { - "default": undefined, - "description": "<p>The value to pick.</p> -", - "name": "array[].value", - "type": "T", - }, - { - "default": undefined, - "description": "<p>The weight of the value.</p> -", - "name": "array[].weight", - "type": "number", - }, - ], - "returns": "T", - "seeAlsos": [], - "since": "Missing", - "sourcePath": "test/scripts/apidoc/signature.example.ts#L377", - "throws": undefined, -} -`; - -exports[`signature > analyzeSignature() > defaultBooleanParamMethod 1`] = ` -{ - "deprecated": undefined, - "description": "<p>Test with a default parameter.</p> -", - "examples": "<div class="language-ts vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ts</span><pre class="shiki shiki-themes github-light github-dark vp-code" v-pre><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">defaultBooleanParamMethod</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(c: boolean </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> true</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">): number</span></span></code></pre> -</div>", - "name": "defaultBooleanParamMethod", - "parameters": [ - { - "default": "true", - "description": "<p>The boolean parameter.</p> -", - "name": "c", - "type": "boolean", - }, - ], - "returns": "number", - "seeAlsos": [], - "since": "Missing", - "sourcePath": "test/scripts/apidoc/signature.example.ts#L105", - "throws": undefined, -} -`; - -exports[`signature > analyzeSignature() > expected and actual methods are equal 1`] = ` -[ - "complexArrayParameter", - "defaultBooleanParamMethod", - "functionParamMethod", - "literalUnionParamMethod", - "methodWithDeprecated", - "methodWithDeprecatedOption", - "methodWithExample", - "methodWithMultipleSeeMarkers", - "methodWithMultipleSeeMarkersAndBackticks", - "methodWithMultipleThrows", - "methodWithSinceMarker", - "methodWithThrows", - "multiParamMethod", - "noParamMethod", - "optionalStringParamMethod", - "optionsInlineParamMethodWithDefaults", - "optionsInterfaceParamMethodWithDefaults", - "optionsParamMethod", - "optionsTypeParamMethodWithDefaults", - "recordParamMethod", - "requiredNumberParamMethod", - "stringUnionParamMethod", -] -`; - -exports[`signature > analyzeSignature() > functionParamMethod 1`] = ` -{ - "deprecated": undefined, - "description": "<p>Test with a function parameters.</p> -", - "examples": "<div class="language-ts vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ts</span><pre class="shiki shiki-themes github-light github-dark vp-code" v-pre><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">functionParamMethod</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(fn: (</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70">a</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> string</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> number): number</span></span></code></pre> -</div>", - "name": "functionParamMethod", - "parameters": [ - { - "default": undefined, - "description": "<p>The function parameter.</p> -", - "name": "fn", - "type": "(a: string) => number", - }, - ], - "returns": "number", - "seeAlsos": [], - "since": "Missing", - "sourcePath": "test/scripts/apidoc/signature.example.ts#L125", - "throws": undefined, -} -`; - -exports[`signature > analyzeSignature() > literalUnionParamMethod 1`] = ` -{ - "deprecated": undefined, - "description": "<p>Test with LiteralUnion.</p> -", - "examples": "<div class="language-ts vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ts</span><pre class="shiki shiki-themes github-light github-dark vp-code" v-pre><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">literalUnionParamMethod</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(value: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'a'</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> |</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'b'</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> |</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> ?</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, namedValue</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'a'</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> |</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'b'</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> |</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> ?</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, array</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> readonly Array</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"><</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'a'</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> |</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'b'</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> |</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> ?></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, namedArray</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> readonly Array</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"><</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'a'</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> |</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'b'</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> |</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> ?></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, mixed</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'a'</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> |</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'b'</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> |</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> ?</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> |</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> readonly Array</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"><</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'a'</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> |</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'b'</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> |</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> ?></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, namedMixed</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'a'</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> |</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'b'</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> |</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> ?</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> |</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> readonly Array</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"><</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'a'</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> |</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'b'</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> |</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> ?></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">)</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> string</span></span></code></pre> -</div>", - "name": "literalUnionParamMethod", - "parameters": [ - { - "default": undefined, - "description": "<p><code>'a'</code> or <code>'b'</code>.</p> -", - "name": "value", - "type": "'a' | 'b' | ?", - }, - { - "default": undefined, - "description": "<p><code>'a'</code> or <code>'b'</code>.</p> -", - "name": "namedValue", - "type": "'a' | 'b' | ?", - }, - { - "default": undefined, - "description": "<p>Array of <code>'a'</code> or <code>'b'</code>.</p> -", - "name": "array", - "type": "Array<'a' | 'b' | ?>", - }, - { - "default": undefined, - "description": "<p>Array of <code>'a'</code> or <code>'b'</code>.</p> -", - "name": "namedArray", - "type": "Array<'a' | 'b' | ?>", - }, - { - "default": undefined, - "description": "<p>Value <code>'a'</code> or <code>'b'</code> or an array thereof.</p> -", - "name": "mixed", - "type": "'a' | 'b' | ? | Array<'a' | 'b' | ?>", - }, - { - "default": undefined, - "description": "<p>Value <code>'a'</code> or <code>'b'</code> or an array thereof.</p> -", - "name": "namedMixed", - "type": "'a' | 'b' | ? | Array<'a' | 'b' | ?>", - }, - ], - "returns": "string", - "seeAlsos": [], - "since": "Missing", - "sourcePath": "test/scripts/apidoc/signature.example.ts#L159", - "throws": undefined, -} -`; - -exports[`signature > analyzeSignature() > methodWithDeprecated 1`] = ` -{ - "deprecated": "<p>do something else</p> -", - "description": "<p>Test with deprecated and see marker.</p> -", - "examples": "<div class="language-ts vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ts</span><pre class="shiki shiki-themes github-light github-dark vp-code" v-pre><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">methodWithDeprecated</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(): number</span></span></code></pre> -</div>", - "name": "methodWithDeprecated", - "parameters": [], - "returns": "number", - "seeAlsos": [ - "test.apidoc.methodWithExample()", - ], - "since": "Missing", - "sourcePath": "test/scripts/apidoc/signature.example.ts#L287", - "throws": undefined, -} -`; - -exports[`signature > analyzeSignature() > methodWithDeprecatedOption 1`] = ` -{ - "deprecated": undefined, - "description": "<p>Test with deprecated option.</p> -", - "examples": "<div class="language-ts vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ts</span><pre class="shiki shiki-themes github-light github-dark vp-code" v-pre><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">methodWithDeprecatedOption</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(option: {</span></span> -<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> a: string,</span></span> -<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> b</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: () </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> number,</span></span> -<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> c: number</span></span> -<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}): number</span></span></code></pre> -</div>", - "name": "methodWithDeprecatedOption", - "parameters": [ - { - "default": undefined, - "description": "<p>The options.</p> -", - "name": "option", - "type": "{ ... }", - }, - { - "default": undefined, - "description": "<p>Some deprecated option.</p> -<p><strong>DEPRECATED:</strong> do something else.</p> -", - "name": "option.a", - "type": "string", - }, - { - "default": undefined, - "description": "<p>Some other deprecated option.</p> -<p><strong>DEPRECATED:</strong> do something else.</p> -", - "name": "option.b", - "type": "() => number", - }, - { - "default": undefined, - "description": "<p>Some other option.</p> -", - "name": "option.c", - "type": "number", - }, - ], - "returns": "number", - "seeAlsos": [], - "since": "Missing", - "sourcePath": "test/scripts/apidoc/signature.example.ts#L318", - "throws": undefined, -} -`; - -exports[`signature > analyzeSignature() > methodWithExample 1`] = ` -{ - "deprecated": undefined, - "description": "<p>Test with example marker.</p> -", - "examples": "<div class="language-ts vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ts</span><pre class="shiki shiki-themes github-light github-dark vp-code" v-pre><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">methodWithExample</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(): number</span></span> -<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">test.apidoc.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">methodWithExample</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">() </span><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D">// 0</span></span></code></pre> -</div>", - "name": "methodWithExample", - "parameters": [], - "returns": "number", - "seeAlsos": [], - "since": "Missing", - "sourcePath": "test/scripts/apidoc/signature.example.ts#L276", - "throws": undefined, -} -`; - -exports[`signature > analyzeSignature() > methodWithMultipleSeeMarkers 1`] = ` -{ - "deprecated": undefined, - "description": "<p>Test with multiple see markers.</p> -", - "examples": "<div class="language-ts vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ts</span><pre class="shiki shiki-themes github-light github-dark vp-code" v-pre><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">methodWithMultipleSeeMarkers</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(): number</span></span></code></pre> -</div>", - "name": "methodWithMultipleSeeMarkers", - "parameters": [], - "returns": "number", - "seeAlsos": [ - "test.apidoc.methodWithExample()", - "test.apidoc.methodWithDeprecated()", - ], - "since": "Missing", - "sourcePath": "test/scripts/apidoc/signature.example.ts#L345", - "throws": undefined, -} -`; - -exports[`signature > analyzeSignature() > methodWithMultipleSeeMarkersAndBackticks 1`] = ` -{ - "deprecated": undefined, - "description": "<p>Test with multiple see markers and backticks.</p> -", - "examples": "<div class="language-ts vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ts</span><pre class="shiki shiki-themes github-light github-dark vp-code" v-pre><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">methodWithMultipleSeeMarkersAndBackticks</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(): number</span></span></code></pre> -</div>", - "name": "methodWithMultipleSeeMarkersAndBackticks", - "parameters": [], - "returns": "number", - "seeAlsos": [ - "test.apidoc.methodWithExample() with parameter <code>foo</code>.", - "test.apidoc.methodWithDeprecated() with parameter <code>bar</code> and <code>baz</code>.", - ], - "since": "Missing", - "sourcePath": "test/scripts/apidoc/signature.example.ts#L355", - "throws": undefined, -} -`; - -exports[`signature > analyzeSignature() > methodWithMultipleThrows 1`] = ` -{ - "deprecated": undefined, - "description": "<p>Test with multiple throws.</p> -", - "examples": "<div class="language-ts vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ts</span><pre class="shiki shiki-themes github-light github-dark vp-code" v-pre><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">methodWithMultipleThrows</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(): number</span></span></code></pre> -</div>", - "name": "methodWithMultipleThrows", - "parameters": [], - "returns": "number", - "seeAlsos": [], - "since": "Missing", - "sourcePath": "test/scripts/apidoc/signature.example.ts#L306", - "throws": "First error case. -Another error case.", -} -`; - -exports[`signature > analyzeSignature() > methodWithSinceMarker 1`] = ` -{ - "deprecated": undefined, - "description": "<p>Test with since marker.</p> -", - "examples": "<div class="language-ts vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ts</span><pre class="shiki shiki-themes github-light github-dark vp-code" v-pre><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">methodWithSinceMarker</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(): number</span></span></code></pre> -</div>", - "name": "methodWithSinceMarker", - "parameters": [], - "returns": "number", - "seeAlsos": [], - "since": "1.0.0", - "sourcePath": "test/scripts/apidoc/signature.example.ts#L364", - "throws": undefined, -} -`; - -exports[`signature > analyzeSignature() > methodWithThrows 1`] = ` -{ - "deprecated": undefined, - "description": "<p>Test with throws.</p> -", - "examples": "<div class="language-ts vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ts</span><pre class="shiki shiki-themes github-light github-dark vp-code" v-pre><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">methodWithThrows</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(): number</span></span></code></pre> -</div>", - "name": "methodWithThrows", - "parameters": [], - "returns": "number", - "seeAlsos": [], - "since": "Missing", - "sourcePath": "test/scripts/apidoc/signature.example.ts#L296", - "throws": "Everytime.", -} -`; - -exports[`signature > analyzeSignature() > multiParamMethod 1`] = ` -{ - "deprecated": undefined, - "description": "<p>Test with multiple parameters.</p> -", - "examples": "<div class="language-ts vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ts</span><pre class="shiki shiki-themes github-light github-dark vp-code" v-pre><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">multiParamMethod</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(a: number, b</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">?:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> string, c: boolean </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF"> true</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">): number</span></span></code></pre> -</div>", - "name": "multiParamMethod", - "parameters": [ - { - "default": undefined, - "description": "<p>The number parameter.</p> -", - "name": "a", - "type": "number", - }, - { - "default": undefined, - "description": "<p>The string parameter.</p> -", - "name": "b?", - "type": "string", - }, - { - "default": "true", - "description": "<p>The boolean parameter.</p> -", - "name": "c", - "type": "boolean", - }, - ], - "returns": "number", - "seeAlsos": [], - "since": "Missing", - "sourcePath": "test/scripts/apidoc/signature.example.ts#L116", - "throws": undefined, -} -`; - -exports[`signature > analyzeSignature() > noParamMethod 1`] = ` -{ - "deprecated": undefined, - "description": "<p>Test with no parameters.</p> -", - "examples": "<div class="language-ts vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ts</span><pre class="shiki shiki-themes github-light github-dark vp-code" v-pre><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">noParamMethod</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(): number</span></span></code></pre> -</div>", - "name": "noParamMethod", - "parameters": [], - "returns": "number", - "seeAlsos": [], - "since": "Missing", - "sourcePath": "test/scripts/apidoc/signature.example.ts#L78", - "throws": undefined, -} -`; - -exports[`signature > analyzeSignature() > optionalStringParamMethod 1`] = ` -{ - "deprecated": undefined, - "description": "<p>Test with an optional parameter.</p> -", - "examples": "<div class="language-ts vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ts</span><pre class="shiki shiki-themes github-light github-dark vp-code" v-pre><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">optionalStringParamMethod</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(b</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">?:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> string): number</span></span></code></pre> -</div>", - "name": "optionalStringParamMethod", - "parameters": [ - { - "default": undefined, - "description": "<p>The string parameter.</p> -", - "name": "b?", - "type": "string", - }, - ], - "returns": "number", - "seeAlsos": [], - "since": "Missing", - "sourcePath": "test/scripts/apidoc/signature.example.ts#L96", - "throws": undefined, -} -`; - -exports[`signature > analyzeSignature() > optionsInlineParamMethodWithDefaults 1`] = ` -{ - "deprecated": undefined, - "description": "<p>Test with a function parameters (inline types) with defaults.</p> -", - "examples": "<div class="language-ts vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ts</span><pre class="shiki shiki-themes github-light github-dark vp-code" v-pre><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">optionsInlineParamMethodWithDefaults</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(a: {</span></span> -<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> value: number</span></span> -<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">} </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { value: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> }, b: {</span></span> -<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> value: number</span></span> -<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">} </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { value: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> }, c: {</span></span> -<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> value: number</span></span> -<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}): number</span></span></code></pre> -</div>", - "name": "optionsInlineParamMethodWithDefaults", - "parameters": [ - { - "default": "{ value: 1 }", - "description": "<p>Parameter with signature default. -It also has a more complex description.</p> -", - "name": "a", - "type": "{ ... }", - }, - { - "default": undefined, - "description": "<p>The number parameter.</p> -", - "name": "a.value?", - "type": "number", - }, - { - "default": "{ value: 1 }", - "description": "<p>Parameter with jsdocs default.</p> -<p>It also has a more complex description.</p> -", - "name": "b", - "type": "{ ... }", - }, - { - "default": undefined, - "description": "<p>The number parameter.</p> -", - "name": "b.value?", - "type": "number", - }, - { - "default": undefined, - "description": "<p>Parameter with inner jsdocs default.</p> -", - "name": "c", - "type": "{ ... }", - }, - { - "default": "2", - "description": "<p>The number parameter. It also has a more complex description.</p> -", - "name": "c.value?", - "type": "number", - }, - ], - "returns": "number", - "seeAlsos": [], - "since": "Missing", - "sourcePath": "test/scripts/apidoc/signature.example.ts#L226", - "throws": undefined, -} -`; - -exports[`signature > analyzeSignature() > optionsInterfaceParamMethodWithDefaults 1`] = ` -{ - "deprecated": undefined, - "description": "<p>Test with a function parameters with defaults.</p> -", - "examples": "<div class="language-ts vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ts</span><pre class="shiki shiki-themes github-light github-dark vp-code" v-pre><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">optionsInterfaceParamMethodWithDefaults</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(a: ParameterOptionsInterfaceA </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { value: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> }, b: ParameterOptionsInterfaceB </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { value: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> }, c: ParameterOptionsInterfaceC): number</span></span></code></pre> -</div>", - "name": "optionsInterfaceParamMethodWithDefaults", - "parameters": [ - { - "default": "{ value: 1 }", - "description": "<p>Parameter with signature default.</p> -", - "name": "a", - "type": "ParameterOptionsInterfaceA", - }, - { - "default": "{ value: 1 }", - "description": "<p>Parameter with jsdocs default.</p> -", - "name": "b", - "type": "ParameterOptionsInterfaceB", - }, - { - "default": undefined, - "description": "<p>Parameter with inner jsdocs default.</p> -", - "name": "c", - "type": "ParameterOptionsInterfaceC", - }, - ], - "returns": "number", - "seeAlsos": [], - "since": "Missing", - "sourcePath": "test/scripts/apidoc/signature.example.ts#L262", - "throws": undefined, -} -`; - -exports[`signature > analyzeSignature() > optionsParamMethod 1`] = ` -{ - "deprecated": undefined, - "description": "<p>Test with an options parameter.</p> -", - "examples": "<div class="language-ts vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ts</span><pre class="shiki shiki-themes github-light github-dark vp-code" v-pre><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">optionsParamMethod</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(options: {</span></span> -<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> a: number,</span></span> -<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> b: string,</span></span> -<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> c: boolean,</span></span> -<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0"> d</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">: () </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> string,</span></span> -<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> e: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'a'</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> |</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'b'</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> |</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> ?</span></span> -<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">})</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> number</span></span></code></pre> -</div>", - "name": "optionsParamMethod", - "parameters": [ - { - "default": undefined, - "description": "<p>The options parameter.</p> -", - "name": "options", - "type": "{ ... }", - }, - { - "default": undefined, - "description": "<p>The number parameter.</p> -", - "name": "options.a", - "type": "number", - }, - { - "default": undefined, - "description": "<p>The string parameter.</p> -", - "name": "options.b?", - "type": "string", - }, - { - "default": undefined, - "description": "<p>The boolean parameter.</p> -", - "name": "options.c", - "type": "boolean", - }, - { - "default": undefined, - "description": "<p>The method parameter.</p> -", - "name": "options.d", - "type": "() => string", - }, - { - "default": "'a'", - "description": "<p>A parameter with inline documentation.</p> -", - "name": "options.e", - "type": "'a' | 'b' | ?", - }, - ], - "returns": "number", - "seeAlsos": [], - "since": "Missing", - "sourcePath": "test/scripts/apidoc/signature.example.ts#L196", - "throws": undefined, -} -`; - -exports[`signature > analyzeSignature() > optionsTypeParamMethodWithDefaults 1`] = ` -{ - "deprecated": undefined, - "description": "<p>Test with a function parameters with defaults.</p> -", - "examples": "<div class="language-ts vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ts</span><pre class="shiki shiki-themes github-light github-dark vp-code" v-pre><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">optionsTypeParamMethodWithDefaults</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(a: ParameterOptionsTypeA </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { value: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> }, b: ParameterOptionsTypeB </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> { value: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF">1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> }, c: ParameterOptionsTypeC): number</span></span></code></pre> -</div>", - "name": "optionsTypeParamMethodWithDefaults", - "parameters": [ - { - "default": "{ value: 1 }", - "description": "<p>Parameter with signature default.</p> -", - "name": "a", - "type": "ParameterOptionsTypeA", - }, - { - "default": "{ value: 1 }", - "description": "<p>Parameter with jsdocs default.</p> -", - "name": "b", - "type": "ParameterOptionsTypeB", - }, - { - "default": undefined, - "description": "<p>Parameter with inner jsdocs default.</p> -", - "name": "c", - "type": "ParameterOptionsTypeC", - }, - ], - "returns": "number", - "seeAlsos": [], - "since": "Missing", - "sourcePath": "test/scripts/apidoc/signature.example.ts#L244", - "throws": undefined, -} -`; - -exports[`signature > analyzeSignature() > recordParamMethod 1`] = ` -{ - "deprecated": undefined, - "description": "<p>Test with a Record parameter.</p> -", - "examples": "<div class="language-ts vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ts</span><pre class="shiki shiki-themes github-light github-dark vp-code" v-pre><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">recordParamMethod</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(object: Record</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"><</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">string, number</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">></span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">): number</span></span></code></pre> -</div>", - "name": "recordParamMethod", - "parameters": [ - { - "default": undefined, - "description": "<p>The Record parameter.</p> -", - "name": "object", - "type": "Record<string, number>", - }, - ], - "returns": "number", - "seeAlsos": [], - "since": "Missing", - "sourcePath": "test/scripts/apidoc/signature.example.ts#L182", - "throws": undefined, -} -`; - -exports[`signature > analyzeSignature() > requiredNumberParamMethod 1`] = ` -{ - "deprecated": undefined, - "description": "<p>Test with a required parameter.</p> -", - "examples": "<div class="language-ts vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ts</span><pre class="shiki shiki-themes github-light github-dark vp-code" v-pre><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">requiredNumberParamMethod</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(a: number): number</span></span></code></pre> -</div>", - "name": "requiredNumberParamMethod", - "parameters": [ - { - "default": undefined, - "description": "<p>The number parameter.</p> -", - "name": "a", - "type": "number", - }, - ], - "returns": "number", - "seeAlsos": [], - "since": "Missing", - "sourcePath": "test/scripts/apidoc/signature.example.ts#L87", - "throws": undefined, -} -`; - -exports[`signature > analyzeSignature() > stringUnionParamMethod 1`] = ` -{ - "deprecated": undefined, - "description": "<p>Test with string union.</p> -", - "examples": "<div class="language-ts vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ts</span><pre class="shiki shiki-themes github-light github-dark vp-code" v-pre><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0">stringUnionParamMethod</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">(value: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'a'</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> |</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'b'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">, options</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583">?:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> {</span></span> -<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> casing: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'lower'</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> |</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'mixed'</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> |</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'upper'</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">,</span></span> -<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> excludes: readonly AlphaNumericChar[],</span></span> -<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8"> format: </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF">'binary'</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> |</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'css'</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> |</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'decimal'</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583"> |</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF"> 'hex'</span></span> -<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8">}): string</span></span></code></pre> -</div>", - "name": "stringUnionParamMethod", - "parameters": [ - { - "default": undefined, - "description": "<p><code>'a'</code> or <code>'b'</code>.</p> -", - "name": "value", - "type": "'a' | 'b'", - }, - { - "default": undefined, - "description": "<p>The options parameter.</p> -", - "name": "options?", - "type": "{ ... }", - }, - { - "default": undefined, - "description": "<p>The casing parameter.</p> -", - "name": "options.casing?", - "type": "'lower' | 'mixed' | 'upper'", - }, - { - "default": undefined, - "description": "<p>The excludes parameter.</p> -", - "name": "options.excludes?", - "type": "readonly AlphaNumericChar[]", - }, - { - "default": undefined, - "description": "<p>The format parameter.</p> -", - "name": "options.format?", - "type": "'binary' | 'css' | 'decimal' | 'hex'", - }, - ], - "returns": "string", - "seeAlsos": [], - "since": "Missing", - "sourcePath": "test/scripts/apidoc/signature.example.ts#L138", - "throws": undefined, -} -`; diff --git a/test/scripts/apidoc/module.spec.ts b/test/scripts/apidoc/module.spec.ts deleted file mode 100644 index 5e55afea..00000000 --- a/test/scripts/apidoc/module.spec.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { beforeAll, describe, expect, it } from 'vitest'; -import { initMarkdownRenderer } from '../../../scripts/apidoc/markdown'; -import { analyzeModule } from '../../../scripts/apidoc/module-methods'; -import * as ModuleTests from './module.example'; -import { loadExampleModules } from './utils'; - -beforeAll(initMarkdownRenderer); -const modules = await loadExampleModules(); - -describe('module', () => { - describe('analyzeModule()', () => { - it('dummy dependency to rerun the test if the example changes', () => { - expect(Object.keys(ModuleTests)).not.toEqual([]); - }); - - it('expected and actual modules are equal', () => { - expect(Object.keys(modules).sort()).toMatchSnapshot(); - }); - - it.each(Object.entries(modules))('%s', (_, module) => { - const actual = analyzeModule(module); - - expect(actual).toMatchSnapshot(); - }); - }); -}); diff --git a/test/scripts/apidoc/signature.debug.ts b/test/scripts/apidoc/signature.debug.ts deleted file mode 100644 index 704b629c..00000000 --- a/test/scripts/apidoc/signature.debug.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * This file exists, because vitest doesn't allow me to debug code outside of src and test. - * And it's easier to test these features independently from the main project. - */ -import { initMarkdownRenderer } from '../../../scripts/apidoc/markdown'; -import { analyzeSignature } from '../../../scripts/apidoc/signature'; -import { loadExampleMethods } from './utils'; - -/* Run with `pnpm tsx test/scripts/apidoc/signature.debug.ts` */ - -await initMarkdownRenderer(); -const methods = await loadExampleMethods(); -for (const [name, method] of Object.entries(methods)) { - console.log('Analyzing:', name); - const result = await analyzeSignature(method, '', method.name); - console.log('Result:', result); -} diff --git a/test/scripts/apidoc/signature.spec.ts b/test/scripts/apidoc/signature.spec.ts deleted file mode 100644 index 51935fcf..00000000 --- a/test/scripts/apidoc/signature.spec.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { beforeAll, describe, expect, it } from 'vitest'; -import { initMarkdownRenderer } from '../../../scripts/apidoc/markdown'; -import { analyzeSignature } from '../../../scripts/apidoc/signature'; -import { SignatureTest } from './signature.example'; -import { loadExampleMethods } from './utils'; - -beforeAll(initMarkdownRenderer); -const methods = await loadExampleMethods(); - -describe('signature', () => { - describe('analyzeSignature()', () => { - it('dummy dependency to rerun the test if the example changes', () => { - expect(new SignatureTest()).toBeTruthy(); - }); - - it('expected and actual methods are equal', () => { - expect(Object.keys(methods)).toMatchSnapshot(); - }); - - it.each(Object.entries(methods))('%s', async (name, signature) => { - const actual = await analyzeSignature(signature, '', name); - - expect(actual).toMatchSnapshot(); - }); - }); -}); diff --git a/test/scripts/apidoc/utils.ts b/test/scripts/apidoc/utils.ts deleted file mode 100644 index 2752f25b..00000000 --- a/test/scripts/apidoc/utils.ts +++ /dev/null @@ -1,66 +0,0 @@ -import type { - DeclarationReflection, - SignatureReflection, - TypeDocOptions, -} from 'typedoc'; -import { - loadProject, - selectApiMethodSignatures, - selectApiModules, -} from '../../../scripts/apidoc/typedoc'; -import { mapByName } from '../../../scripts/apidoc/utils'; - -/** - * Returns a record with the (Module-Name -> (Method-Name -> Method-Signature)) for the project. - * - * @param options The TypeDoc options. - * @param includeTestModules Whether to include the test modules. - */ -export async function loadProjectModules( - options?: Partial<TypeDocOptions>, - includeTestModules = false -): Promise< - Record<string, [DeclarationReflection, Record<string, SignatureReflection>]> -> { - const [, project] = await loadProject(options); - - const modules = selectApiModules(project, includeTestModules); - - return mapByName(modules, (m) => [m, selectApiMethodSignatures(m)]); -} - -/** - * Loads the example methods using TypeDoc. - */ -export async function loadExampleMethods(): Promise< - Record<string, SignatureReflection> -> { - const modules = await loadProjectModules( - { - entryPoints: ['test/scripts/apidoc/signature.example.ts'], - }, - true - ); - return modules['SignatureTest'][1]; -} - -/** - * Loads the example modules using TypeDoc. - */ -export async function loadExampleModules(): Promise< - Record<string, DeclarationReflection> -> { - const modules = await loadProjectModules( - { - entryPoints: ['test/scripts/apidoc/module.example.ts'], - }, - true - ); - - const result: Record<string, DeclarationReflection> = {}; - for (const key in modules) { - result[key] = modules[key][0]; - } - - return result; -} diff --git a/test/scripts/apidoc/verify-jsdoc-tags.spec.ts b/test/scripts/apidoc/verify-jsdoc-tags.spec.ts deleted file mode 100644 index da34316e..00000000 --- a/test/scripts/apidoc/verify-jsdoc-tags.spec.ts +++ /dev/null @@ -1,315 +0,0 @@ -import { existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'; -import { dirname, resolve } from 'node:path'; -import { fileURLToPath } from 'node:url'; -import type { ReflectionType, SomeType } from 'typedoc'; -import validator from 'validator'; -import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'; -import { initMarkdownRenderer } from '../../../scripts/apidoc/markdown'; -import { analyzeSignature } from '../../../scripts/apidoc/signature'; -import { - MISSING_DESCRIPTION, - extractDeprecated, - extractDescription, - extractJoinedRawExamples, - extractModuleFieldName, - extractRawDefault, - extractSeeAlsos, - extractSince, - extractSummaryDefault, - extractTagContent, -} from '../../../scripts/apidoc/typedoc'; -import { loadProjectModules } from './utils'; - -// This test ensures, that every method -// - has working examples -// - running these do not log anything, unless the method is deprecated - -beforeAll(initMarkdownRenderer); - -const tempDir = resolve(dirname(fileURLToPath(import.meta.url)), 'temp'); - -afterAll(() => { - // Remove temp folder - if (existsSync(tempDir)) { - rmSync(tempDir, { recursive: true }); - } -}); - -const modules = await loadProjectModules(); - -function resolveDirToModule(moduleName: string): string { - return resolve(tempDir, moduleName); -} - -function resolvePathToMethodFile( - moduleName: string, - methodName: string -): string { - const dir = resolveDirToModule(moduleName); - return resolve(dir, `${methodName}.ts`); -} - -const allowedReferences = new Set( - Object.values(modules).flatMap(([module, methods]) => { - const moduleFieldName = extractModuleFieldName(module); - return Object.keys(methods).map( - (methodName) => `faker.${moduleFieldName}.${methodName}` - ); - }) -); -const allowedLinks = new Set( - Object.values(modules).flatMap(([module, methods]) => { - const moduleFieldName = extractModuleFieldName(module); - return [ - `/api/${moduleFieldName}.html`, - ...Object.keys(methods).map( - (methodName) => - `/api/${moduleFieldName}.html#${methodName.toLowerCase()}` - ), - ]; - }) -); - -function assertDescription(description: string, isHtml: boolean): void { - const linkRegexp = isHtml ? /(href)="([^"]+)"/g : /\[([^\]]+)\]\(([^)]+)\)/g; - const links = [...description.matchAll(linkRegexp)].map((m) => m[2]); - - for (const link of links) { - if (!isHtml) { - expect(link).toMatch(/^https?:\/\//); - expect(link).toSatisfy(validator.isURL); - } - - if (isHtml ? link.startsWith('/api/') : link.includes('fakerjs.dev/api/')) { - expect(allowedLinks, `${link} to point to a valid target`).toContain( - link.replace(/.*fakerjs.dev\//, '/') - ); - } - } -} - -// keep in sync with analyzeParameterOptions -function assertNestedParameterDefault( - name: string, - parameterType?: SomeType -): void { - if (!parameterType) { - return; - } - - switch (parameterType.type) { - case 'array': { - return assertNestedParameterDefault( - `${name}[]`, - parameterType.elementType - ); - } - - case 'union': { - for (const type of parameterType.types) { - assertNestedParameterDefault(name, type); - } - - return; - } - - case 'reflection': { - for (const property of parameterType.declaration.children ?? []) { - const reflection = property.comment - ? property - : (property.type as ReflectionType)?.declaration?.signatures?.[0]; - const comment = reflection?.comment; - const tagDefault = extractRawDefault({ comment }) || undefined; - const summaryDefault = extractSummaryDefault(comment, false); - - if (summaryDefault) { - expect( - tagDefault, - `Expect jsdoc summary default and @default for ${name}.${property.name} to be the same` - ).toBe(summaryDefault); - } - } - - return; - } - - case 'typeOperator': { - return assertNestedParameterDefault(name, parameterType.target); - } - - default: { - return; - } - } -} - -describe('verify JSDoc tags', () => { - describe.each(Object.entries(modules))( - '%s', - (moduleName, [module, methodsByName]) => { - describe('verify module', () => { - it('verify description', () => { - const description = extractDescription(module); - assertDescription(description, false); - }); - }); - - describe.each(Object.entries(methodsByName))( - '%s', - (methodName, signature) => { - beforeAll(() => { - // Write temp files to disk - - // Extract examples and make them runnable - const examples = extractJoinedRawExamples(signature) ?? ''; - - // Save examples to a file to run them later in the specific tests - const dir = resolveDirToModule(moduleName); - mkdirSync(dir, { recursive: true }); - - const path = resolvePathToMethodFile(moduleName, methodName); - const imports = [ - ...new Set(examples.match(/(?<!\.)faker[^.]*(?=\.)/g)), - ]; - writeFileSync( - path, - `import { ${imports.join( - ', ' - )} } from '../../../../../src';\n\n${examples}` - ); - }); - - it('verify description', () => { - const description = extractDescription(signature); - assertDescription(description, false); - }); - - it('verify @example tag', async () => { - // Extract the examples - const examples = extractJoinedRawExamples(signature); - - expect( - examples, - `${moduleName}.${methodName} to have examples` - ).not.toBe(''); - - // Grab path to example file - const path = resolvePathToMethodFile(moduleName, methodName); - - // Executing the examples should not throw - await expect( - import(`${path}?scope=example`) - ).resolves.toBeDefined(); - }); - - // This only checks whether the whole method is deprecated or not - // It does not check whether the method is deprecated for a specific set of arguments - it('verify @deprecated tag', async () => { - // Grab path to example file - const path = resolvePathToMethodFile(moduleName, methodName); - - const consoleWarnSpy = vi.spyOn(console, 'warn'); - - // Run the examples - await import(`${path}?scope=deprecated`); - - // Verify that deprecated methods log a warning - const deprecatedFlag = extractDeprecated(signature) !== undefined; - if (deprecatedFlag) { - expect(consoleWarnSpy).toHaveBeenCalled(); - expect( - extractTagContent('@deprecated', signature).join(''), - '@deprecated tag without message' - ).not.toBe(''); - } else { - expect(consoleWarnSpy).not.toHaveBeenCalled(); - } - }); - - it('verify @param tags', async () => { - // This must run before analyzeSignature - for (const param of signature.parameters ?? []) { - const type = param.type; - const paramDefault = param.defaultValue; - const commentDefault = extractSummaryDefault( - param.comment, - false - ); - if (paramDefault) { - if ( - /^{.*}$/.test(paramDefault) || - paramDefault.includes('\n') - ) { - expect(commentDefault).toBeUndefined(); - } else { - expect( - commentDefault, - `Expect '${param.name}'s js implementation default to be the same as the jsdoc summary default.` - ).toBe(paramDefault); - } - } - - assertNestedParameterDefault(param.name, type); - } - - for (const param of ( - await analyzeSignature(signature, '', methodName) - ).parameters) { - const { name, description } = param; - const plainDescription = description - .replaceAll(/<[^>]+>/g, '') - .trim(); - expect( - plainDescription, - `Expect param ${name} to have a description` - ).not.toBe(MISSING_DESCRIPTION); - assertDescription(description, true); - } - }); - - it('verify @see tags', () => { - for (const link of extractSeeAlsos(signature)) { - if (link.startsWith('faker.')) { - // Expected @see faker.xxx.yyy() - expect(link, 'Expect method reference to contain ()').toContain( - '(' - ); - expect(link, 'Expect method reference to contain ()').toContain( - ')' - ); - expect( - link, - "Expect method reference to have a ': ' after the parenthesis" - ).toContain('): '); - expect( - link, - 'Expect method reference to have a description starting with a capital letter' - ).toMatch(/\): [A-Z]/); - expect( - link, - 'Expect method reference to start with a standard description phrase' - ).toMatch( - /\): (?:For generating |For more information about |For using |For the replacement method)/ - ); - expect( - link, - 'Expect method reference to have a description ending with a dot' - ).toMatch(/\.$/); - expect(allowedReferences).toContain(link.replace(/\(.*/, '')); - } - } - }); - - it('verify @since tag', () => { - const since = extractSince(signature); - expect(since, '@since to be present').toBeTruthy(); - expect(since).not.toBe(MISSING_DESCRIPTION); - expect(since, '@since to be a valid semver').toSatisfy( - validator.isSemVer - ); - }); - } - ); - } - ); -}); diff --git a/test/scripts/apidoc/.gitignore b/test/scripts/apidocs/.gitignore index a6d7ecd9..a6d7ecd9 100644 --- a/test/scripts/apidoc/.gitignore +++ b/test/scripts/apidocs/.gitignore diff --git a/test/scripts/apidocs/__snapshots__/class.spec.ts.snap b/test/scripts/apidocs/__snapshots__/class.spec.ts.snap new file mode 100644 index 00000000..6461169d --- /dev/null +++ b/test/scripts/apidocs/__snapshots__/class.spec.ts.snap @@ -0,0 +1,75 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`class > expected and actual modules are equal 1`] = ` +[ + "ModuleDeprecationTest", + "ModuleExampleTest", + "ModuleFakerJsLinkTest", + "ModuleNextFakerJsLinkTest", + "ModuleSimpleTest", +] +`; + +exports[`class > processClass(ModuleDeprecationTest) 1`] = ` +{ + "camelTitle": "moduleDeprecationTest", + "category": undefined, + "deprecated": "Well, this is deprecated.", + "description": "This is a description for a module with a code example.", + "examples": [], + "methods": [], + "title": "ModuleDeprecationTest", +} +`; + +exports[`class > processClass(ModuleExampleTest) 1`] = ` +{ + "camelTitle": "moduleExampleTest", + "category": undefined, + "deprecated": undefined, + "description": "This is a description for a module with a code example.", + "examples": [ + "new ModuleExampleTest()", + ], + "methods": [], + "title": "ModuleExampleTest", +} +`; + +exports[`class > processClass(ModuleFakerJsLinkTest) 1`] = ` +{ + "camelTitle": "moduleFakerJsLinkTest", + "category": undefined, + "deprecated": undefined, + "description": "Description with a link to our [website](https://fakerjs.dev/) +and [api docs](https://fakerjs.dev/api/).", + "examples": [], + "methods": [], + "title": "ModuleFakerJsLinkTest", +} +`; + +exports[`class > processClass(ModuleNextFakerJsLinkTest) 1`] = ` +{ + "camelTitle": "moduleNextFakerJsLinkTest", + "category": undefined, + "deprecated": undefined, + "description": "Description with a link to our [website](https://next.fakerjs.dev/) +and [api docs](https://next.fakerjs.dev/api/).", + "examples": [], + "methods": [], + "title": "ModuleNextFakerJsLinkTest", +} +`; + +exports[`class > processClass(ModuleSimpleTest) 1`] = ` +{ + "camelTitle": "moduleSimpleTest", + "category": undefined, + "deprecated": undefined, + "description": "A simple module without anything special.", + "examples": [], + "methods": [], + "title": "ModuleSimpleTest", +} +`; diff --git a/test/scripts/apidocs/__snapshots__/method.spec.ts.snap b/test/scripts/apidocs/__snapshots__/method.spec.ts.snap new file mode 100644 index 00000000..5a5518e4 --- /dev/null +++ b/test/scripts/apidocs/__snapshots__/method.spec.ts.snap @@ -0,0 +1,1671 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`method > expected and actual methods are equal 1`] = ` +[ + "complexArrayParameter", + "defaultBooleanParamMethod", + "functionParamMethod", + "literalUnionParamMethod", + "methodWithDeprecated", + "methodWithDeprecatedOption", + "methodWithExample", + "methodWithMultipleSeeMarkers", + "methodWithMultipleSeeMarkersAndBackticks", + "methodWithMultipleThrows", + "methodWithSinceMarker", + "methodWithThrows", + "multiParamMethod", + "noParamMethod", + "optionalStringParamMethod", + "optionsInlineParamMethodWithDefaults", + "optionsInterfaceParamMethodWithDefaults", + "optionsParamMethod", + "optionsTypeParamMethodWithDefaults", + "recordParamMethod", + "requiredNumberParamMethod", + "stringUnionParamMethod", +] +`; + +exports[`method > processMethodLike(complexArrayParameter) 1`] = ` +{ + "name": "complexArrayParameter", + "signatures": [ + { + "deprecated": undefined, + "description": "Complex array parameter.", + "examples": [], + "parameters": [ + { + "default": undefined, + "description": "The type of the entries to pick from.", + "name": "<T>", + "type": { + "text": "any", + "type": "simple", + }, + }, + { + "default": undefined, + "description": "Array to pick the value from.", + "name": "array", + "type": { + "text": "Array<{ ... }>", + "type": "generic", + "typeParameters": [ + { + "text": "{ ... }", + "type": "simple", + }, + ], + }, + }, + { + "default": undefined, + "description": "The value to pick.", + "name": "array[].value", + "type": { + "resolvedType": { + "text": "any", + "type": "simple", + }, + "text": "T", + "type": "shadow", + }, + }, + { + "default": undefined, + "description": "The weight of the value.", + "name": "array[].weight", + "type": { + "text": "number", + "type": "simple", + }, + }, + ], + "returns": { + "resolvedType": { + "text": "any", + "type": "simple", + }, + "text": "T", + "type": "shadow", + }, + "seeAlsos": [], + "signature": "function complexArrayParameter<T>( + array: ReadonlyArray<{ + weight: number; + value: T; + }> + ): T;", + "since": "1.0.0", + "throws": [], + }, + ], + "source": { + "column": -1, + "filePath": "test/scripts/apidocs/method.example.ts", + "line": -1, + }, +} +`; + +exports[`method > processMethodLike(defaultBooleanParamMethod) 1`] = ` +{ + "name": "defaultBooleanParamMethod", + "signatures": [ + { + "deprecated": undefined, + "description": "Test with a default parameter.", + "examples": [], + "parameters": [ + { + "default": "true", + "description": "The boolean parameter.", + "name": "c", + "type": { + "text": "boolean", + "type": "simple", + }, + }, + ], + "returns": { + "text": "number", + "type": "simple", + }, + "seeAlsos": [], + "signature": "function defaultBooleanParamMethod(c: boolean = true): number;", + "since": "1.0.0", + "throws": [], + }, + ], + "source": { + "column": -1, + "filePath": "test/scripts/apidocs/method.example.ts", + "line": -1, + }, +} +`; + +exports[`method > processMethodLike(functionParamMethod) 1`] = ` +{ + "name": "functionParamMethod", + "signatures": [ + { + "deprecated": undefined, + "description": "Test with a function parameters.", + "examples": [], + "parameters": [ + { + "default": undefined, + "description": "The function parameter.", + "name": "fn", + "type": { + "text": "(a: string) => number", + "type": "simple", + }, + }, + ], + "returns": { + "text": "number", + "type": "simple", + }, + "seeAlsos": [], + "signature": "function functionParamMethod(fn: (a: string) => number): number;", + "since": "1.0.0", + "throws": [], + }, + ], + "source": { + "column": -1, + "filePath": "test/scripts/apidocs/method.example.ts", + "line": -1, + }, +} +`; + +exports[`method > processMethodLike(literalUnionParamMethod) 1`] = ` +{ + "name": "literalUnionParamMethod", + "signatures": [ + { + "deprecated": undefined, + "description": "Test with LiteralUnion.", + "examples": [], + "parameters": [ + { + "default": undefined, + "description": "\`'a'\` or \`'b'\`.", + "name": "value", + "type": { + "text": "'a' | 'b' | string", + "type": "union", + "types": [ + { + "text": "'a' | 'b'", + "type": "union", + "types": [ + { + "text": "'a'", + "type": "simple", + }, + { + "text": "'b'", + "type": "simple", + }, + ], + }, + { + "text": "string", + "type": "simple", + }, + ], + }, + }, + { + "default": undefined, + "description": "\`'a'\` or \`'b'\`.", + "name": "namedValue", + "type": { + "text": "AB | string", + "type": "union", + "types": [ + { + "resolvedType": { + "text": "'a' | 'b'", + "type": "union", + "types": [ + { + "text": "'a'", + "type": "simple", + }, + { + "text": "'b'", + "type": "simple", + }, + ], + }, + "text": "AB", + "type": "shadow", + }, + { + "text": "string", + "type": "simple", + }, + ], + }, + }, + { + "default": undefined, + "description": "Array of \`'a'\` or \`'b'\`.", + "name": "array", + "type": { + "text": "Array<'a' | 'b' | string>", + "type": "generic", + "typeParameters": [ + { + "text": "'a' | 'b' | string", + "type": "union", + "types": [ + { + "text": "'a' | 'b'", + "type": "union", + "types": [ + { + "text": "'a'", + "type": "simple", + }, + { + "text": "'b'", + "type": "simple", + }, + ], + }, + { + "text": "string", + "type": "simple", + }, + ], + }, + ], + }, + }, + { + "default": undefined, + "description": "Array of \`'a'\` or \`'b'\`.", + "name": "namedArray", + "type": { + "text": "Array<AB | string>", + "type": "generic", + "typeParameters": [ + { + "text": "AB | string", + "type": "union", + "types": [ + { + "resolvedType": { + "text": "'a' | 'b'", + "type": "union", + "types": [ + { + "text": "'a'", + "type": "simple", + }, + { + "text": "'b'", + "type": "simple", + }, + ], + }, + "text": "AB", + "type": "shadow", + }, + { + "text": "string", + "type": "simple", + }, + ], + }, + ], + }, + }, + { + "default": undefined, + "description": "Value \`'a'\` or \`'b'\` or an array thereof.", + "name": "mixed", + "type": { + "text": "'a' | 'b' | string | Array<'a' | 'b' | string>", + "type": "union", + "types": [ + { + "text": "'a'", + "type": "simple", + }, + { + "text": "'b'", + "type": "simple", + }, + { + "text": "string & { zz_IGNORE_ME?: undefined; }", + "type": "simple", + }, + { + "text": "Array<'a' | 'b' | string>", + "type": "generic", + "typeParameters": [ + { + "text": "'a' | 'b' | string", + "type": "union", + "types": [ + { + "text": "'a' | 'b'", + "type": "union", + "types": [ + { + "text": "'a'", + "type": "simple", + }, + { + "text": "'b'", + "type": "simple", + }, + ], + }, + { + "text": "string", + "type": "simple", + }, + ], + }, + ], + }, + ], + }, + }, + { + "default": undefined, + "description": "Value \`'a'\` or \`'b'\` or an array thereof.", + "name": "namedMixed", + "type": { + "text": "'a' | 'b' | string | Array<AB | string>", + "type": "union", + "types": [ + { + "text": "'a'", + "type": "simple", + }, + { + "text": "'b'", + "type": "simple", + }, + { + "text": "string & { zz_IGNORE_ME?: undefined; }", + "type": "simple", + }, + { + "text": "Array<AB | string>", + "type": "generic", + "typeParameters": [ + { + "text": "AB | string", + "type": "union", + "types": [ + { + "resolvedType": { + "text": "'a' | 'b'", + "type": "union", + "types": [ + { + "text": "'a'", + "type": "simple", + }, + { + "text": "'b'", + "type": "simple", + }, + ], + }, + "text": "AB", + "type": "shadow", + }, + { + "text": "string", + "type": "simple", + }, + ], + }, + ], + }, + ], + }, + }, + ], + "returns": { + "text": "string", + "type": "simple", + }, + "seeAlsos": [], + "signature": "function literalUnionParamMethod( + value: LiteralUnion<'a' | 'b'>, + namedValue: LiteralUnion<AB>, + array: ReadonlyArray<LiteralUnion<'a' | 'b'>>, + namedArray: ReadonlyArray<LiteralUnion<AB>>, + mixed: LiteralUnion<'a' | 'b'> | ReadonlyArray<LiteralUnion<'a' | 'b'>>, + namedMixed: ReadonlyArray<LiteralUnion<AB>> | LiteralUnion<AB> + ): string;", + "since": "1.0.0", + "throws": [], + }, + ], + "source": { + "column": -1, + "filePath": "test/scripts/apidocs/method.example.ts", + "line": -1, + }, +} +`; + +exports[`method > processMethodLike(methodWithDeprecated) 1`] = ` +{ + "name": "methodWithDeprecated", + "signatures": [ + { + "deprecated": "do something else", + "description": "Test with deprecated and see marker.", + "examples": [], + "parameters": [], + "returns": { + "text": "number", + "type": "simple", + }, + "seeAlsos": [ + "test.apidocs.methodWithExample()", + ], + "signature": "function methodWithDeprecated(): number;", + "since": "1.0.0", + "throws": [], + }, + ], + "source": { + "column": -1, + "filePath": "test/scripts/apidocs/method.example.ts", + "line": -1, + }, +} +`; + +exports[`method > processMethodLike(methodWithDeprecatedOption) 1`] = ` +{ + "name": "methodWithDeprecatedOption", + "signatures": [ + { + "deprecated": undefined, + "description": "Test with deprecated option.", + "examples": [], + "parameters": [ + { + "default": undefined, + "description": "The options.", + "name": "option", + "type": { + "text": "{ ... }", + "type": "simple", + }, + }, + { + "default": undefined, + "description": "Some deprecated option. + +**DEPRECATED:** do something else.", + "name": "option.a", + "type": { + "text": "string", + "type": "simple", + }, + }, + { + "default": undefined, + "description": "Some other deprecated option. + +**DEPRECATED:** do something else.", + "name": "option.b", + "type": { + "text": "() => number", + "type": "simple", + }, + }, + { + "default": undefined, + "description": "Some other option.", + "name": "option.c", + "type": { + "text": "number", + "type": "simple", + }, + }, + ], + "returns": { + "text": "number", + "type": "simple", + }, + "seeAlsos": [], + "signature": "function methodWithDeprecatedOption(option: { + a: string; + b: () => number; + c: number; + }): number;", + "since": "1.0.0", + "throws": [], + }, + ], + "source": { + "column": -1, + "filePath": "test/scripts/apidocs/method.example.ts", + "line": -1, + }, +} +`; + +exports[`method > processMethodLike(methodWithExample) 1`] = ` +{ + "name": "methodWithExample", + "signatures": [ + { + "deprecated": undefined, + "description": "Test with example marker.", + "examples": [ + "test.apidocs.methodWithExample() // 0", + ], + "parameters": [], + "returns": { + "text": "number", + "type": "simple", + }, + "seeAlsos": [], + "signature": "function methodWithExample(): number;", + "since": "1.0.0", + "throws": [], + }, + ], + "source": { + "column": -1, + "filePath": "test/scripts/apidocs/method.example.ts", + "line": -1, + }, +} +`; + +exports[`method > processMethodLike(methodWithMultipleSeeMarkers) 1`] = ` +{ + "name": "methodWithMultipleSeeMarkers", + "signatures": [ + { + "deprecated": undefined, + "description": "Test with multiple see markers.", + "examples": [], + "parameters": [], + "returns": { + "text": "number", + "type": "simple", + }, + "seeAlsos": [ + "test.apidocsmethodWithExample()", + "test.apidocsmethodWithDeprecated()", + ], + "signature": "function methodWithMultipleSeeMarkers(): number;", + "since": "1.0.0", + "throws": [], + }, + ], + "source": { + "column": -1, + "filePath": "test/scripts/apidocs/method.example.ts", + "line": -1, + }, +} +`; + +exports[`method > processMethodLike(methodWithMultipleSeeMarkersAndBackticks) 1`] = ` +{ + "name": "methodWithMultipleSeeMarkersAndBackticks", + "signatures": [ + { + "deprecated": undefined, + "description": "Test with multiple see markers and backticks.", + "examples": [], + "parameters": [], + "returns": { + "text": "number", + "type": "simple", + }, + "seeAlsos": [ + "test.apidocsmethodWithExample() with parameter \`foo\`.", + "test.apidocsmethodWithDeprecated() with parameter \`bar\` and \`baz\`.", + ], + "signature": "function methodWithMultipleSeeMarkersAndBackticks(): number;", + "since": "1.0.0", + "throws": [], + }, + ], + "source": { + "column": -1, + "filePath": "test/scripts/apidocs/method.example.ts", + "line": -1, + }, +} +`; + +exports[`method > processMethodLike(methodWithMultipleThrows) 1`] = ` +{ + "name": "methodWithMultipleThrows", + "signatures": [ + { + "deprecated": undefined, + "description": "Test with multiple throws.", + "examples": [], + "parameters": [], + "returns": { + "text": "number", + "type": "simple", + }, + "seeAlsos": [], + "signature": "function methodWithMultipleThrows(): number;", + "since": "1.0.0", + "throws": [ + "First error case.", + "Another error case.", + ], + }, + ], + "source": { + "column": -1, + "filePath": "test/scripts/apidocs/method.example.ts", + "line": -1, + }, +} +`; + +exports[`method > processMethodLike(methodWithSinceMarker) 1`] = ` +{ + "name": "methodWithSinceMarker", + "signatures": [ + { + "deprecated": undefined, + "description": "Test with since marker.", + "examples": [], + "parameters": [], + "returns": { + "text": "number", + "type": "simple", + }, + "seeAlsos": [], + "signature": "function methodWithSinceMarker(): number;", + "since": "1.0.0", + "throws": [], + }, + ], + "source": { + "column": -1, + "filePath": "test/scripts/apidocs/method.example.ts", + "line": -1, + }, +} +`; + +exports[`method > processMethodLike(methodWithThrows) 1`] = ` +{ + "name": "methodWithThrows", + "signatures": [ + { + "deprecated": undefined, + "description": "Test with throws.", + "examples": [], + "parameters": [], + "returns": { + "text": "number", + "type": "simple", + }, + "seeAlsos": [], + "signature": "function methodWithThrows(): number;", + "since": "1.0.0", + "throws": [ + "Everytime.", + ], + }, + ], + "source": { + "column": -1, + "filePath": "test/scripts/apidocs/method.example.ts", + "line": -1, + }, +} +`; + +exports[`method > processMethodLike(multiParamMethod) 1`] = ` +{ + "name": "multiParamMethod", + "signatures": [ + { + "deprecated": undefined, + "description": "Test with multiple parameters.", + "examples": [], + "parameters": [ + { + "default": undefined, + "description": "The number parameter.", + "name": "a", + "type": { + "text": "number", + "type": "simple", + }, + }, + { + "default": undefined, + "description": "The string parameter.", + "name": "b?", + "type": { + "text": "string", + "type": "simple", + }, + }, + { + "default": "true", + "description": "The boolean parameter.", + "name": "c", + "type": { + "text": "boolean", + "type": "simple", + }, + }, + ], + "returns": { + "text": "number", + "type": "simple", + }, + "seeAlsos": [], + "signature": "function multiParamMethod(a: number, b?: string, c: boolean = true): number;", + "since": "1.0.0", + "throws": [], + }, + ], + "source": { + "column": -1, + "filePath": "test/scripts/apidocs/method.example.ts", + "line": -1, + }, +} +`; + +exports[`method > processMethodLike(noParamMethod) 1`] = ` +{ + "name": "noParamMethod", + "signatures": [ + { + "deprecated": undefined, + "description": "Test with no parameters.", + "examples": [], + "parameters": [], + "returns": { + "text": "number", + "type": "simple", + }, + "seeAlsos": [], + "signature": "function noParamMethod(): number;", + "since": "1.0.0", + "throws": [], + }, + ], + "source": { + "column": -1, + "filePath": "test/scripts/apidocs/method.example.ts", + "line": -1, + }, +} +`; + +exports[`method > processMethodLike(optionalStringParamMethod) 1`] = ` +{ + "name": "optionalStringParamMethod", + "signatures": [ + { + "deprecated": undefined, + "description": "Test with an optional parameter.", + "examples": [], + "parameters": [ + { + "default": undefined, + "description": "The string parameter.", + "name": "b?", + "type": { + "text": "string", + "type": "simple", + }, + }, + ], + "returns": { + "text": "number", + "type": "simple", + }, + "seeAlsos": [], + "signature": "function optionalStringParamMethod(b?: string): number;", + "since": "1.0.0", + "throws": [], + }, + ], + "source": { + "column": -1, + "filePath": "test/scripts/apidocs/method.example.ts", + "line": -1, + }, +} +`; + +exports[`method > processMethodLike(optionsInlineParamMethodWithDefaults) 1`] = ` +{ + "name": "optionsInlineParamMethodWithDefaults", + "signatures": [ + { + "deprecated": undefined, + "description": "Test with a function parameters (inline types) with defaults.", + "examples": [], + "parameters": [ + { + "default": "{ value: 1 }", + "description": "Parameter with signature default. +It also has a more complex description.", + "name": "a", + "type": { + "text": "{ ... }", + "type": "simple", + }, + }, + { + "default": undefined, + "description": "The number parameter.", + "name": "a.value?", + "type": { + "text": "number", + "type": "simple", + }, + }, + { + "default": undefined, + "description": "Parameter with jsdocs default. + +It also has a more complex description. + +Defaults to \`{ value: 1 }\`.", + "name": "b", + "type": { + "text": "{ ... }", + "type": "simple", + }, + }, + { + "default": undefined, + "description": "The number parameter.", + "name": "b.value?", + "type": { + "text": "number", + "type": "simple", + }, + }, + { + "default": undefined, + "description": "Parameter with inner jsdocs default.", + "name": "c", + "type": { + "text": "{ ... }", + "type": "simple", + }, + }, + { + "default": undefined, + "description": "The number parameter.", + "name": "c.value?", + "type": { + "text": "number", + "type": "simple", + }, + }, + ], + "returns": { + "text": "number", + "type": "simple", + }, + "seeAlsos": [], + "signature": "function optionsInlineParamMethodWithDefaults( + a: { + value?: number; + } = { value: 1 }, + b: { + value?: number; + }, + c: { + value?: number; + } + ): number;", + "since": "1.0.0", + "throws": [], + }, + ], + "source": { + "column": -1, + "filePath": "test/scripts/apidocs/method.example.ts", + "line": -1, + }, +} +`; + +exports[`method > processMethodLike(optionsInterfaceParamMethodWithDefaults) 1`] = ` +{ + "name": "optionsInterfaceParamMethodWithDefaults", + "signatures": [ + { + "deprecated": undefined, + "description": "Test with a function parameters with defaults.", + "examples": [], + "parameters": [ + { + "default": "{ value: 1 }", + "description": "Parameter with signature default.", + "name": "a", + "type": { + "text": "ParameterOptionsInterfaceA", + "type": "simple", + }, + }, + { + "default": undefined, + "description": "Parameter with jsdocs default. Defaults to \`{ value: 1 }\`.", + "name": "b", + "type": { + "text": "ParameterOptionsInterfaceB", + "type": "simple", + }, + }, + { + "default": undefined, + "description": "Parameter with inner jsdocs default.", + "name": "c", + "type": { + "text": "ParameterOptionsInterfaceC", + "type": "simple", + }, + }, + ], + "returns": { + "text": "number", + "type": "simple", + }, + "seeAlsos": [], + "signature": "function optionsInterfaceParamMethodWithDefaults( + a: ParameterOptionsInterfaceA = { value: 1 }, + b: ParameterOptionsInterfaceB, + c: ParameterOptionsInterfaceC + ): number;", + "since": "1.0.0", + "throws": [], + }, + ], + "source": { + "column": -1, + "filePath": "test/scripts/apidocs/method.example.ts", + "line": -1, + }, +} +`; + +exports[`method > processMethodLike(optionsParamMethod) 1`] = ` +{ + "name": "optionsParamMethod", + "signatures": [ + { + "deprecated": undefined, + "description": "Test with an options parameter.", + "examples": [], + "parameters": [ + { + "default": undefined, + "description": "The options parameter.", + "name": "options", + "type": { + "text": "{ ... }", + "type": "simple", + }, + }, + { + "default": undefined, + "description": "The number parameter.", + "name": "options.a", + "type": { + "text": "number", + "type": "simple", + }, + }, + { + "default": undefined, + "description": "The string parameter.", + "name": "options.b?", + "type": { + "text": "string", + "type": "simple", + }, + }, + { + "default": undefined, + "description": "The boolean parameter.", + "name": "options.c", + "type": { + "text": "boolean", + "type": "simple", + }, + }, + { + "default": undefined, + "description": "The method parameter.", + "name": "options.d", + "type": { + "text": "() => string", + "type": "simple", + }, + }, + { + "default": "'a'", + "description": "A parameter with inline documentation.", + "name": "options.e", + "type": { + "text": "'a' | 'b' | string", + "type": "union", + "types": [ + { + "text": "'a' | 'b'", + "type": "union", + "types": [ + { + "text": "'a'", + "type": "simple", + }, + { + "text": "'b'", + "type": "simple", + }, + ], + }, + { + "text": "string", + "type": "simple", + }, + ], + }, + }, + ], + "returns": { + "text": "number", + "type": "simple", + }, + "seeAlsos": [], + "signature": "function optionsParamMethod(options: { + a: number; + b?: string; + c: boolean; + d: () => string; + e: LiteralUnion<'a' | 'b'>; + }): number;", + "since": "1.0.0", + "throws": [], + }, + ], + "source": { + "column": -1, + "filePath": "test/scripts/apidocs/method.example.ts", + "line": -1, + }, +} +`; + +exports[`method > processMethodLike(optionsTypeParamMethodWithDefaults) 1`] = ` +{ + "name": "optionsTypeParamMethodWithDefaults", + "signatures": [ + { + "deprecated": undefined, + "description": "Test with a function parameters with defaults.", + "examples": [], + "parameters": [ + { + "default": "{ value: 1 }", + "description": "Parameter with signature default.", + "name": "a", + "type": { + "text": "{ ... }", + "type": "simple", + }, + }, + { + "default": undefined, + "description": "Options value.", + "name": "a.value?", + "type": { + "text": "number", + "type": "simple", + }, + }, + { + "default": undefined, + "description": "Parameter with jsdocs default. Defaults to \`{ value: 1 }\`.", + "name": "b", + "type": { + "text": "{ ... }", + "type": "simple", + }, + }, + { + "default": undefined, + "description": "Options value.", + "name": "b.value?", + "type": { + "text": "number", + "type": "simple", + }, + }, + { + "default": undefined, + "description": "Parameter with inner jsdocs default.", + "name": "c", + "type": { + "text": "{ ... }", + "type": "simple", + }, + }, + { + "default": undefined, + "description": "Options value. Defaults to \`0\`.", + "name": "c.value?", + "type": { + "text": "number", + "type": "simple", + }, + }, + ], + "returns": { + "text": "number", + "type": "simple", + }, + "seeAlsos": [], + "signature": "function optionsTypeParamMethodWithDefaults( + a: ParameterOptionsTypeA = { value: 1 }, + b: ParameterOptionsTypeB, + c: ParameterOptionsTypeC + ): number;", + "since": "1.0.0", + "throws": [], + }, + ], + "source": { + "column": -1, + "filePath": "test/scripts/apidocs/method.example.ts", + "line": -1, + }, +} +`; + +exports[`method > processMethodLike(recordParamMethod) 1`] = ` +{ + "name": "recordParamMethod", + "signatures": [ + { + "deprecated": undefined, + "description": "Test with a Record parameter.", + "examples": [], + "parameters": [ + { + "default": undefined, + "description": "The Record parameter.", + "name": "object", + "type": { + "text": "Record<string, number>", + "type": "simple", + }, + }, + ], + "returns": { + "text": "number", + "type": "simple", + }, + "seeAlsos": [], + "signature": "function recordParamMethod(object: Record<string, number>): number;", + "since": "1.0.0", + "throws": [], + }, + ], + "source": { + "column": -1, + "filePath": "test/scripts/apidocs/method.example.ts", + "line": -1, + }, +} +`; + +exports[`method > processMethodLike(requiredNumberParamMethod) 1`] = ` +{ + "name": "requiredNumberParamMethod", + "signatures": [ + { + "deprecated": undefined, + "description": "Test with a required parameter.", + "examples": [], + "parameters": [ + { + "default": undefined, + "description": "The number parameter.", + "name": "a", + "type": { + "text": "number", + "type": "simple", + }, + }, + ], + "returns": { + "text": "number", + "type": "simple", + }, + "seeAlsos": [], + "signature": "function requiredNumberParamMethod(a: number): number;", + "since": "1.0.0", + "throws": [], + }, + ], + "source": { + "column": -1, + "filePath": "test/scripts/apidocs/method.example.ts", + "line": -1, + }, +} +`; + +exports[`method > processMethodLike(stringUnionParamMethod) 1`] = ` +{ + "name": "stringUnionParamMethod", + "signatures": [ + { + "deprecated": undefined, + "description": "Test with string union.", + "examples": [], + "parameters": [ + { + "default": undefined, + "description": "\`'a'\` or \`'b'\`.", + "name": "value", + "type": { + "text": "'a' | 'b'", + "type": "union", + "types": [ + { + "text": "'a'", + "type": "simple", + }, + { + "text": "'b'", + "type": "simple", + }, + ], + }, + }, + { + "default": undefined, + "description": "The options parameter.", + "name": "options?", + "type": { + "text": "{ ... }", + "type": "simple", + }, + }, + { + "default": undefined, + "description": "The casing parameter.", + "name": "options.casing?", + "type": { + "resolvedType": { + "text": "'lower' | 'upper' | 'mixed'", + "type": "union", + "types": [ + { + "text": "'lower'", + "type": "simple", + }, + { + "text": "'upper'", + "type": "simple", + }, + { + "text": "'mixed'", + "type": "simple", + }, + ], + }, + "text": "Casing", + "type": "shadow", + }, + }, + { + "default": undefined, + "description": "The excludes parameter.", + "name": "options.excludes?", + "type": { + "text": "AlphaNumericChar[]", + "type": "generic", + "typeParameters": [ + { + "resolvedType": { + "text": "'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h' | 'i' | 'j' | 'k' | 'l' | 'm' | 'n' | 'o' | 'p' | 'q' | 'r' | 's' | 't' | 'u' | 'v' | 'w' | 'x' | 'y' | 'z' | 'A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'G' | 'H' | 'I' | 'J' | 'K' | 'L' | 'M' | 'N' | 'O' | 'P' | 'Q' | 'R' | 'S' | 'T' | 'U' | 'V' | 'W' | 'X' | 'Y' | 'Z' | '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'", + "type": "union", + "types": [ + { + "text": "'a'", + "type": "simple", + }, + { + "text": "'b'", + "type": "simple", + }, + { + "text": "'c'", + "type": "simple", + }, + { + "text": "'d'", + "type": "simple", + }, + { + "text": "'e'", + "type": "simple", + }, + { + "text": "'f'", + "type": "simple", + }, + { + "text": "'g'", + "type": "simple", + }, + { + "text": "'h'", + "type": "simple", + }, + { + "text": "'i'", + "type": "simple", + }, + { + "text": "'j'", + "type": "simple", + }, + { + "text": "'k'", + "type": "simple", + }, + { + "text": "'l'", + "type": "simple", + }, + { + "text": "'m'", + "type": "simple", + }, + { + "text": "'n'", + "type": "simple", + }, + { + "text": "'o'", + "type": "simple", + }, + { + "text": "'p'", + "type": "simple", + }, + { + "text": "'q'", + "type": "simple", + }, + { + "text": "'r'", + "type": "simple", + }, + { + "text": "'s'", + "type": "simple", + }, + { + "text": "'t'", + "type": "simple", + }, + { + "text": "'u'", + "type": "simple", + }, + { + "text": "'v'", + "type": "simple", + }, + { + "text": "'w'", + "type": "simple", + }, + { + "text": "'x'", + "type": "simple", + }, + { + "text": "'y'", + "type": "simple", + }, + { + "text": "'z'", + "type": "simple", + }, + { + "text": "'A'", + "type": "simple", + }, + { + "text": "'B'", + "type": "simple", + }, + { + "text": "'C'", + "type": "simple", + }, + { + "text": "'D'", + "type": "simple", + }, + { + "text": "'E'", + "type": "simple", + }, + { + "text": "'F'", + "type": "simple", + }, + { + "text": "'G'", + "type": "simple", + }, + { + "text": "'H'", + "type": "simple", + }, + { + "text": "'I'", + "type": "simple", + }, + { + "text": "'J'", + "type": "simple", + }, + { + "text": "'K'", + "type": "simple", + }, + { + "text": "'L'", + "type": "simple", + }, + { + "text": "'M'", + "type": "simple", + }, + { + "text": "'N'", + "type": "simple", + }, + { + "text": "'O'", + "type": "simple", + }, + { + "text": "'P'", + "type": "simple", + }, + { + "text": "'Q'", + "type": "simple", + }, + { + "text": "'R'", + "type": "simple", + }, + { + "text": "'S'", + "type": "simple", + }, + { + "text": "'T'", + "type": "simple", + }, + { + "text": "'U'", + "type": "simple", + }, + { + "text": "'V'", + "type": "simple", + }, + { + "text": "'W'", + "type": "simple", + }, + { + "text": "'X'", + "type": "simple", + }, + { + "text": "'Y'", + "type": "simple", + }, + { + "text": "'Z'", + "type": "simple", + }, + { + "text": "'0'", + "type": "simple", + }, + { + "text": "'1'", + "type": "simple", + }, + { + "text": "'2'", + "type": "simple", + }, + { + "text": "'3'", + "type": "simple", + }, + { + "text": "'4'", + "type": "simple", + }, + { + "text": "'5'", + "type": "simple", + }, + { + "text": "'6'", + "type": "simple", + }, + { + "text": "'7'", + "type": "simple", + }, + { + "text": "'8'", + "type": "simple", + }, + { + "text": "'9'", + "type": "simple", + }, + ], + }, + "text": "AlphaNumericChar", + "type": "shadow", + }, + ], + }, + }, + { + "default": undefined, + "description": "The format parameter.", + "name": "options.format?", + "type": { + "text": "'hex' | 'css' | 'binary' | 'decimal'", + "type": "union", + "types": [ + { + "text": "'hex'", + "type": "simple", + }, + { + "text": "'css'", + "type": "simple", + }, + { + "text": "'binary'", + "type": "simple", + }, + { + "text": "'decimal'", + "type": "simple", + }, + ], + }, + }, + ], + "returns": { + "text": "string", + "type": "simple", + }, + "seeAlsos": [], + "signature": "function stringUnionParamMethod( + value: 'a' | 'b', + options?: { + casing?: Casing; + format?: 'hex' | ColorFormat; + excludes?: ReadonlyArray<AlphaNumericChar>; + } + ): string;", + "since": "1.0.0", + "throws": [], + }, + ], + "source": { + "column": -1, + "filePath": "test/scripts/apidocs/method.example.ts", + "line": -1, + }, +} +`; diff --git a/test/scripts/apidoc/module.example.ts b/test/scripts/apidocs/class.example.ts index 0e5d9d89..0e5d9d89 100644 --- a/test/scripts/apidoc/module.example.ts +++ b/test/scripts/apidocs/class.example.ts diff --git a/test/scripts/apidocs/class.spec.ts b/test/scripts/apidocs/class.spec.ts new file mode 100644 index 00000000..8ebce847 --- /dev/null +++ b/test/scripts/apidocs/class.spec.ts @@ -0,0 +1,22 @@ +import { describe, expect, it } from 'vitest'; +import { processClass } from '../../../scripts/apidocs/processing/class'; +import * as ModuleTests from './class.example'; +import { loadExampleClasses } from './utils'; + +const modules = loadExampleClasses(); + +describe('class', () => { + it('dummy dependency to rerun the test if the example changes', () => { + expect(Object.keys(ModuleTests)).not.toEqual([]); + }); + + it('expected and actual modules are equal', () => { + expect(Object.keys(modules).sort()).toMatchSnapshot(); + }); + + it.each(Object.entries(modules))('processClass(%s)', (_, module) => { + const actual = processClass(module); + + expect(actual).toMatchSnapshot(); + }); +}); diff --git a/test/scripts/apidoc/signature.example.ts b/test/scripts/apidocs/method.example.ts index c42d2d45..e72f5f3f 100644 --- a/test/scripts/apidoc/signature.example.ts +++ b/test/scripts/apidocs/method.example.ts @@ -74,6 +74,8 @@ export type AB = 'a' | 'b'; export class SignatureTest { /** * Test with no parameters. + * + * @since 1.0.0 */ noParamMethod(): number { return 0; @@ -83,6 +85,8 @@ export class SignatureTest { * Test with a required parameter. * * @param a The number parameter. + * + * @since 1.0.0 */ requiredNumberParamMethod(a: number): number { return a; @@ -92,6 +96,8 @@ export class SignatureTest { * Test with an optional parameter. * * @param b The string parameter. + * + * @since 1.0.0 */ optionalStringParamMethod(b?: string): number { return b ? 0 : 1; @@ -101,6 +107,8 @@ export class SignatureTest { * Test with a default parameter. * * @param c The boolean parameter. + * + * @since 1.0.0 */ defaultBooleanParamMethod(c: boolean = true): number { return c ? 1 : 0; @@ -112,6 +120,8 @@ export class SignatureTest { * @param a The number parameter. * @param b The string parameter. * @param c The boolean parameter. + * + * @since 1.0.0 */ multiParamMethod(a: number, b?: string, c: boolean = true): number { return c ? a : b ? 0 : 1; @@ -121,6 +131,8 @@ export class SignatureTest { * Test with a function parameters. * * @param fn The function parameter. + * + * @since 1.0.0 */ functionParamMethod(fn: (a: string) => number): number { return fn('a'); @@ -134,12 +146,23 @@ export class SignatureTest { * @param options.casing The casing parameter. * @param options.format The format parameter. * @param options.excludes The excludes parameter. + * + * @since 1.0.0 */ stringUnionParamMethod( value: 'a' | 'b', options?: { + /** + * The casing parameter. + */ casing?: Casing; + /** + * The format parameter. + */ format?: 'hex' | ColorFormat; + /** + * The excludes parameter. + */ excludes?: ReadonlyArray<AlphaNumericChar>; } ): string { @@ -155,6 +178,8 @@ export class SignatureTest { * @param namedArray Array of `'a'` or `'b'`. * @param mixed Value `'a'` or `'b'` or an array thereof. * @param namedMixed Value `'a'` or `'b'` or an array thereof. + * + * @since 1.0.0 */ literalUnionParamMethod( value: LiteralUnion<'a' | 'b'>, @@ -178,6 +203,8 @@ export class SignatureTest { * Test with a Record parameter. * * @param object The Record parameter. + * + * @since 1.0.0 */ recordParamMethod(object: Record<string, number>): number { return object.a; @@ -192,11 +219,25 @@ export class SignatureTest { * @param options.c The boolean parameter. * @param options.d The method parameter. * @param options.e The LiteralUnion parameter. + * + * @since 1.0.0 */ optionsParamMethod(options: { + /** + * The number parameter. + */ a: number; + /** + * The string parameter. + */ b?: string; + /** + * The boolean parameter. + */ c: boolean; + /** + * The method parameter. + */ d: () => string; /** * A parameter with inline documentation. @@ -222,11 +263,28 @@ export class SignatureTest { * @param b.value The number parameter. * @param c Parameter with inner jsdocs default. * @param c.value The number parameter. It also has a more complex description. Defaults to `2`. + * + * @since 1.0.0 */ optionsInlineParamMethodWithDefaults( - a: { value?: number } = { value: 1 }, - b: { value?: number }, - c: { value?: number } + a: { + /** + * The number parameter. + */ + value?: number; + } = { value: 1 }, + b: { + /** + * The number parameter. + */ + value?: number; + }, + c: { + /** + * The number parameter. + */ + value?: number; + } ): number { return a.value ?? b.value ?? c.value ?? -1; } @@ -240,6 +298,8 @@ export class SignatureTest { * @param b.value The number parameter. * @param c Parameter with inner jsdocs default. * @param c.value The number parameter. Defaults to `2`. + * + * @since 1.0.0 */ optionsTypeParamMethodWithDefaults( a: ParameterOptionsTypeA = { value: 1 }, @@ -258,6 +318,8 @@ export class SignatureTest { * @param b.value The number parameter. * @param c Parameter with inner jsdocs default. * @param c.value The number parameter. Defaults to `2`. + * + * @since 1.0.0 */ optionsInterfaceParamMethodWithDefaults( a: ParameterOptionsInterfaceA = { value: 1 }, @@ -271,7 +333,9 @@ export class SignatureTest { * Test with example marker. * * @example - * test.apidoc.methodWithExample() // 0 + * test.apidocs.methodWithExample() // 0 + * + * @since 1.0.0 */ methodWithExample(): number { return 0; @@ -280,7 +344,9 @@ export class SignatureTest { /** * Test with deprecated and see marker. * - * @see test.apidoc.methodWithExample() + * @see test.apidocs.methodWithExample() + * + * @since 1.0.0 * * @deprecated do something else */ @@ -292,6 +358,8 @@ export class SignatureTest { * Test with throws. * * @throws Everytime. + * + * @since 1.0.0 */ methodWithThrows(): number { throw new FakerError('Test error'); @@ -302,6 +370,8 @@ export class SignatureTest { * * @throws First error case. * @throws Another error case. + * + * @since 1.0.0 */ methodWithMultipleThrows(): number { throw new FakerError('Another test error'); @@ -314,6 +384,8 @@ export class SignatureTest { * @param option.a Some deprecated option. * @param option.b Some other deprecated option. * @param option.c Some other option. + * + * @since 1.0.0 */ methodWithDeprecatedOption(option: { /** @@ -339,8 +411,10 @@ export class SignatureTest { /** * Test with multiple see markers. * - * @see test.apidoc.methodWithExample() - * @see test.apidoc.methodWithDeprecated() + * @see test.apidocsmethodWithExample() + * @see test.apidocsmethodWithDeprecated() + * + * @since 1.0.0 */ methodWithMultipleSeeMarkers(): number { return 0; @@ -349,8 +423,10 @@ export class SignatureTest { /** * Test with multiple see markers and backticks. * - * @see test.apidoc.methodWithExample() with parameter `foo`. - * @see test.apidoc.methodWithDeprecated() with parameter `bar` and `baz`. + * @see test.apidocsmethodWithExample() with parameter `foo`. + * @see test.apidocsmethodWithDeprecated() with parameter `bar` and `baz`. + * + * @since 1.0.0 */ methodWithMultipleSeeMarkersAndBackticks(): number { return 0; @@ -373,6 +449,8 @@ export class SignatureTest { * @param array Array to pick the value from. * @param array[].weight The weight of the value. * @param array[].value The value to pick. + * + * @since 1.0.0 */ complexArrayParameter<T>( array: ReadonlyArray<{ diff --git a/test/scripts/apidocs/method.spec.ts b/test/scripts/apidocs/method.spec.ts new file mode 100644 index 00000000..14c9cfc0 --- /dev/null +++ b/test/scripts/apidocs/method.spec.ts @@ -0,0 +1,30 @@ +import { describe, expect, it } from 'vitest'; +import { processMethodLike } from '../../../scripts/apidocs/processing/method'; +import { SignatureTest } from './method.example'; +import { loadExampleMethods } from './utils'; + +const methods = loadExampleMethods(); + +describe('method', () => { + it('dummy dependency to rerun the test if the example changes', () => { + expect(new SignatureTest()).toBeTruthy(); + }); + + it('expected and actual methods are equal', () => { + expect(Object.keys(methods)).toMatchSnapshot(); + }); + + it.each(Object.entries(methods))( + 'processMethodLike(%s)', + (name, signature) => { + const actual = processMethodLike(name, signature); + actual.source = { + filePath: actual.source.filePath, + line: -1, + column: -1, + }; + + expect(actual).toMatchSnapshot(); + } + ); +}); diff --git a/test/scripts/apidocs/utils.ts b/test/scripts/apidocs/utils.ts new file mode 100644 index 00000000..e15bcd8f --- /dev/null +++ b/test/scripts/apidocs/utils.ts @@ -0,0 +1,38 @@ +import type { ClassDeclaration, MethodDeclaration, SourceFile } from 'ts-morph'; +import { getProject } from '../../../scripts/apidocs/project'; + +/** + * Loads the example methods. + */ +export function loadExampleMethods(): Record<string, MethodDeclaration> { + return Object.fromEntries( + loadProjectFile('test/scripts/apidocs/method.example.ts') + .getClassOrThrow('SignatureTest') + .getMethods() + .map((m) => [m.getName(), m] as const) + .sort(([a], [b]) => a.localeCompare(b)) // Relevant for Object.keys() order + ); +} + +/** + * Loads the example classes. + */ +export function loadExampleClasses(): Record<string, ClassDeclaration> { + return Object.fromEntries( + loadProjectFile('test/scripts/apidocs/class.example.ts') + .getClasses() + .map((m) => [m.getNameOrThrow(), m] as const) + .sort(([a], [b]) => a.localeCompare(b)) // Relevant for Object.keys() order + ); +} + +/** + * Loads the project. + * + * @param sourceFile The source file to load. + */ +function loadProjectFile(sourceFile: string): SourceFile { + const project = getProject(); + + return project.addSourceFileAtPath(sourceFile); +} diff --git a/test/scripts/apidocs/verify-jsdoc-tags.spec.ts b/test/scripts/apidocs/verify-jsdoc-tags.spec.ts new file mode 100644 index 00000000..16f0ea51 --- /dev/null +++ b/test/scripts/apidocs/verify-jsdoc-tags.spec.ts @@ -0,0 +1,286 @@ +import { existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import validator from 'validator'; +import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'; +import { processComponents } from '../../../scripts/apidocs/generate'; +import { extractSummaryDefault } from '../../../scripts/apidocs/output/page'; +import { getProject } from '../../../scripts/apidocs/project'; + +// This test suite ensures, that every method +// - has working examples +// - running these do not log anything, unless the method is deprecated +// - has a valid @since tag +// - has valid @see tags +// - has proper links in the description + +const tempDir = resolve(dirname(fileURLToPath(import.meta.url)), 'temp'); +const relativeImportPath = `${'../'.repeat(5)}src`; + +afterAll(() => { + // Remove temp folder + if (existsSync(tempDir)) { + rmSync(tempDir, { recursive: true }); + } +}); + +const modules = processComponents(getProject()); + +function resolveDirToModule(moduleName: string): string { + return resolve(tempDir, moduleName); +} + +function resolvePathToMethodFile( + moduleName: string, + methodName: string, + signature: number +): string { + const dir = resolveDirToModule(moduleName); + return resolve(dir, `${methodName}_${signature}.ts`); +} + +const allowedReferences = new Set( + modules.flatMap(({ camelTitle, methods, category }) => { + return methods.map(({ name }) => + category ? `faker.${camelTitle}.${name}` : `${camelTitle}.${name}` + ); + }) +); +const allowedLinks = new Set( + modules.flatMap(({ camelTitle, methods }) => { + return [ + `/api/${camelTitle}.html`, + ...methods.map( + ({ name }) => `/api/${camelTitle}.html#${name.toLowerCase()}` + ), + ]; + }) +); + +function assertDescription(description: string): void { + const linkRegexp = /\[([^\]]+)\]\(([^)]+)\)/g; + const links = [...description.matchAll(linkRegexp)].map((m) => m[2]); + + for (const link of links) { + expect(link).toMatch(/^https?:\/\//); + expect(link).toSatisfy(validator.isURL); + + if (link.includes('fakerjs.dev/api/')) { + expect(allowedLinks, `${link} to point to a valid target`).toContain( + link.replace(/.*fakerjs.dev\//, '/') + ); + } + } +} + +describe('verify JSDoc tags', () => { + describe.each(modules.map((m) => [m.camelTitle, m]))( + '%s', + (moduleName, module) => { + describe('verify module', () => { + it('verify description', () => { + assertDescription(module.description); + }); + }); + + describe.each(module.methods.map((m) => [m.name, m]))( + '%s', + (methodName, method) => { + describe.each(method.signatures.map((s, i) => [i, s]))( + '%i', + (signatureIndex, signature) => { + beforeAll(() => { + // Write temp files to disk + // By extracting the examples + // Guessing required imports + // And saving them to disk for later execution + + const dir = resolveDirToModule(moduleName); + mkdirSync(dir, { recursive: true }); + const path = resolvePathToMethodFile( + moduleName, + methodName, + signatureIndex + ); + + let examples = signature.examples.join('\n'); + if (moduleName === 'faker' && methodName === 'constructor') { + // That case should demonstrate an error and is thus not suitable for testing + examples = examples.replace( + 'customFaker.music.genre()', + '// customFaker.music.genre()' + ); + } + + // Replace imports for users with our source path + examples = examples.replaceAll( + " from '@faker-js/faker'", + ` from '${relativeImportPath}'` + ); + + if (moduleName === 'randomizer') { + examples = `import { generateMersenne32Randomizer } from '${relativeImportPath}/internal/mersenne'; + +const randomizer = generateMersenne32Randomizer(); + +${examples}`; + } + + // If imports are present, we expect them to be complete + if (!examples.includes('import ')) { + const imports = [ + // collect the imports for the various locales e.g. fakerDE_CH + ...new Set(examples.match(/(?<!\.)faker[^.]*(?=\.)/g)), + ]; + + if (imports.length > 0) { + examples = `import { ${imports.join( + ', ' + )} } from '${relativeImportPath}';\n\n${examples}`; + } + } + + writeFileSync(path, examples); + }); + + it('verify description', () => { + assertDescription(signature.description); + }); + + it('verify @example tag', async () => { + const examples = signature.examples.join('\n'); + + expect( + examples, + `${moduleName}.${methodName} to have examples` + ).not.toBe(''); + + // Grab path to example file + const path = resolvePathToMethodFile( + moduleName, + methodName, + signatureIndex + ); + + // Executing the examples should not throw + await expect( + import(`${path}?scope=example`), + examples + ).resolves.toBeDefined(); + }); + + // This only checks whether the whole method is deprecated or not + // It does not check whether the method is deprecated for a specific set of arguments + it('verify @deprecated tag', async () => { + // Grab path to example file + const path = resolvePathToMethodFile( + moduleName, + methodName, + signatureIndex + ); + + const consoleWarnSpy = vi.spyOn(console, 'warn'); + + // Run the examples + await import(`${path}?scope=deprecated`); + + // Verify that deprecated methods log a warning + const { deprecated } = signature; + if (deprecated == null) { + expect(consoleWarnSpy).not.toHaveBeenCalled(); + } else { + expect(consoleWarnSpy).toHaveBeenCalled(); + expect(deprecated).not.toBe(''); + } + }); + + describe.each(signature.parameters.map((p) => [p.name, p]))( + '%s', + (_, parameter) => { + it('verify default value', () => { + const { + name, + default: paramDefault, + description, + } = parameter; + + const commentDefault = extractSummaryDefault(description); + if (paramDefault) { + if ( + /^{.*}$/.test(paramDefault) || + paramDefault.includes('\n') + ) { + expect(commentDefault).toBeUndefined(); + } else if ( + !name.includes('.') && + // Skip check of defaults in descriptions if it is a paraphrased function call + (commentDefault || + (!description.includes('Defaults to') && + !paramDefault.includes('('))) + ) { + expect( + commentDefault, + `Expect '${name}'s js implementation default to be the same as the jsdoc summary default` + ).toBe(paramDefault); + } + } + }); + + it('verify description', () => { + assertDescription(parameter.description); + }); + } + ); + + it('verify @see tags', () => { + for (const link of signature.seeAlsos) { + if (link.startsWith('faker.')) { + // Expected @see faker.xxx.yyy() + expect( + link, + 'Expect method reference to contain ()' + ).toContain('('); + expect( + link, + 'Expect method reference to contain ()' + ).toContain(')'); + expect( + link, + "Expect method reference to have a ': ' after the parenthesis" + ).toContain('): '); + expect( + link, + 'Expect method reference to have a description starting with a capital letter' + ).toMatch(/\): [A-Z]/); + expect( + link, + 'Expect method reference to start with a standard description phrase' + ).toMatch( + /\): (?:For generating |For more information about |For using |For the replacement method)/ + ); + expect( + link, + 'Expect method reference to have a description ending with a dot' + ).toMatch(/\.$/); + expect(allowedReferences).toContain( + link.replace(/\(.*/, '') + ); + } + } + }); + + it('verify @since tag', () => { + const { since } = signature; + expect(since, '@since to be present').toBeTruthy(); + expect(since).not.toBe(''); + expect(since, '@since to be a valid semver').toSatisfy( + validator.isSemVer + ); + }); + } + ); + } + ); + } + ); +}); diff --git a/tsconfig.json b/tsconfig.json index a290fc07..a949c9c3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -17,6 +17,6 @@ "node_modules", "dist", // required for the typedoc related tests on macOS #2280 - "test/scripts/apidoc/temp" + "test/scripts/apidocs/temp" ] } |
