diff --git a/docs/api/.gitignore b/docs/api/.gitignore index d77f28d2ebc..0b938bbecd2 100644 --- a/docs/api/.gitignore +++ b/docs/api/.gitignore @@ -1,4 +1,3 @@ *.md -!fake.md !localization.md *.ts diff --git a/docs/api/fake.md b/docs/api/fake.md deleted file mode 100644 index 8fae509adab..00000000000 --- a/docs/api/fake.md +++ /dev/null @@ -1,11 +0,0 @@ -# Fake - -Useful generator method `faker.fake()` for combining faker API methods using a mustache string format. - -```js -faker.fake('{{name.lastName}}, {{name.firstName}} {{name.suffix}}'); -// Wintheiser, Shaylee Sr. - -faker.fake('{{company.bs}} is short for {{address.streetName}}'); -// cutting-edge leverage web services is short for Flatley Rue -``` diff --git a/scripts/apidoc.ts b/scripts/apidoc.ts index fd9ebcd0d25..cb4ed2a4e12 100644 --- a/scripts/apidoc.ts +++ b/scripts/apidoc.ts @@ -1,88 +1,13 @@ -import { writeFileSync } from 'fs'; import { resolve } from 'path'; -import type { Options } from 'prettier'; -import { format } from 'prettier'; -import sanitizeHtml from 'sanitize-html'; import * as TypeDoc from 'typedoc'; -import { createMarkdownRenderer } from 'vitepress'; -import prettierConfig from '../.prettierrc.cjs'; -import type { - Method, - MethodParameter, -} from '../docs/.vitepress/components/api-docs/method'; -import faker from '../src'; -// TODO ST-DDT 2022-02-20: Actually import this/fix module import errors -// import vitepressConfig from '../docs/.vitepress/config'; +import { writeApiPagesIndex } from './apidoc/apiDocsWriter'; +import { processDirectMethods } from './apidoc/directMethods'; +import { processModuleMethods } from './apidoc/moduleMethods'; +import type { PageIndex } from './apidoc/utils'; +import { pathOutputDir } from './apidoc/utils'; -const pathRoot = resolve(__dirname, '..'); -const pathDocsDir = resolve(pathRoot, 'docs'); -const pathDocsApiPages = resolve(pathDocsDir, '.vitepress', 'api-pages.mjs'); -const pathOutputDir = resolve(pathDocsDir, 'api'); const pathOutputJson = resolve(pathOutputDir, 'typedoc.json'); -const scriptCommand = 'pnpm run generate:api-docs'; - -const markdown = createMarkdownRenderer( - pathOutputDir - // TODO ST-DDT 2022-02-20: Actually import this/fix module import errors - // vitepressConfig.markdown -); - -const prettierMarkdown: Options = { - ...prettierConfig, - parser: 'markdown', -}; - -const prettierTypescript: Options = { - ...prettierConfig, - parser: 'typescript', -}; - -const prettierBabel: Options = { - ...prettierConfig, - parser: 'babel', -}; - -const htmlSanitizeOptions: sanitizeHtml.IOptions = { - allowedTags: ['a', 'code', 'div', 'li', 'span', 'p', 'pre', 'ul'], - allowedAttributes: { - a: ['href', 'target', 'rel'], - div: ['class'], - pre: ['v-pre'], - span: ['class'], - }, - selfClosing: [], -}; - -export function prettifyMethodName(method: string): string { - return ( - // Capitalize and insert space before upper case characters - method.substring(0, 1).toUpperCase() + - method.substring(1).replace(/([A-Z]+)/g, ' $1') - ); -} - -function toBlock(comment?: TypeDoc.Comment): string { - return ( - (comment?.shortText.trim() || 'Missing') + - (comment?.text ? '\n\n' + comment.text : '') - ); -} - -function mdToHtml(md: string): string { - const rawHtml = markdown.render(md); - const safeHtml: string = sanitizeHtml(rawHtml, htmlSanitizeOptions); - // Revert some escaped characters for comparison. - if (rawHtml.replace(/>/g, '>') === safeHtml.replace(/>/g, '>')) { - return safeHtml; - } else { - console.debug('Rejected unsafe md:', md); - console.error('Rejected unsafe html:', rawHtml.replace(/>/g, '>')); - console.error('Expected safe html:', safeHtml.replace(/>/g, '>')); - throw new Error('Found unsafe html'); - } -} - async function build(): Promise { const app = new TypeDoc.Application(); @@ -102,197 +27,15 @@ async function build(): Promise { // Project may not have converted correctly return; } - // Useful for analyzing the content + // Useful for manually analyzing the content await app.generateJson(project, pathOutputJson); + console.log(pathOutputDir); - const modules = project - .getChildrenByKind(TypeDoc.ReflectionKind.Namespace)[0] - .getChildrenByKind(TypeDoc.ReflectionKind.Class); - - const modulesPages: Array<{ text: string; link: string }> = []; - modulesPages.push({ text: 'Fake', link: '/api/fake.html' }); + const modulesPages: PageIndex = []; modulesPages.push({ text: 'Localization', link: '/api/localization.html' }); - - // Generate module file - for (const module of modules) { - const moduleName = module.name.replace('_', ''); - const lowerModuleName = - moduleName.substring(0, 1).toLowerCase() + moduleName.substring(1); - if (faker[lowerModuleName] === undefined) { - continue; - } - console.log(`Processing Module ${moduleName}`); - - modulesPages.push({ - text: moduleName, - link: `/api/${lowerModuleName}.html`, - }); - - const methods: Method[] = []; - - // Generate method section - for (const method of module.getChildrenByKind( - TypeDoc.ReflectionKind.Method - )) { - const methodName = method.name; - const prettyMethodName = prettifyMethodName(methodName); - console.debug(`- method ${prettyMethodName}`); - const signature = method.signatures[0]; - - 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, - description: mdToHtml(toBlock(parameter.comment)), - }); - } - - // Collect Parameters - const signatureParameters: string[] = []; - let requiresArgs = false; - for ( - let index = 0; - signature.parameters && index < signature.parameters.length; - index++ - ) { - const parameter = signature.parameters[index]; - - const parameterDefault = parameter.defaultValue; - const parameterRequired = typeof parameterDefault === 'undefined'; - if (index === 0) { - requiresArgs = parameterRequired; - } - const parameterName = parameter.name + (parameterRequired ? '?' : ''); - const parameterType = parameter.type.toString(); - - let parameterDefaultSignatureText = ''; - if (!parameterRequired) { - parameterDefaultSignatureText = ' = ' + parameterDefault; - } - - signatureParameters.push( - parameterName + ': ' + parameterType + parameterDefaultSignatureText - ); - parameters.push({ - name: parameter.name, - type: parameterType, - default: parameterDefault, - description: mdToHtml(toBlock(parameter.comment)), - }); - } - - // Generate usage section - - let signatureTypeParametersString = ''; - if (signatureTypeParameters.length !== 0) { - signatureTypeParametersString = `<${signatureTypeParameters.join( - ', ' - )}>`; - } - const signatureParametersString = signatureParameters.join(', '); - - let examples = `faker.${lowerModuleName}.${methodName}${signatureTypeParametersString}(${signatureParametersString}): ${signature.type.toString()}\n`; - faker.seed(0); - if (!requiresArgs) { - try { - let example = JSON.stringify(faker[lowerModuleName][methodName]()); - if (example.length > 50) { - example = example.substring(0, 47) + '...'; - } - - examples += `faker.${lowerModuleName}.${methodName}()`; - examples += (example ? ` // => ${example}` : '') + '\n'; - } catch (error) { - // Ignore the error => hide the example call + result. - } - } - const exampleTags = - signature?.comment?.tags - .filter((tag) => tag.tagName === 'example') - .map((tag) => tag.text.trimEnd()) || []; - - if (exampleTags.length > 0) { - examples += exampleTags.join('\n').trim() + '\n'; - } - - const seeAlsos = - signature.comment?.tags - .filter((t) => t.tagName === 'see') - .map((t) => t.text.trim()) ?? []; - - methods.push({ - name: methodName, - title: prettyMethodName, - description: mdToHtml(toBlock(signature.comment)), - parameters: parameters, - returns: signature.type.toString(), - examples: mdToHtml('```ts\n' + examples + '```'), - deprecated: signature.comment?.hasTag('deprecated') ?? false, - seeAlsos, - }); - } - - // Write api docs page - let content = ` - - - # ${moduleName} - - - - - ::: v-pre - - ${toBlock(module.comment)} - - ::: - - - `.replace(/\n +/g, '\n'); - - content = format(content, prettierMarkdown); - - writeFileSync(resolve(pathOutputDir, lowerModuleName + '.md'), content); - - // Write api docs data - - let contentTs = ` - import type { Method } from '../.vitepress/components/api-docs/method'; - - export const ${lowerModuleName}: Method[] = ${JSON.stringify( - methods, - null, - 2 - )}`; - - contentTs = format(contentTs, prettierTypescript); - - writeFileSync(resolve(pathOutputDir, lowerModuleName + '.ts'), contentTs); - } - - // Write api-pages.mjs - console.log('Updating api-pages.mjs'); - modulesPages.sort((a, b) => a.text.localeCompare(b.text)); - let apiPagesContent = ` - // This file is automatically generated. - // Run '${scriptCommand}' to update - export const apiPages = ${JSON.stringify(modulesPages)}; - `.replace(/\n +/, '\n'); - - apiPagesContent = format(apiPagesContent, prettierBabel); - - writeFileSync(pathDocsApiPages, apiPagesContent); + modulesPages.push(...processModuleMethods(project)); + modulesPages.push(...processDirectMethods(project)); + writeApiPagesIndex(modulesPages); } build().catch(console.error); diff --git a/scripts/apidoc/apiDocsWriter.ts b/scripts/apidoc/apiDocsWriter.ts new file mode 100644 index 00000000000..e2b4281620a --- /dev/null +++ b/scripts/apidoc/apiDocsWriter.ts @@ -0,0 +1,135 @@ +import { writeFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import type { Options } from 'prettier'; +import { format } from 'prettier'; +import prettierConfig from '../../.prettierrc.cjs'; +import type { Method } from '../../docs/.vitepress/components/api-docs/method'; +import type { PageIndex } from './utils'; +import { pathDocsDir, pathOutputDir } from './utils'; + +const pathDocsApiPages = resolve(pathDocsDir, '.vitepress', 'api-pages.mjs'); + +const scriptCommand = 'pnpm run generate:api-docs'; + +const prettierMarkdown: Options = { + ...prettierConfig, + parser: 'markdown', +}; + +const prettierTypescript: Options = { + ...prettierConfig, + parser: 'typescript', +}; + +const prettierBabel: Options = { + ...prettierConfig, + parser: 'babel', +}; + +/** + * 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. + */ +export function writeApiDocsModulePage( + moduleName: string, + lowerModuleName: string, + comment: string +): void { + // Write api docs page + let content = ` + + + # ${moduleName} + + + + + ::: v-pre + + ${comment} + + ::: + + + `.replace(/\n +/g, '\n'); + + content = format(content, prettierMarkdown); + + writeFileSync(resolve(pathOutputDir, lowerModuleName + '.md'), content); +} + +/** + * Writes the api page for the given method to the correct location. + * + * @param methodName The name of the method to write the docs for. + */ +export function writeApiDocsDirectPage(methodName: string): void { + let content = ` + + + + `.replace(/\n +/g, '\n'); + + content = format(content, prettierMarkdown); + + writeFileSync(resolve(pathOutputDir, methodName + '.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. + */ +export function writeApiDocsData( + lowerModuleName: string, + methods: Method[] +): void { + let contentTs = ` +import type { Method } from '../.vitepress/components/api-docs/method'; + +export const ${lowerModuleName}: Method[] = ${JSON.stringify( + methods, + null, + 2 + )}`; + + contentTs = format(contentTs, prettierTypescript); + + writeFileSync(resolve(pathOutputDir, lowerModuleName + '.ts'), contentTs); +} + +/** + * Writes the api docs index to correct location. + * + * @param pages The pages to write into the index. + */ +export function writeApiPagesIndex(pages: PageIndex): void { + // Write api-pages.mjs + console.log('Updating api-pages.mjs'); + pages.sort((a, b) => a.text.localeCompare(b.text)); + let apiPagesContent = ` + // This file is automatically generated. + // Run '${scriptCommand}' to update + export const apiPages = ${JSON.stringify(pages)}; + `.replace(/\n +/, '\n'); + + apiPagesContent = format(apiPagesContent, prettierBabel); + + writeFileSync(pathDocsApiPages, apiPagesContent); +} diff --git a/scripts/apidoc/directMethods.ts b/scripts/apidoc/directMethods.ts new file mode 100644 index 00000000000..616226ee522 --- /dev/null +++ b/scripts/apidoc/directMethods.ts @@ -0,0 +1,56 @@ +import * as TypeDoc from 'typedoc'; +import { writeApiDocsData, writeApiDocsDirectPage } from './apiDocsWriter'; +import { analyzeSignature } from './signature'; +import type { Page, PageIndex } from './utils'; + +/** + * Analyzes and writes the documentation for direct methods such as `faker.fake()`. + * + * @param project The project used to extract the direct methods. + * @returns The generated pages. + */ +export function processDirectMethods( + project: TypeDoc.ProjectReflection +): PageIndex { + const pages: PageIndex = []; + + const directs = project + .getChildrenByKind(TypeDoc.ReflectionKind.Class) + .filter((ref) => ref.name === 'Faker')[0] + .getChildrenByKind(TypeDoc.ReflectionKind.Property) + .filter((ref) => ['fake', 'unique'].includes(ref.name)); + + for (const direct of directs) { + pages.push(processDirectMethod(direct)); + } + + return pages; +} + +/** + * Analyzes and writes the documentation for a direct method such as `faker.fake()`. + * + * @param direct The direct method to process. + * @returns The generated pages. + */ +export function processDirectMethod( + direct: TypeDoc.DeclarationReflection +): Page { + const methodName = direct.name; + const upperMethodName = + methodName.substring(0, 1).toUpperCase() + methodName.substring(1); + console.log(`Processing Direct: ${upperMethodName}`); + + const signature = (direct.type as TypeDoc.ReflectionType).declaration + .signatures[0]; + + writeApiDocsDirectPage(methodName); + writeApiDocsData(methodName, [ + analyzeSignature(signature, undefined, methodName), + ]); + + return { + text: upperMethodName, + link: `/api/${methodName}.html`, + }; +} diff --git a/scripts/apidoc/moduleMethods.ts b/scripts/apidoc/moduleMethods.ts new file mode 100644 index 00000000000..89c42a33a56 --- /dev/null +++ b/scripts/apidoc/moduleMethods.ts @@ -0,0 +1,67 @@ +import * as TypeDoc from 'typedoc'; +import type { Method } from '../../docs/.vitepress/components/api-docs/method'; +import faker from '../../src'; +import { writeApiDocsData, writeApiDocsModulePage } from './apiDocsWriter'; +import { analyzeSignature, toBlock } from './signature'; +import type { PageIndex } from './utils'; + +/** + * 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 function processModuleMethods( + project: TypeDoc.ProjectReflection +): PageIndex { + const modules = project + .getChildrenByKind(TypeDoc.ReflectionKind.Namespace)[0] + .getChildrenByKind(TypeDoc.ReflectionKind.Class); + + const pages: PageIndex = []; + // Generate module file + for (const module of modules) { + pages.push(...processModuleMethod(module)); + } + + return pages; +} + +/** + * Analyzes and writes the documentation for a module and its methods such as `faker.animal.cat()`. + * + * @param direct The module to process. + * @returns The generated pages. + */ +function processModuleMethod(module: TypeDoc.DeclarationReflection): PageIndex { + const moduleName = module.name.replace('_', ''); + const lowerModuleName = + moduleName.substring(0, 1).toLowerCase() + moduleName.substring(1); + if (faker[lowerModuleName] === undefined) { + return []; + } + console.log(`Processing Module ${moduleName}`); + + const methods: Method[] = []; + + // Generate method section + for (const method of module.getChildrenByKind( + TypeDoc.ReflectionKind.Method + )) { + const methodName = method.name; + console.debug(`- ${methodName}`); + const signature = method.signatures[0]; + + methods.push(analyzeSignature(signature, lowerModuleName, methodName)); + } + + writeApiDocsModulePage(moduleName, lowerModuleName, toBlock(module.comment)); + writeApiDocsData(lowerModuleName, methods); + + return [ + { + text: moduleName, + link: `/api/${lowerModuleName}.html`, + }, + ]; +} diff --git a/scripts/apidoc/signature.ts b/scripts/apidoc/signature.ts new file mode 100644 index 00000000000..cd4e4e769f2 --- /dev/null +++ b/scripts/apidoc/signature.ts @@ -0,0 +1,166 @@ +import sanitizeHtml from 'sanitize-html'; +import type * as TypeDoc from 'typedoc'; +import { createMarkdownRenderer } from 'vitepress'; +import type { + Method, + MethodParameter, +} from '../../docs/.vitepress/components/api-docs/method'; +import faker from '../../src'; +import { pathOutputDir } from './utils'; +// TODO ST-DDT 2022-02-20: Actually import this/fix module import errors +// import vitepressConfig from '../../docs/.vitepress/config'; + +export function prettifyMethodName(method: string): string { + return ( + // Capitalize and insert space before upper case characters + method.substring(0, 1).toUpperCase() + + method.substring(1).replace(/([A-Z]+)/g, ' $1') + ); +} + +export function toBlock(comment?: TypeDoc.Comment): string { + return ( + (comment?.shortText.trim() || 'Missing') + + (comment?.text ? '\n\n' + comment.text : '') + ); +} + +const markdown = createMarkdownRenderer( + pathOutputDir, + undefined, + // TODO ST-DDT 2022-02-20: Actually import this/fix module import errors + // vitepressConfig.markdown, + '/' +); + +const htmlSanitizeOptions: sanitizeHtml.IOptions = { + allowedTags: ['a', 'code', 'div', 'li', 'span', 'p', 'pre', 'ul'], + allowedAttributes: { + a: ['href', 'target', 'rel'], + div: ['class'], + pre: ['v-pre'], + span: ['class'], + }, + selfClosing: [], +}; + +function mdToHtml(md: string): string { + const rawHtml = markdown.render(md); + const safeHtml: string = sanitizeHtml(rawHtml, htmlSanitizeOptions); + // Revert some escaped characters for comparison. + if (rawHtml.replace(/>/g, '>') === safeHtml.replace(/>/g, '>')) { + return safeHtml; + } else { + console.debug('Rejected unsafe md:', md); + console.error('Rejected unsafe html:', rawHtml.replace(/>/g, '>')); + console.error('Expected safe html:', safeHtml.replace(/>/g, '>')); + throw new Error('Found unsafe html'); + } +} + +export function analyzeSignature( + signature: TypeDoc.SignatureReflection, + moduleName: string, + methodName: string +): 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, + description: mdToHtml(toBlock(parameter.comment)), + }); + } + + // Collect Parameters + const signatureParameters: string[] = []; + let requiresArgs = false; + for ( + let index = 0; + signature.parameters && index < signature.parameters.length; + index++ + ) { + const parameter = signature.parameters[index]; + + const parameterDefault = parameter.defaultValue; + const parameterRequired = typeof parameterDefault === 'undefined'; + if (index === 0) { + requiresArgs = parameterRequired; + } + const parameterName = parameter.name + (parameterRequired ? '?' : ''); + const parameterType = parameter.type.toString(); + + let parameterDefaultSignatureText = ''; + if (!parameterRequired) { + parameterDefaultSignatureText = ' = ' + parameterDefault; + } + + signatureParameters.push( + parameterName + ': ' + parameterType + parameterDefaultSignatureText + ); + parameters.push({ + name: parameter.name, + type: parameterType, + default: parameterDefault, + description: mdToHtml(toBlock(parameter.comment)), + }); + } + + // Generate usage section + + let signatureTypeParametersString = ''; + if (signatureTypeParameters.length !== 0) { + signatureTypeParametersString = `<${signatureTypeParameters.join(', ')}>`; + } + const signatureParametersString = signatureParameters.join(', '); + + let examples: string; + if (moduleName) { + examples = `faker.${moduleName}.${methodName}${signatureTypeParametersString}(${signatureParametersString}): ${signature.type.toString()}\n`; + } else { + examples = `faker.${methodName}${signatureTypeParametersString}(${signatureParametersString}): ${signature.type.toString()}\n`; + } + faker.seed(0); + if (!requiresArgs && moduleName) { + try { + let example = JSON.stringify(faker[moduleName][methodName]()); + if (example.length > 50) { + example = example.substring(0, 47) + '...'; + } + + examples += `faker.${moduleName}.${methodName}()`; + examples += (example ? ` // => ${example}` : '') + '\n'; + } catch (error) { + // Ignore the error => hide the example call + result. + } + } + const exampleTags = + signature?.comment?.tags + .filter((tag) => tag.tagName === 'example') + .map((tag) => tag.text.trimEnd()) || []; + + if (exampleTags.length > 0) { + examples += exampleTags.join('\n').trim() + '\n'; + } + + const seeAlsos = + signature.comment?.tags + .filter((t) => t.tagName === 'see') + .map((t) => t.text.trim()) ?? []; + + const prettyMethodName = prettifyMethodName(methodName); + return { + name: methodName, + title: prettyMethodName, + description: mdToHtml(toBlock(signature.comment)), + parameters: parameters, + returns: signature.type.toString(), + examples: mdToHtml('```ts\n' + examples + '```'), + deprecated: signature.comment?.hasTag('deprecated') ?? false, + seeAlsos, + }; +} diff --git a/scripts/apidoc/utils.ts b/scripts/apidoc/utils.ts new file mode 100644 index 00000000000..ee38d58d120 --- /dev/null +++ b/scripts/apidoc/utils.ts @@ -0,0 +1,8 @@ +import { resolve } from 'node:path'; + +export type Page = { text: string; link: string }; +export type PageIndex = Array; + +const pathRoot = resolve(__dirname, '..', '..'); +export const pathDocsDir = resolve(pathRoot, 'docs'); +export const pathOutputDir = resolve(pathDocsDir, 'api'); diff --git a/src/fake.ts b/src/fake.ts index a20aae11d45..d56b4794741 100644 --- a/src/fake.ts +++ b/src/fake.ts @@ -30,7 +30,9 @@ export class Fake { * * Please note that is NOT possible to use any non-faker methods or plain js script in there. * - * @param str The format string that will get interpolated. + * @param str The format string that will get interpolated. May not be empty. + * + * @see faker.helpers.mustache() to use custom functions for resolution. * * @example * faker.fake('{{name.lastName}}') // 'Barrows' diff --git a/src/unique.ts b/src/unique.ts index acb6931fb01..6ef5eee5502 100644 --- a/src/unique.ts +++ b/src/unique.ts @@ -28,6 +28,7 @@ export class Unique { * Generates a unique result using the results of the given method. * Used unique entries will be stored internally and filtered from subsequent calls. * + * @template Method The type of the method to execute. * @param method The method used to generate the values. * @param args The arguments used to call the method. * @param opts The optional options used to configure this method.