Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
6eb7a4d
refactor: _prefix module helpers
ST-DDT Mar 3, 2026
332c09e
feat: introduce standalone module functions (transform script)
ST-DDT Apr 9, 2026
9e82be4
chore: run transform script (dirty)
ST-DDT Mar 3, 2026
2e53347
refactor: transform aircraft
ST-DDT Mar 3, 2026
72cfc95
refactor: transform color
ST-DDT Mar 4, 2026
1bf806a
refactor: transform commerce
ST-DDT Mar 4, 2026
0de5355
refactor: transform date
ST-DDT Mar 4, 2026
5e35c22
refactor: transform finance
ST-DDT Mar 4, 2026
108e80e
refactor: transform food
ST-DDT Mar 4, 2026
50a520d
refactor: transform git
ST-DDT Mar 4, 2026
0cb5153
refactor: transform helpers
ST-DDT Mar 4, 2026
6f346db
refactor: transform image
ST-DDT Mar 5, 2026
7623771
refactor: transform internet
ST-DDT Mar 5, 2026
a7d2f60
refactor: transform location
ST-DDT Mar 5, 2026
0b4eab8
refactor: transform number
ST-DDT Mar 5, 2026
0ac6ec2
refactor: transform lorem
ST-DDT Mar 5, 2026
ccff0f0
refactor: transform internet
ST-DDT Mar 5, 2026
d987157
refactor: transform person
ST-DDT Mar 5, 2026
5989897
refactor: transform phone
ST-DDT Mar 6, 2026
7a9dadd
refactor: transform science
ST-DDT Mar 5, 2026
3ca64aa
refactor: transform string
ST-DDT Mar 6, 2026
d1be5b2
refactor: transform system
ST-DDT Mar 6, 2026
c71818a
refactor: transform word
ST-DDT Mar 6, 2026
a1fda8a
infra: introduce generate-module-tree script
ST-DDT Mar 14, 2026
111e4db
chore: apply generate-module-tree script
ST-DDT Mar 14, 2026
a83e7b1
refactor: hardcode fake entrypoints
ST-DDT Mar 16, 2026
7cf17b3
chore: cleanup
ST-DDT Mar 16, 2026
3da3ff2
test: update test snapshots
ST-DDT Apr 9, 2026
3212549
transform helpers
ST-DDT Apr 22, 2026
c5d072a
module script
ST-DDT Apr 22, 2026
344b3e5
apply module script
ST-DDT Apr 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
300 changes: 300 additions & 0 deletions scripts/generate-module-tree.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,300 @@
import { writeFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { SyntaxKind } from 'ts-morph';
import { getDeprecated, getJsDocs } from './apidocs/processing/jsdocs';
import { getProject } from './apidocs/project';
import { toCamelCase, toKebabCase } from './shared/character-case';
import { formatTypescript } from './shared/format';
import { FILE_PATH_SRC } from './shared/paths';

const project = getProject();

const directories = project
.getDirectoryOrThrow('src')
.getDirectoryOrThrow('modules')
.getDirectories();

const moduleNames = new Set(directories.map((dir) => dir.getBaseName()));

//#region Module
for (const directory of directories) {
const moduleName = directory.getBaseName();

console.log(`Processing module: ${moduleName}`);
//#region Index
const indexFile = directory.getSourceFileOrThrow('index.ts');

const header = indexFile
.getStatements()[0]
?.getLeadingCommentRanges()
.map((c) => c.getText());

const imports = new Set([
`import { SimpleModuleBase } from '../../internal/module-base';`,
`import { ModuleBase } from '../../internal/module-base';`,
`import type { Faker } from '../../faker';`,
`import type { LiteralUnion } from '../../internal/types';`,
`import { fakerRegistry } from '../../registry';`,
`import type { Distributor } from '../../distributors/distributor';`,
]);
if (moduleName === 'image') {
imports.add(`import type { SexType } from '../person';`);
}

const exports: string[] = indexFile
.getExportDeclarations()
.map((exp) => exp.getText());

const typesFile = directory.getSourceFile('_types.ts');
if (typesFile) {
const typesToImport = [
typesFile.getEnums(),
typesFile.getTypeAliases(),
typesFile.getInterfaces(),
]
.flat()
.filter((decl) => decl.isExported())
.map((decl) => decl.getName());

if (typesToImport.length > 0) {
imports.add(
`import type { ${typesToImport.join(', ')} } from './_types';`
);
}
}

const content: string[] = [];
const classes = indexFile?.getClasses() ?? [];

//#region Module Classes
for (const cls of classes) {
content.push(getJsDocs(cls).getText());
const methodNames = cls.getMethods().map((method) => method.getName());
for (const method of cls.getMethods()) {
method.remove();
}

for (const methodName of methodNames) {
const methodFile = directory.getSourceFileOrThrow(
`${toKebabCase(methodName)}.ts`
);

const typesToImport = [
methodFile.getEnums(),
methodFile.getTypeAliases(),
methodFile.getInterfaces(),
]
.flat()
.filter((decl) => decl.isExported())
.map((decl) => decl.getName());

imports.add(
`import { ${methodName} as ${toCamelCase(moduleName, methodName)} } from './${toKebabCase(methodName)}';`
);
if (typesToImport.length > 0) {
imports.add(
`import type { ${typesToImport.join(', ')} } from './${toKebabCase(methodName)}';`
);
}

const functions = methodFile
.getChildrenOfKind(SyntaxKind.FunctionDeclaration)
.filter((fn) => fn.isExported())
.filter((fn) => fn.getName() === methodName);

const parts: string[] = [];

const restoreFakerTreeInvocations = (
_: string,
module: string,
method: string
): string =>
methodNames.includes(`${module}${method}`)
? `faker.${moduleName}.${module}${method}(`
: moduleNames.has(module)
? `faker.${module}.${toCamelCase(method)}(`
: `faker.${module}${method}(`;

for (const child of functions) {
//#region Module Functions
const jsDocs = child.getJsDocs()[0];

if (child.hasBody()) {
const params = child
.getSignature()
.getParameters()
.slice(1)
.map((param) => param.getName());

const isDeprecated = jsDocs && getDeprecated(jsDocs);

child.setBodyText(
`${
isDeprecated
? '// eslint-disable-next-line @typescript-eslint/no-deprecated -- Internal call\n'
: ''
}return ${toCamelCase(moduleName, methodName)}(this.faker.fakerCore, ${params.join(', ')});`
);
}

if (jsDocs) {
let description = jsDocs
.getFullText()
// Param
.replace(' * @param fakerCore The FakerCore to use.\n', '')
.replaceAll(/ +\*\n +\*\n/g, ' *\n')
// Examples
.replaceAll(
new RegExp(`${methodName}\\(fakerCore(?:, ?)?`, 'g'),
`faker.${moduleName}.${methodName}(`
)
// Method References
.replaceAll(
/\b([a-z]+)([A-Z][a-zA-Z]+)\(fakerCore(?:, ?)?/g,
restoreFakerTreeInvocations
)
.replaceAll(
/\b([a-zA-Z]+)\(fakerCore(?:, ?)?/g,
(_, method: string) =>
`faker.${moduleName}.${toCamelCase(method)}(`
)
// Fake cleanup
.replaceAll(
'Defaults to `[ fakerCore.locale.raw ]`.',
'Defaults to `[ fakerRegistry, this.faker.rawDefinitions ]`.'
);

if (methodName === 'fake') {
description = description.replaceAll(', [...]', '');
}

parts.push(description);
}

const signature = child
.getSignature()
.getDeclaration()
.getText()
// Adapt signature
.replace('export function ', '')
.replace(/\((\n +)?fakerCore: FakerCore,?/, '(')
// Adapt nested options defaults
.replaceAll(
/(?<= +\* .*?)\bgetDefaultRefDate\(fakerCore(?:, ?)?/g,
'faker.defaultRefDate('
)
.replaceAll(
/(?<= +\* .*?)\b([a-z]+)([A-Z][a-zA-Z]+)\(fakerCore(?:, ?)?/g,
restoreFakerTreeInvocations
);

parts.push(signature);
//#endregion
}

cls.addMember(
parts
.join('\n')
// Fake cleanup
.replaceAll(
'[fakerCore.locale.raw]',
'[fakerRegistry, this.faker.rawDefinitions]'
)
);
}
//#endregion

content.push(cls.getText(), '');
}

content.unshift(...header, ...imports, '', ...exports, '');

writeFileSync(
resolve(FILE_PATH_SRC, 'modules', moduleName, 'index.ts'),
await formatTypescript(content.join('\n')),
'utf8'
);
//#endregion

//#region Module Registry
const methodNames = new Set(
directory
.getSourceFiles()
.filter(
(file) =>
file.getBaseName() !== 'index.ts' &&
!file.getBaseName().startsWith('_')
)
.flatMap((file) =>
file
.getFunctions()
.filter((fn) => fn.isExported())
.filter(
(fn) =>
toKebabCase(fn.getNameOrThrow()) ===
file.getBaseNameWithoutExtension()
)
.map((fn) => fn.getNameOrThrow())
.map((s) => toCamelCase(s))
)
);

const registry = `
${[...methodNames]
.map(
(methodName) =>
`import { ${methodName} } from './${toKebabCase(methodName)}';`
)
.join('\n')}

export const ${toCamelCase(moduleName)}Module = {
${[...methodNames]
.toSorted()
.map((methodName) => ` ${methodName},`)
.join('\n')}
};
`;

writeFileSync(
resolve(FILE_PATH_SRC, 'modules', moduleName, 'registry.ts'),
await formatTypescript(registry),
'utf8'
);
//#endregion
}
//#endregion

//#region Global Module Registry
console.log('Generating module registry...');
const fakerRegistryContent = `
import type { FakerCore } from '.';
${[...moduleNames]
.map(
(name) =>
`import { ${toCamelCase(name)}Module as ${toCamelCase(name)} } from './modules/${name}/registry';`
)
.join('\n')}
import { utilsModule as utils } from './utils/registry';

/**
* Global Registry for the Faker library, containing all module registries.
*/
type FakerRegistry = Record<
string,
Record<string, (fakerCore: FakerCore, ...args: never[]) => unknown>
>;

/**
* Global registry for the Faker library, containing all module registries with their standalone module functions.
*/
export const fakerRegistry = {
${[...moduleNames, 'utils'].map((name) => ` ${toCamelCase(name)},`).join('\n')}
} satisfies FakerRegistry;
`;

writeFileSync(
resolve(FILE_PATH_SRC, 'registry.ts'),
await formatTypescript(fakerRegistryContent),
'utf8'
);
//#endregion
19 changes: 19 additions & 0 deletions scripts/shared/character-case.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export function toKebabCase(...values: string[]): string {
return values
.join('-')
.replaceAll(/([a-z])([A-Z])/g, '$1-$2')
.replaceAll(/[\s_]+/g, '-')
.toLowerCase();
}

export function toCamelCase(...values: string[]): string {
const text = values
.flatMap((value) => value.split(/[\s_-]+/))
.map(toPascalCase)
.join('');
return text.substring(0, 1).toLowerCase() + text.substring(1);
}

export function toPascalCase(value: string): string {
return value.substring(0, 1).toUpperCase() + value.substring(1);
}
2 changes: 1 addition & 1 deletion scripts/shared/paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export const FILE_PATH_DOCS_LOCALES = resolve(FILE_PATH_DOCS, 'locales');
/**
* The path to the src directory.
*/
const FILE_PATH_SRC = resolve(FILE_PATH_PROJECT, 'src');
export const FILE_PATH_SRC = resolve(FILE_PATH_PROJECT, 'src');
/**
* The path to the locale source files.
*/
Expand Down
Loading
Loading