aboutsummaryrefslogtreecommitdiff
path: root/scripts
diff options
context:
space:
mode:
Diffstat (limited to 'scripts')
-rw-r--r--scripts/apidoc.ts7
-rw-r--r--scripts/apidoc/faker-class.ts86
-rw-r--r--scripts/apidoc/faker-utilities.ts40
-rw-r--r--scripts/apidoc/generate.ts42
-rw-r--r--scripts/apidoc/module-methods.ts122
-rw-r--r--scripts/apidoc/parameter-defaults.ts136
-rw-r--r--scripts/apidoc/signature.ts365
-rw-r--r--scripts/apidoc/typedoc.ts421
-rw-r--r--scripts/apidoc/utils.ts79
-rw-r--r--scripts/apidoc/writer.ts249
-rw-r--r--scripts/apidocs.ts7
-rw-r--r--scripts/apidocs/diff.ts (renamed from scripts/apidoc/diff.ts)21
-rw-r--r--scripts/apidocs/generate.ts45
-rw-r--r--scripts/apidocs/output/constants.ts1
-rw-r--r--scripts/apidocs/output/diff-index.ts81
-rw-r--r--scripts/apidocs/output/page-index.ts38
-rw-r--r--scripts/apidocs/output/page.ts172
-rw-r--r--scripts/apidocs/output/search-index.ts34
-rw-r--r--scripts/apidocs/output/source-base-url.ts38
-rw-r--r--scripts/apidocs/processing/class.ts223
-rw-r--r--scripts/apidocs/processing/error.ts40
-rw-r--r--scripts/apidocs/processing/jsdocs.ts91
-rw-r--r--scripts/apidocs/processing/method.ts197
-rw-r--r--scripts/apidocs/processing/parameter.ts203
-rw-r--r--scripts/apidocs/processing/signature.ts158
-rw-r--r--scripts/apidocs/processing/source.ts37
-rw-r--r--scripts/apidocs/processing/type.ts231
-rw-r--r--scripts/apidocs/project.ts9
-rw-r--r--scripts/apidocs/utils/format.ts (renamed from scripts/apidoc/format.ts)2
-rw-r--r--scripts/apidocs/utils/markdown.ts (renamed from scripts/apidoc/markdown.ts)44
-rw-r--r--scripts/apidocs/utils/paths.ts24
-rw-r--r--scripts/apidocs/utils/value-checks.ts69
-rw-r--r--scripts/diff.ts8
-rw-r--r--scripts/generate-locales.ts2
34 files changed, 1750 insertions, 1572 deletions
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('&gt;', '>')
.replaceAll('&lt;', '<')
.replaceAll('&amp;', '&')
- .replaceAll('&quot;', '"')
.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