From fdafaa4681da85c416098256654fe96c171a850b Mon Sep 17 00:00:00 2001 From: ST-DDT Date: Sat, 28 Oct 2023 22:06:33 +0200 Subject: infra(unicorn): filename-case (#2492) --- scripts/apidoc/apiDocsWriter.ts | 238 ------------------ scripts/apidoc/faker-class.ts | 81 ++++++ scripts/apidoc/faker-utilities.ts | 39 +++ scripts/apidoc/fakerClass.ts | 81 ------ scripts/apidoc/fakerUtilities.ts | 39 --- scripts/apidoc/generate.ts | 12 +- scripts/apidoc/module-methods.ts | 121 +++++++++ scripts/apidoc/moduleMethods.ts | 121 --------- scripts/apidoc/parameter-defaults.ts | 136 ++++++++++ scripts/apidoc/parameterDefaults.ts | 136 ---------- scripts/apidoc/typedoc.ts | 2 +- scripts/apidoc/writer.ts | 238 ++++++++++++++++++ scripts/generate-locales.ts | 476 +++++++++++++++++++++++++++++++++++ scripts/generateLocales.ts | 476 ----------------------------------- 14 files changed, 1098 insertions(+), 1098 deletions(-) delete mode 100644 scripts/apidoc/apiDocsWriter.ts create mode 100644 scripts/apidoc/faker-class.ts create mode 100644 scripts/apidoc/faker-utilities.ts delete mode 100644 scripts/apidoc/fakerClass.ts delete mode 100644 scripts/apidoc/fakerUtilities.ts create mode 100644 scripts/apidoc/module-methods.ts delete mode 100644 scripts/apidoc/moduleMethods.ts create mode 100644 scripts/apidoc/parameter-defaults.ts delete mode 100644 scripts/apidoc/parameterDefaults.ts create mode 100644 scripts/apidoc/writer.ts create mode 100644 scripts/generate-locales.ts delete mode 100644 scripts/generateLocales.ts (limited to 'scripts') diff --git a/scripts/apidoc/apiDocsWriter.ts b/scripts/apidoc/apiDocsWriter.ts deleted file mode 100644 index b5fa5806..00000000 --- a/scripts/apidoc/apiDocsWriter.ts +++ /dev/null @@ -1,238 +0,0 @@ -import { writeFileSync } from 'node:fs'; -import { resolve } from 'node:path'; -import type { ProjectReflection } from 'typedoc'; -import { ReflectionKind } from 'typedoc'; -import type { Method } from '../../docs/.vitepress/components/api-docs/method'; -import type { APIGroup } from '../../docs/api/api-types'; -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. - */ -export async function writeApiDocsModule( - moduleName: string, - lowerModuleName: string, - comment: string, - examples: string | undefined, - deprecated: string | undefined, - methods: Method[] -): Promise { - await writeApiDocsModulePage( - moduleName, - lowerModuleName, - comment, - examples, - deprecated, - methods - ); - writeApiDocsModuleData(lowerModuleName, methods); - - return { - text: moduleName, - link: `/api/${lowerModuleName}.html`, - methods, - diff: methods.reduce( - (data, method) => ({ - ...data, - [method.name]: methodDiffHash(method), - }), - { - moduleHash: diffHash({ - name: moduleName, - field: lowerModuleName, - deprecated, - comment, - }), - } - ), - }; -} - -/** - * 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 { - // Write api docs page - let content = ` - - - - - - # ${moduleName} - - ::: v-pre - - ${ - deprecated == null - ? '' - : `
-

Deprecated

-

This module is deprecated and will be removed in a future version.

- ${deprecated} -
` - } - - ${comment} - - ${examples == null ? '' : `
${examples}
`} - - ::: - - ${methods - .map( - (method) => ` - ## ${method.name} - - - ` - ) - .join('')} - `.replace(/\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 { - // Write api-pages.ts - console.log('Updating api-pages.ts'); - pages.splice(0, 0, { text: 'Overview', link: '/api/' }); - let apiPagesContent = ` - // This file is automatically generated. - // Run '${scriptCommand}' to update - export const apiPages = ${JSON.stringify(pages)}; - `.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 { - 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/apidoc/faker-class.ts b/scripts/apidoc/faker-class.ts new file mode 100644 index 00000000..44648831 --- /dev/null +++ b/scripts/apidoc/faker-class.ts @@ -0,0 +1,81 @@ +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 { + 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 { + const randomizerClass = project + .getChildrenByKind(ReflectionKind.Interface) + .find((clazz) => clazz.name === 'Randomizer'); + + return processClass(randomizerClass); +} + +async function processClass( + clazz: DeclarationReflection +): Promise { + 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); +} + +async function processConstructor( + clazz: DeclarationReflection +): Promise { + 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 new file mode 100644 index 00000000..0da03581 --- /dev/null +++ b/scripts/apidoc/faker-utilities.ts @@ -0,0 +1,39 @@ +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 { + const fakerUtilities = project + .getChildrenByKind(ReflectionKind.Function) + .filter((method) => !method.flags.isPrivate); + + return processUtilities(fakerUtilities); +} + +async function processUtilities( + fakerUtilities: DeclarationReflection[] +): Promise { + 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/fakerClass.ts b/scripts/apidoc/fakerClass.ts deleted file mode 100644 index ea190cc7..00000000 --- a/scripts/apidoc/fakerClass.ts +++ /dev/null @@ -1,81 +0,0 @@ -import type { DeclarationReflection, ProjectReflection } from 'typedoc'; -import { ReflectionKind } from 'typedoc'; -import type { Method } from '../../docs/.vitepress/components/api-docs/method'; -import { writeApiDocsModule } from './apiDocsWriter'; -import { analyzeModule, processModuleMethods } from './moduleMethods'; -import { analyzeSignature } from './signature'; -import { extractModuleFieldName, selectApiSignature } from './typedoc'; -import type { ModuleSummary } from './utils'; - -export async function processFakerClasses( - project: ProjectReflection -): Promise { - 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 { - const randomizerClass = project - .getChildrenByKind(ReflectionKind.Interface) - .find((clazz) => clazz.name === 'Randomizer'); - - return processClass(randomizerClass); -} - -async function processClass( - clazz: DeclarationReflection -): Promise { - 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); -} - -async function processConstructor( - clazz: DeclarationReflection -): Promise { - 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/fakerUtilities.ts b/scripts/apidoc/fakerUtilities.ts deleted file mode 100644 index d164857b..00000000 --- a/scripts/apidoc/fakerUtilities.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type { DeclarationReflection, ProjectReflection } from 'typedoc'; -import { ReflectionKind } from 'typedoc'; -import type { Method } from '../../docs/.vitepress/components/api-docs/method'; -import { writeApiDocsModule } from './apiDocsWriter'; -import { processMethods } from './moduleMethods'; -import { selectApiSignature } from './typedoc'; -import type { ModuleSummary } from './utils'; - -export async function processFakerUtilities( - project: ProjectReflection -): Promise { - const fakerUtilities = project - .getChildrenByKind(ReflectionKind.Function) - .filter((method) => !method.flags.isPrivate); - - return processUtilities(fakerUtilities); -} - -async function processUtilities( - fakerUtilities: DeclarationReflection[] -): Promise { - 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 index aa33a46e..316757cc 100644 --- a/scripts/apidoc/generate.ts +++ b/scripts/apidoc/generate.ts @@ -1,15 +1,15 @@ 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 './apiDocsWriter'; -import { processFakerClasses, processFakerRandomizer } from './fakerClass'; -import { processFakerUtilities } from './fakerUtilities'; -import { processModules } from './moduleMethods'; -import { loadProject } from './typedoc'; -import { pathOutputDir } from './utils'; +} from './writer'; const pathOutputJson = resolve(pathOutputDir, 'typedoc.json'); diff --git a/scripts/apidoc/module-methods.ts b/scripts/apidoc/module-methods.ts new file mode 100644 index 00000000..772a7236 --- /dev/null +++ b/scripts/apidoc/module-methods.ts @@ -0,0 +1,121 @@ +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 { + 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 { + 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 + ); +} + +/** + * 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 { + 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, + accessor: string = '' +): Promise { + 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/moduleMethods.ts b/scripts/apidoc/moduleMethods.ts deleted file mode 100644 index 33255a17..00000000 --- a/scripts/apidoc/moduleMethods.ts +++ /dev/null @@ -1,121 +0,0 @@ -import type { - DeclarationReflection, - ProjectReflection, - SignatureReflection, -} from 'typedoc'; -import type { Method } from '../../docs/.vitepress/components/api-docs/method'; -import { writeApiDocsModule } from './apiDocsWriter'; -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'; - -/** - * 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 { - 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 { - 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 - ); -} - -/** - * 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 { - 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, - accessor: string = '' -): Promise { - 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 new file mode 100644 index 00000000..34264b05 --- /dev/null +++ b/scripts/apidoc/parameter-defaults.ts @@ -0,0 +1,136 @@ +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; +} + +/** + * 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[symbol.declarations.length - 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 +{ + readonly priority = 0; + + supports(item: unknown): item is Reflection { + return item instanceof Reflection; + } + + toObject( + item: Reflection, + obj: Partial + ): Partial { + (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 +): 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/parameterDefaults.ts b/scripts/apidoc/parameterDefaults.ts deleted file mode 100644 index 34264b05..00000000 --- a/scripts/apidoc/parameterDefaults.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; -} - -/** - * 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[symbol.declarations.length - 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 -{ - readonly priority = 0; - - supports(item: unknown): item is Reflection { - return item instanceof Reflection; - } - - toObject( - item: Reflection, - obj: Partial - ): Partial { - (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 -): 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/typedoc.ts b/scripts/apidoc/typedoc.ts index 9fa1fdad..0d3799df 100644 --- a/scripts/apidoc/typedoc.ts +++ b/scripts/apidoc/typedoc.ts @@ -19,7 +19,7 @@ import { DefaultParameterAwareSerializer, parameterDefaultReader, patchProjectParameterDefaults, -} from './parameterDefaults'; +} from './parameter-defaults'; import { mapByName } from './utils'; type CommentHolder = Pick; diff --git a/scripts/apidoc/writer.ts b/scripts/apidoc/writer.ts new file mode 100644 index 00000000..b5fa5806 --- /dev/null +++ b/scripts/apidoc/writer.ts @@ -0,0 +1,238 @@ +import { writeFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import type { ProjectReflection } from 'typedoc'; +import { ReflectionKind } from 'typedoc'; +import type { Method } from '../../docs/.vitepress/components/api-docs/method'; +import type { APIGroup } from '../../docs/api/api-types'; +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. + */ +export async function writeApiDocsModule( + moduleName: string, + lowerModuleName: string, + comment: string, + examples: string | undefined, + deprecated: string | undefined, + methods: Method[] +): Promise { + await writeApiDocsModulePage( + moduleName, + lowerModuleName, + comment, + examples, + deprecated, + methods + ); + writeApiDocsModuleData(lowerModuleName, methods); + + return { + text: moduleName, + link: `/api/${lowerModuleName}.html`, + methods, + diff: methods.reduce( + (data, method) => ({ + ...data, + [method.name]: methodDiffHash(method), + }), + { + moduleHash: diffHash({ + name: moduleName, + field: lowerModuleName, + deprecated, + comment, + }), + } + ), + }; +} + +/** + * 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 { + // Write api docs page + let content = ` + + + + + + # ${moduleName} + + ::: v-pre + + ${ + deprecated == null + ? '' + : `
+

Deprecated

+

This module is deprecated and will be removed in a future version.

+ ${deprecated} +
` + } + + ${comment} + + ${examples == null ? '' : `
${examples}
`} + + ::: + + ${methods + .map( + (method) => ` + ## ${method.name} + + + ` + ) + .join('')} + `.replace(/\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 { + // Write api-pages.ts + console.log('Updating api-pages.ts'); + pages.splice(0, 0, { text: 'Overview', link: '/api/' }); + let apiPagesContent = ` + // This file is automatically generated. + // Run '${scriptCommand}' to update + export const apiPages = ${JSON.stringify(pages)}; + `.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 { + 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/generate-locales.ts b/scripts/generate-locales.ts new file mode 100644 index 00000000..d2e9513e --- /dev/null +++ b/scripts/generate-locales.ts @@ -0,0 +1,476 @@ +#!/usr/bin/env node + +/** + * This file contains a script that can be used to update the following files: + * + * - `src/locale/.ts` + * - `src/locales//index.ts` + * - `src/locales///index.ts` + * - `src/docs/guide/localization.md` + * + * If you wish to edit all/specific locale data files you can do so using the + * `updateLocaleFileHook()` method. + * Please remember to not commit your temporary update code. + * + * Run this script using `pnpm run generate:locales` + */ +import { + existsSync, + lstatSync, + readdirSync, + readFileSync, + writeFileSync, +} from 'node:fs'; +import { resolve } from 'node:path'; +import type { Options } from 'prettier'; +import { format } from 'prettier'; +import options from '../.prettierrc.js'; +import type { LocaleDefinition, MetadataDefinition } from '../src/definitions'; + +// Constants + +const pathRoot = resolve(__dirname, '..'); +const pathLocale = resolve(pathRoot, 'src', 'locale'); +const pathLocales = resolve(pathRoot, 'src', 'locales'); +const pathLocaleIndex = resolve(pathLocale, 'index.ts'); +const pathLocalesIndex = resolve(pathLocales, 'index.ts'); +const pathDocsGuideLocalization = resolve( + pathRoot, + 'docs', + 'guide', + 'localization.md' +); + +// Workaround for nameOf +type PascalCase = + TName extends `${infer Prefix}_${infer Remainder}` + ? `${Capitalize}${PascalCase}` + : Capitalize; + +type DefinitionType = { + [key in keyof LocaleDefinition]-?: PascalCase<`${key}Definition`>; +}; + +/** + * The types of the definitions. + */ +const definitionsTypes: DefinitionType = { + airline: 'AirlineDefinition', + animal: 'AnimalDefinition', + color: 'ColorDefinition', + commerce: 'CommerceDefinition', + company: 'CompanyDefinition', + database: 'DatabaseDefinition', + date: 'DateDefinition', + finance: 'FinanceDefinition', + hacker: 'HackerDefinition', + internet: 'InternetDefinition', + location: 'LocationDefinition', + lorem: 'LoremDefinition', + metadata: 'MetadataDefinition', + music: 'MusicDefinition', + person: 'PersonDefinition', + phone_number: 'PhoneNumberDefinition', + science: 'ScienceDefinition', + system: 'SystemDefinition', + vehicle: 'VehicleDefinition', + word: 'WordDefinition', +}; + +const prettierTsOptions: Options = { ...options, parser: 'typescript' }; +const prettierMdOptions: Options = { ...options, parser: 'markdown' }; + +const scriptCommand = 'pnpm run generate:locales'; + +const autoGeneratedCommentHeader = `/* + * This file is automatically generated. + * Run '${scriptCommand}' to update. + */`; + +// Helper functions + +function removeIndexTs(files: string[]): string[] { + const index = files.indexOf('index.ts'); + if (index !== -1) { + files.splice(index, 1); + } + + return files; +} + +function removeTsSuffix(files: string[]): string[] { + return files.map((file) => file.replace('.ts', '')); +} + +function escapeImport(parent: string, module: string): string { + if (['name', 'type', 'switch', parent].includes(module)) { + return `${module}_`; + } + + return module; +} + +function escapeField(parent: string, module: string): string { + if (['name', 'type', 'switch', parent].includes(module)) { + return `${module}: ${module}_`; + } + + return module; +} + +async function generateLocaleFile(locale: string): Promise { + const parts = locale.split('_'); + const locales = [locale]; + + for (let i = parts.length - 1; i > 0; i--) { + const fallback = parts.slice(0, i).join('_'); + if (existsSync(resolve(pathLocales, fallback))) { + locales.push(fallback); + } + } + + // TODO @Shinigami92 2023-03-07: Remove 'en' fallback in a separate PR + if (locales[locales.length - 1] !== 'en' && locale !== 'base') { + locales.push('en'); + } + + if (locales[locales.length - 1] !== 'base') { + locales.push('base'); + } + + let content = ` + ${autoGeneratedCommentHeader} + + import { Faker } from '../faker'; + ${locales + .map((imp) => `import ${imp} from '../locales/${imp}';`) + .join('\n')} + + export const faker = new Faker({ + locale: ${ + locales.length === 1 ? locales[0] : `[${locales.join(', ')}]` + }, + }); + `; + + content = await format(content, prettierTsOptions); + writeFileSync(resolve(pathLocale, `${locale}.ts`), content); +} + +async function generateLocalesIndexFile( + path: string, + name: string, + type: string, + depth: number +): Promise { + let modules = readdirSync(path); + modules = modules.filter((file) => !file.startsWith('.')); + modules = removeIndexTs(modules); + modules = removeTsSuffix(modules); + modules.sort(); + + const content = [autoGeneratedCommentHeader]; + let fieldType = ''; + if (type !== 'any') { + fieldType = `: ${type}`; + content.push( + `import type { ${type.replace(/\[.*/, '')} } from '..${'/..'.repeat( + depth + )}';` + ); + } + + content.push( + ...modules.map( + (module) => `import ${escapeImport(name, module)} from './${module}';` + ), + '', + `const ${name}${fieldType} = { + ${modules.map((module) => `${escapeField(name, module)},`).join('\n')} + };`, + '', + `export default ${name};` + ); + + writeFileSync( + resolve(path, 'index.ts'), + await format(content.join('\n'), prettierTsOptions) + ); +} + +async function generateRecursiveModuleIndexes( + path: string, + name: string, + definition: string, + depth: number +): Promise { + await generateLocalesIndexFile(path, name, definition, depth); + + let submodules = readdirSync(path); + submodules = removeIndexTs(submodules); + for (const submodule of submodules) { + const pathModule = resolve(path, submodule); + await updateLocaleFile(pathModule); + // Only process sub folders recursively + if (lstatSync(pathModule).isDirectory()) { + let moduleDefinition = + definition === 'any' ? 'any' : `${definition}['${submodule}']`; + + // Overwrite types of src/locales///index.ts for known definition types + if (depth === 1) { + moduleDefinition = definitionsTypes[submodule] ?? 'any'; + } + + // Recursive + await generateRecursiveModuleIndexes( + pathModule, + submodule, + moduleDefinition, + depth + 1 + ); + } + } +} + +/** + * Intermediate helper function to allow selectively updating locale data files. + * Use the `updateLocaleFileHook()` method to temporarily add your custom per file processing/update logic. + * + * @param filePath The full file path to the file. + */ +async function updateLocaleFile(filePath: string): Promise { + if (lstatSync(filePath).isFile()) { + const [locale, moduleKey, entryKey] = filePath + .substring(pathLocales.length + 1, filePath.length - 3) + .split(/[\\/]/); + await updateLocaleFileHook(filePath, locale, moduleKey, entryKey); + } +} + +/** + * Use this hook method to selectively update locale data files (not for index.ts files). + * This method is intended to be temporarily overwritten for one-time updates. + * + * @param filePath The full file path to the file. + * @param locale The locale for that file. + * @param definitionKey The definition key of the current file (ex. 'location'). + * @param entryName The entry key of the current file (ex. 'state'). Is `undefined` if `definitionKey` is `'metadata'`. + */ +async function updateLocaleFileHook( + filePath: string, + locale: string, + definitionKey: string, + entryName: string | undefined +): Promise { + // this needs to stay so all arguments are "used" + if (filePath === 'never') { + console.log(`${filePath} <-> ${locale} @ ${definitionKey} -> ${entryName}`); + } + + await normalizeLocaleFile(filePath, definitionKey); +} + +/** + * Normalizes the data of a locale file based on a set of rules. + * Those include: + * - filter the entry list for duplicates + * - limiting the maximum entries of a file to 1000 + * - sorting the entries alphabetically + * + * This function mutates the file by reading and writing to it! + * + * @param filePath The full file path to the file. + * @param definitionKey The definition key of the current file (ex. 'location'). + */ +async function normalizeLocaleFile(filePath: string, definitionKey: string) { + function normalizeDataRecursive(localeData: T): T { + if (typeof localeData !== 'object' || localeData === null) { + // we can only traverse object-like structs + return localeData; + } + + if (Array.isArray(localeData)) { + return ( + [...new Set(localeData)] + // limit entries to 1k + .slice(0, 1000) + // sort entries alphabetically + .sort() as T + ); + } + + const result = {} as T; + for (const key of Object.keys(localeData)) { + result[key] = normalizeDataRecursive(localeData[key]); + } + + return result; + } + + const legacyDefinitions = ['app', 'cell_phone', 'team']; + const definitionsToSkip = [ + 'airline', + 'animal', + 'color', + 'commerce', + 'company', + 'database', + 'date', + 'finance', + 'hacker', + 'internet', + 'location', + 'lorem', + 'metadata', + 'music', + 'person', + 'phone_number', + 'science', + 'system', + 'vehicle', + 'word', + ...legacyDefinitions, + ]; + if (definitionsToSkip.includes(definitionKey)) { + return; + } + + console.log(`Running data normalization for:`, filePath); + + const fileContent = readFileSync(filePath).toString(); + const searchString = 'export default '; + const compareIndex = fileContent.indexOf(searchString) + searchString.length; + const compareString = fileContent.substring(compareIndex); + + const isDynamicFile = compareString.startsWith('mergeArrays'); + const isNonApplicable = compareString.startsWith('null'); + const isFrozenData = compareString.startsWith('Object.freeze'); + if (isDynamicFile || isNonApplicable || isFrozenData) { + return; + } + + const validEntryListStartCharacters = ['[', '{']; + const staticFileOpenSyntax = validEntryListStartCharacters.find( + (validStart) => compareString.startsWith(validStart) + ); + if (staticFileOpenSyntax === undefined) { + console.log('Found an unhandled dynamic file:', filePath); + return; + } + + const fileContentPreData = fileContent.substring(0, compareIndex); + // eslint-disable-next-line @typescript-eslint/no-var-requires + const localeData = normalizeDataRecursive(require(filePath).default); + + // We reattach the content before the actual data implementation to keep stuff like comments. + // In the long term we should probably define a whether we want those in the files at all. + const newContent = fileContentPreData + JSON.stringify(localeData); + + writeFileSync(filePath, await format(newContent, prettierTsOptions)); +} + +// Start of actual logic + +async function main(): Promise { + const locales = readdirSync(pathLocales); + removeIndexTs(locales); + + let localeIndexImports = ''; + let localeIndexExportsIndividual = ''; + let localeIndexExportsGrouped = ''; + let localesIndexExports = ''; + + let localizationLocales = + '| Locale | Name | Faker |\n| :--- | :--- | :--- |\n'; + + for (const locale of locales) { + const pathModules = resolve(pathLocales, locale); + const pathMetadata = resolve(pathModules, 'metadata.ts'); + let localeTitle = 'No title found'; + try { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const metadata: MetadataDefinition = require(pathMetadata).default; + const { title } = metadata; + if (!title) { + throw new Error( + `No title property found on ${JSON.stringify(metadata)}` + ); + } + + localeTitle = title; + } catch (error) { + console.error( + `Failed to load ${pathMetadata}. Please make sure the file exists and exports a MetadataDefinition.` + ); + console.error(error); + } + + const localizedFaker = `faker${locale.replace(/^([a-z]+)/, (part) => + part.toUpperCase() + )}`; + + localeIndexImports += `import { faker as ${localizedFaker} } from './${locale}';\n`; + localeIndexExportsIndividual += ` ${localizedFaker},\n`; + localeIndexExportsGrouped += ` ${locale}: ${localizedFaker},\n`; + localesIndexExports += `export { default as ${locale} } from './${locale}';\n`; + localizationLocales += `| \`${locale}\` | ${localeTitle} | \`${localizedFaker}\` |\n`; + + // src/locale/.ts + await generateLocaleFile(locale); + + // src/locales/**/index.ts + await generateRecursiveModuleIndexes( + pathModules, + locale, + 'LocaleDefinition', + 1 + ); + } + + // src/locale/index.ts + + let localeIndexContent = ` + ${autoGeneratedCommentHeader} + + ${localeIndexImports} + + export { + ${localeIndexExportsIndividual} + }; + + export const allFakers = { + ${localeIndexExportsGrouped} + } as const; + `; + + localeIndexContent = await format(localeIndexContent, prettierTsOptions); + writeFileSync(pathLocaleIndex, localeIndexContent); + + // src/locales/index.ts + + let localesIndexContent = ` + ${autoGeneratedCommentHeader} + + ${localesIndexExports} + `; + + localesIndexContent = await format(localesIndexContent, prettierTsOptions); + writeFileSync(pathLocalesIndex, localesIndexContent); + + // docs/guide/localization.md + + localizationLocales = await format(localizationLocales, prettierMdOptions); + + let localizationContent = readFileSync(pathDocsGuideLocalization, 'utf8'); + localizationContent = localizationContent.replace( + /(^$).*(^$)/gms, + `$1\n\n\n\n${localizationLocales}\n$2` + ); + writeFileSync(pathDocsGuideLocalization, localizationContent); +} + +main().catch((error) => { + // Workaround until top level await is available + console.error(error); + process.exit(1); +}); diff --git a/scripts/generateLocales.ts b/scripts/generateLocales.ts deleted file mode 100644 index d2e9513e..00000000 --- a/scripts/generateLocales.ts +++ /dev/null @@ -1,476 +0,0 @@ -#!/usr/bin/env node - -/** - * This file contains a script that can be used to update the following files: - * - * - `src/locale/.ts` - * - `src/locales//index.ts` - * - `src/locales///index.ts` - * - `src/docs/guide/localization.md` - * - * If you wish to edit all/specific locale data files you can do so using the - * `updateLocaleFileHook()` method. - * Please remember to not commit your temporary update code. - * - * Run this script using `pnpm run generate:locales` - */ -import { - existsSync, - lstatSync, - readdirSync, - readFileSync, - writeFileSync, -} from 'node:fs'; -import { resolve } from 'node:path'; -import type { Options } from 'prettier'; -import { format } from 'prettier'; -import options from '../.prettierrc.js'; -import type { LocaleDefinition, MetadataDefinition } from '../src/definitions'; - -// Constants - -const pathRoot = resolve(__dirname, '..'); -const pathLocale = resolve(pathRoot, 'src', 'locale'); -const pathLocales = resolve(pathRoot, 'src', 'locales'); -const pathLocaleIndex = resolve(pathLocale, 'index.ts'); -const pathLocalesIndex = resolve(pathLocales, 'index.ts'); -const pathDocsGuideLocalization = resolve( - pathRoot, - 'docs', - 'guide', - 'localization.md' -); - -// Workaround for nameOf -type PascalCase = - TName extends `${infer Prefix}_${infer Remainder}` - ? `${Capitalize}${PascalCase}` - : Capitalize; - -type DefinitionType = { - [key in keyof LocaleDefinition]-?: PascalCase<`${key}Definition`>; -}; - -/** - * The types of the definitions. - */ -const definitionsTypes: DefinitionType = { - airline: 'AirlineDefinition', - animal: 'AnimalDefinition', - color: 'ColorDefinition', - commerce: 'CommerceDefinition', - company: 'CompanyDefinition', - database: 'DatabaseDefinition', - date: 'DateDefinition', - finance: 'FinanceDefinition', - hacker: 'HackerDefinition', - internet: 'InternetDefinition', - location: 'LocationDefinition', - lorem: 'LoremDefinition', - metadata: 'MetadataDefinition', - music: 'MusicDefinition', - person: 'PersonDefinition', - phone_number: 'PhoneNumberDefinition', - science: 'ScienceDefinition', - system: 'SystemDefinition', - vehicle: 'VehicleDefinition', - word: 'WordDefinition', -}; - -const prettierTsOptions: Options = { ...options, parser: 'typescript' }; -const prettierMdOptions: Options = { ...options, parser: 'markdown' }; - -const scriptCommand = 'pnpm run generate:locales'; - -const autoGeneratedCommentHeader = `/* - * This file is automatically generated. - * Run '${scriptCommand}' to update. - */`; - -// Helper functions - -function removeIndexTs(files: string[]): string[] { - const index = files.indexOf('index.ts'); - if (index !== -1) { - files.splice(index, 1); - } - - return files; -} - -function removeTsSuffix(files: string[]): string[] { - return files.map((file) => file.replace('.ts', '')); -} - -function escapeImport(parent: string, module: string): string { - if (['name', 'type', 'switch', parent].includes(module)) { - return `${module}_`; - } - - return module; -} - -function escapeField(parent: string, module: string): string { - if (['name', 'type', 'switch', parent].includes(module)) { - return `${module}: ${module}_`; - } - - return module; -} - -async function generateLocaleFile(locale: string): Promise { - const parts = locale.split('_'); - const locales = [locale]; - - for (let i = parts.length - 1; i > 0; i--) { - const fallback = parts.slice(0, i).join('_'); - if (existsSync(resolve(pathLocales, fallback))) { - locales.push(fallback); - } - } - - // TODO @Shinigami92 2023-03-07: Remove 'en' fallback in a separate PR - if (locales[locales.length - 1] !== 'en' && locale !== 'base') { - locales.push('en'); - } - - if (locales[locales.length - 1] !== 'base') { - locales.push('base'); - } - - let content = ` - ${autoGeneratedCommentHeader} - - import { Faker } from '../faker'; - ${locales - .map((imp) => `import ${imp} from '../locales/${imp}';`) - .join('\n')} - - export const faker = new Faker({ - locale: ${ - locales.length === 1 ? locales[0] : `[${locales.join(', ')}]` - }, - }); - `; - - content = await format(content, prettierTsOptions); - writeFileSync(resolve(pathLocale, `${locale}.ts`), content); -} - -async function generateLocalesIndexFile( - path: string, - name: string, - type: string, - depth: number -): Promise { - let modules = readdirSync(path); - modules = modules.filter((file) => !file.startsWith('.')); - modules = removeIndexTs(modules); - modules = removeTsSuffix(modules); - modules.sort(); - - const content = [autoGeneratedCommentHeader]; - let fieldType = ''; - if (type !== 'any') { - fieldType = `: ${type}`; - content.push( - `import type { ${type.replace(/\[.*/, '')} } from '..${'/..'.repeat( - depth - )}';` - ); - } - - content.push( - ...modules.map( - (module) => `import ${escapeImport(name, module)} from './${module}';` - ), - '', - `const ${name}${fieldType} = { - ${modules.map((module) => `${escapeField(name, module)},`).join('\n')} - };`, - '', - `export default ${name};` - ); - - writeFileSync( - resolve(path, 'index.ts'), - await format(content.join('\n'), prettierTsOptions) - ); -} - -async function generateRecursiveModuleIndexes( - path: string, - name: string, - definition: string, - depth: number -): Promise { - await generateLocalesIndexFile(path, name, definition, depth); - - let submodules = readdirSync(path); - submodules = removeIndexTs(submodules); - for (const submodule of submodules) { - const pathModule = resolve(path, submodule); - await updateLocaleFile(pathModule); - // Only process sub folders recursively - if (lstatSync(pathModule).isDirectory()) { - let moduleDefinition = - definition === 'any' ? 'any' : `${definition}['${submodule}']`; - - // Overwrite types of src/locales///index.ts for known definition types - if (depth === 1) { - moduleDefinition = definitionsTypes[submodule] ?? 'any'; - } - - // Recursive - await generateRecursiveModuleIndexes( - pathModule, - submodule, - moduleDefinition, - depth + 1 - ); - } - } -} - -/** - * Intermediate helper function to allow selectively updating locale data files. - * Use the `updateLocaleFileHook()` method to temporarily add your custom per file processing/update logic. - * - * @param filePath The full file path to the file. - */ -async function updateLocaleFile(filePath: string): Promise { - if (lstatSync(filePath).isFile()) { - const [locale, moduleKey, entryKey] = filePath - .substring(pathLocales.length + 1, filePath.length - 3) - .split(/[\\/]/); - await updateLocaleFileHook(filePath, locale, moduleKey, entryKey); - } -} - -/** - * Use this hook method to selectively update locale data files (not for index.ts files). - * This method is intended to be temporarily overwritten for one-time updates. - * - * @param filePath The full file path to the file. - * @param locale The locale for that file. - * @param definitionKey The definition key of the current file (ex. 'location'). - * @param entryName The entry key of the current file (ex. 'state'). Is `undefined` if `definitionKey` is `'metadata'`. - */ -async function updateLocaleFileHook( - filePath: string, - locale: string, - definitionKey: string, - entryName: string | undefined -): Promise { - // this needs to stay so all arguments are "used" - if (filePath === 'never') { - console.log(`${filePath} <-> ${locale} @ ${definitionKey} -> ${entryName}`); - } - - await normalizeLocaleFile(filePath, definitionKey); -} - -/** - * Normalizes the data of a locale file based on a set of rules. - * Those include: - * - filter the entry list for duplicates - * - limiting the maximum entries of a file to 1000 - * - sorting the entries alphabetically - * - * This function mutates the file by reading and writing to it! - * - * @param filePath The full file path to the file. - * @param definitionKey The definition key of the current file (ex. 'location'). - */ -async function normalizeLocaleFile(filePath: string, definitionKey: string) { - function normalizeDataRecursive(localeData: T): T { - if (typeof localeData !== 'object' || localeData === null) { - // we can only traverse object-like structs - return localeData; - } - - if (Array.isArray(localeData)) { - return ( - [...new Set(localeData)] - // limit entries to 1k - .slice(0, 1000) - // sort entries alphabetically - .sort() as T - ); - } - - const result = {} as T; - for (const key of Object.keys(localeData)) { - result[key] = normalizeDataRecursive(localeData[key]); - } - - return result; - } - - const legacyDefinitions = ['app', 'cell_phone', 'team']; - const definitionsToSkip = [ - 'airline', - 'animal', - 'color', - 'commerce', - 'company', - 'database', - 'date', - 'finance', - 'hacker', - 'internet', - 'location', - 'lorem', - 'metadata', - 'music', - 'person', - 'phone_number', - 'science', - 'system', - 'vehicle', - 'word', - ...legacyDefinitions, - ]; - if (definitionsToSkip.includes(definitionKey)) { - return; - } - - console.log(`Running data normalization for:`, filePath); - - const fileContent = readFileSync(filePath).toString(); - const searchString = 'export default '; - const compareIndex = fileContent.indexOf(searchString) + searchString.length; - const compareString = fileContent.substring(compareIndex); - - const isDynamicFile = compareString.startsWith('mergeArrays'); - const isNonApplicable = compareString.startsWith('null'); - const isFrozenData = compareString.startsWith('Object.freeze'); - if (isDynamicFile || isNonApplicable || isFrozenData) { - return; - } - - const validEntryListStartCharacters = ['[', '{']; - const staticFileOpenSyntax = validEntryListStartCharacters.find( - (validStart) => compareString.startsWith(validStart) - ); - if (staticFileOpenSyntax === undefined) { - console.log('Found an unhandled dynamic file:', filePath); - return; - } - - const fileContentPreData = fileContent.substring(0, compareIndex); - // eslint-disable-next-line @typescript-eslint/no-var-requires - const localeData = normalizeDataRecursive(require(filePath).default); - - // We reattach the content before the actual data implementation to keep stuff like comments. - // In the long term we should probably define a whether we want those in the files at all. - const newContent = fileContentPreData + JSON.stringify(localeData); - - writeFileSync(filePath, await format(newContent, prettierTsOptions)); -} - -// Start of actual logic - -async function main(): Promise { - const locales = readdirSync(pathLocales); - removeIndexTs(locales); - - let localeIndexImports = ''; - let localeIndexExportsIndividual = ''; - let localeIndexExportsGrouped = ''; - let localesIndexExports = ''; - - let localizationLocales = - '| Locale | Name | Faker |\n| :--- | :--- | :--- |\n'; - - for (const locale of locales) { - const pathModules = resolve(pathLocales, locale); - const pathMetadata = resolve(pathModules, 'metadata.ts'); - let localeTitle = 'No title found'; - try { - // eslint-disable-next-line @typescript-eslint/no-var-requires - const metadata: MetadataDefinition = require(pathMetadata).default; - const { title } = metadata; - if (!title) { - throw new Error( - `No title property found on ${JSON.stringify(metadata)}` - ); - } - - localeTitle = title; - } catch (error) { - console.error( - `Failed to load ${pathMetadata}. Please make sure the file exists and exports a MetadataDefinition.` - ); - console.error(error); - } - - const localizedFaker = `faker${locale.replace(/^([a-z]+)/, (part) => - part.toUpperCase() - )}`; - - localeIndexImports += `import { faker as ${localizedFaker} } from './${locale}';\n`; - localeIndexExportsIndividual += ` ${localizedFaker},\n`; - localeIndexExportsGrouped += ` ${locale}: ${localizedFaker},\n`; - localesIndexExports += `export { default as ${locale} } from './${locale}';\n`; - localizationLocales += `| \`${locale}\` | ${localeTitle} | \`${localizedFaker}\` |\n`; - - // src/locale/.ts - await generateLocaleFile(locale); - - // src/locales/**/index.ts - await generateRecursiveModuleIndexes( - pathModules, - locale, - 'LocaleDefinition', - 1 - ); - } - - // src/locale/index.ts - - let localeIndexContent = ` - ${autoGeneratedCommentHeader} - - ${localeIndexImports} - - export { - ${localeIndexExportsIndividual} - }; - - export const allFakers = { - ${localeIndexExportsGrouped} - } as const; - `; - - localeIndexContent = await format(localeIndexContent, prettierTsOptions); - writeFileSync(pathLocaleIndex, localeIndexContent); - - // src/locales/index.ts - - let localesIndexContent = ` - ${autoGeneratedCommentHeader} - - ${localesIndexExports} - `; - - localesIndexContent = await format(localesIndexContent, prettierTsOptions); - writeFileSync(pathLocalesIndex, localesIndexContent); - - // docs/guide/localization.md - - localizationLocales = await format(localizationLocales, prettierMdOptions); - - let localizationContent = readFileSync(pathDocsGuideLocalization, 'utf8'); - localizationContent = localizationContent.replace( - /(^$).*(^$)/gms, - `$1\n\n\n\n${localizationLocales}\n$2` - ); - writeFileSync(pathDocsGuideLocalization, localizationContent); -} - -main().catch((error) => { - // Workaround until top level await is available - console.error(error); - process.exit(1); -}); -- cgit v1.2.3