diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 00000000000..5b3084290b7 --- /dev/null +++ b/src/config.ts @@ -0,0 +1,20 @@ +/** + * The possible configuration options, that can be set. + * This type exists to be extended for plugins via type augmentation. + * + * The `@default` tag is used to indicate the default value, that should be used if, the config is absent. + */ +export interface FakerConfig { + /** + * The function used to generate the `refDate` date instance, if not provided as method param. + * The function must return a new valid `Date` instance for every call. + * + * @see [Reproducible Results](https://fakerjs.dev/guide/usage.html#reproducible-results) + * @see faker.seed(): For generating reproducible values. + * + * @since 9.0.0 + * + * @default () => new Date() + */ + defaultRefDate?: () => Date; +} diff --git a/src/core.ts b/src/core.ts new file mode 100644 index 00000000000..b661f6490cf --- /dev/null +++ b/src/core.ts @@ -0,0 +1,102 @@ +import type { FakerConfig } from './config'; +import type { LocaleDefinition } from './definitions'; +import type { Randomizer } from './randomizer'; +import { mergeLocales } from './utils/merge-locales'; +import { generateMersenne53Randomizer } from './utils/mersenne'; + +/** + * The core grants access to the locale data, the randomizer and config settings. + */ +export interface FakerCore { + /** + * The locale data associated with this instance. + * + * Always present, but it might be empty if the locale data is not available. + */ + readonly locale: LocaleDefinition; + + /** + * The randomizer used to generate random values. + */ + readonly randomizer: Randomizer; + + /** + * The configuration settings used by this instance. + */ + readonly config: FakerConfig; +} + +export interface FakerOptions { + /** + * The locale definitions to use. If not provided, this core will not have any locale data and thus all methods that rely on locale data will throw an error when called. + * + * @default {} + */ + locale?: LocaleDefinition | LocaleDefinition[]; + /** + * The randomizer used to generate random values. + * + * @default generateMersenne53Randomizer() + */ + randomizer?: Randomizer; + /** + * The configuration options for all methods. + * + * @default {} + */ + config?: FakerConfig; + /** + * The initial seed to use. + * The seed can be used to generate reproducible values. + * + * Refer to the `seed()` method for more information. + * + * Defaults to a random seed. + */ + seed?: number; +} + +/** + * Helper function to create a FakerCore instance. + * + * @param options The options to create the FakerCore instance with. + * @param options.locale The locale definitions to use. + * If not provided, this core will not have any locale data and thus all methods that rely on locale data will throw an error when called. + * This can be useful if you want to use least amount of memory possible and only use methods that do not rely on locale data. + * @param options.randomizer The randomizer used to generate random values. + * Defaults to `generateMersenne53Randomizer()`. + * @param options.config The configuration options for all methods. + * Defaults to an empty config. + * @param options.seed The initial seed to use. + * The seed can be used to generate reproducible values. + * Refer to the `seed()` method for more information. + * Defaults to a random seed. + * + * @returns The newly created FakerCore instance. + * + * @example + * import { createFakerCore, en } from '@faker-js/faker'; + * + * createFakerCore() // no locale data, default randomizer and empty config + * createFakerCore({ locale: en }) // custom locale data, default randomizer and empty config + * + * @since 10.5.0 + */ +export function createFakerCore(options: FakerOptions = {}): FakerCore { + const { + locale = {}, + randomizer = generateMersenne53Randomizer(), + config = {}, + seed, + } = options; + + if (randomizer != null && seed != null) { + randomizer.seed(seed); + } + + return { + locale: Array.isArray(locale) ? mergeLocales(locale) : locale, + randomizer, + config, + }; +} diff --git a/src/faker.ts b/src/faker.ts index cac89a32bda..18352f37261 100644 --- a/src/faker.ts +++ b/src/faker.ts @@ -1,3 +1,4 @@ +import type { FakerOptions } from './core'; import type { LocaleDefinition, MetadataDefinition } from './definitions'; import { FakerError } from './errors/faker-error'; import type { LocaleProxy } from './internal/locale-proxy'; @@ -26,9 +27,7 @@ import { ScienceModule } from './modules/science'; import { SystemModule } from './modules/system'; import { VehicleModule } from './modules/vehicle'; import { WordModule } from './modules/word'; -import type { Randomizer } from './randomizer'; import { SimpleFaker } from './simple-faker'; -import { mergeLocales } from './utils/merge-locales'; /** * This is Faker's main class containing all modules that can be used to generate data. @@ -56,7 +55,6 @@ import { mergeLocales } from './utils/merge-locales'; * customFaker.music.genre(); // throws Error as this data is not available in `es` */ export class Faker extends SimpleFaker { - readonly rawDefinitions: LocaleDefinition; readonly definitions: LocaleProxy; readonly airline: AirlineModule = new AirlineModule(this); @@ -84,6 +82,10 @@ export class Faker extends SimpleFaker { readonly vehicle: VehicleModule = new VehicleModule(this); readonly word: WordModule = new WordModule(this); + get rawDefinitions(): LocaleDefinition { + return this.fakerCore.locale; + } + /** * Creates a new instance of Faker. * @@ -120,51 +122,20 @@ export class Faker extends SimpleFaker { * * @since 8.0.0 */ - constructor(options: { - /** - * The locale data to use for this instance. - * If an array is provided, the first locale that has a definition for a given property will be used. - * Please make sure that all required locales and their parent locales are present, e.g. `[de_AT, de, en, base]`. - * - * @see mergeLocales(): For more information about how the locales are merged. - */ - locale: LocaleDefinition | LocaleDefinition[]; - - /** - * The Randomizer to use. - * Specify this only if you want to use it to achieve a specific goal, - * such as sharing the same random generator with other instances/tools. - * - * @default generateMersenne53Randomizer() - */ - randomizer?: Randomizer; + constructor(options: FakerOptions) { + super(options); - /** - * The initial seed to use. - * The seed can be used to generate reproducible values. - * - * Refer to the `seed()` method for more information. - * - * Defaults to a random seed. - */ - seed?: number; - }) { - super({ randomizer: options.randomizer, seed: options.seed }); + const { locale } = options; - let { locale } = options; - - if (Array.isArray(locale)) { - if (locale.length === 0) { - throw new FakerError( - 'The locale option must contain at least one locale definition.' - ); - } - - locale = mergeLocales(locale); + // TODO @ST-DDT 2026-03-08: We should either not throw or throw consistently when locale data are empty. + // And likely refer to simpleFaker as alternative + if (Array.isArray(locale) && locale.length === 0) { + throw new FakerError( + 'The locale option must contain at least one locale definition.' + ); } - this.rawDefinitions = locale; - this.definitions = createLocaleProxy(this.rawDefinitions); + this.definitions = createLocaleProxy(this.fakerCore.locale); } /** @@ -179,8 +150,6 @@ export class Faker extends SimpleFaker { * @since 8.1.0 */ getMetadata(): MetadataDefinition { - return this.rawDefinitions.metadata ?? {}; + return this.fakerCore.locale.metadata ?? {}; } } - -export type FakerOptions = ConstructorParameters[0]; diff --git a/src/index.ts b/src/index.ts index c100642a9e5..641380ca80a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,6 @@ +export type { FakerConfig } from './config'; +export { createFakerCore } from './core'; +export type { FakerCore, FakerOptions } from './core'; export type { AirlineDefinition, AnimalDefinition, @@ -30,7 +33,6 @@ export type { } from './definitions'; export { FakerError } from './errors/faker-error'; export { Faker } from './faker'; -export type { FakerOptions } from './faker'; export * from './locale'; export { fakerEN as faker } from './locale'; export * from './locales'; diff --git a/src/modules/helpers/eval.ts b/src/modules/helpers/eval.ts index 739f5af8e77..bf3a07cdbd0 100644 --- a/src/modules/helpers/eval.ts +++ b/src/modules/helpers/eval.ts @@ -66,7 +66,7 @@ const REGEX_DOT_OR_BRACKET = /\.|\(/; export function fakeEval( expression: string, faker: Faker, - entrypoints: ReadonlyArray = [faker, faker.rawDefinitions] + entrypoints: ReadonlyArray = [faker, faker.fakerCore.locale] ): unknown { if (expression.length === 0) { throw new FakerError('Eval expression cannot be empty.'); diff --git a/src/modules/number/index.ts b/src/modules/number/index.ts index 05ce591ecf8..dc5df50c158 100644 --- a/src/modules/number/index.ts +++ b/src/modules/number/index.ts @@ -96,8 +96,7 @@ export class NumberModule extends SimpleModuleBase { throw new FakerError(`Max ${max} should be greater than min ${min}.`); } - // @ts-expect-error: access private member field - const randomizer = this.faker._randomizer; + const { randomizer } = this.faker.fakerCore; const real = randomizer.next(); const delta = effectiveMax - effectiveMin + 1; // +1 for inclusive max bounds and even distribution return Math.floor(real * delta + effectiveMin) * multipleOf; @@ -210,8 +209,7 @@ export class NumberModule extends SimpleModuleBase { return int / factor; } - // @ts-expect-error: access private member field - const randomizer = this.faker._randomizer; + const { randomizer } = this.faker.fakerCore; const real = randomizer.next(); return real * (max - min) + min; } diff --git a/src/modules/person/index.ts b/src/modules/person/index.ts index 85154c35ff9..247315c00d3 100644 --- a/src/modules/person/index.ts +++ b/src/modules/person/index.ts @@ -137,13 +137,10 @@ export class PersonModule extends ModuleBase { * @since 8.0.0 */ lastName(sex?: SexType): string { - if (this.faker.rawDefinitions.person?.last_name_pattern != null) { + const patterns = this.faker.fakerCore.locale.person?.last_name_pattern; + if (patterns != null) { const pattern = this.faker.helpers.weightedArrayElement( - selectDefinition( - this.faker, - sex, - this.faker.rawDefinitions.person.last_name_pattern - ) + selectDefinition(this.faker, sex, patterns) ); return this.faker.helpers.fake(pattern); } diff --git a/src/simple-faker.ts b/src/simple-faker.ts index 1573444745d..e3e90cb24ed 100644 --- a/src/simple-faker.ts +++ b/src/simple-faker.ts @@ -1,3 +1,5 @@ +import type { FakerCore, FakerOptions } from './core'; +import { createFakerCore } from './core'; import { randomSeed } from './internal/seed'; import { DatatypeModule } from './modules/datatype'; import { SimpleDateModule } from './modules/date'; @@ -5,8 +7,8 @@ import { SimpleHelpersModule } from './modules/helpers'; import { SimpleLocationModule } from './modules/location'; import { NumberModule } from './modules/number'; import { StringModule } from './modules/string'; -import type { Randomizer } from './randomizer'; -import { generateMersenne53Randomizer } from './utils/mersenne'; + +export const DEFAULT_REF_DATE_SOURCE: () => Date = () => new Date(); /** * This is a simplified Faker class that doesn't need any localized data to generate its output. @@ -29,13 +31,18 @@ import { generateMersenne53Randomizer } from './utils/mersenne'; * simpleFaker.string.uuid(); // 'c50e1f5c-86e8-4aa9-888e-168e0a182519' */ export class SimpleFaker { - protected _defaultRefDate: () => Date = () => new Date(); + /** + * The faker core containing the randomizer and config to use. + * + * @internal + */ + readonly fakerCore: FakerCore; /** * Gets a new reference date used to generate relative dates. */ get defaultRefDate(): () => Date { - return this._defaultRefDate; + return this.fakerCore.config.defaultRefDate ?? DEFAULT_REF_DATE_SOURCE; } /** @@ -75,15 +82,12 @@ export class SimpleFaker { dateOrSource: string | Date | number | (() => Date) = () => new Date() ): void { if (typeof dateOrSource === 'function') { - this._defaultRefDate = dateOrSource; + this.fakerCore.config.defaultRefDate = dateOrSource; } else { - this._defaultRefDate = () => new Date(dateOrSource); + this.fakerCore.config.defaultRefDate = () => new Date(dateOrSource); } } - /** @internal */ - private readonly _randomizer: Randomizer; - readonly datatype: DatatypeModule = new DatatypeModule(this); readonly date: SimpleDateModule = new SimpleDateModule(this); readonly helpers: SimpleHelpersModule = new SimpleHelpersModule(this); @@ -118,35 +122,8 @@ export class SimpleFaker { * * @since 8.1.0 */ - constructor( - options: { - /** - * The Randomizer to use. - * Specify this only if you want to use it to achieve a specific goal, - * such as sharing the same random generator with other instances/tools. - * - * @default generateMersenne53Randomizer() - */ - randomizer?: Randomizer; - - /** - * The initial seed to use. - * The seed can be used to generate reproducible values. - * - * Refer to the `seed()` method for more information. - * - * Defaults to a random seed. - */ - seed?: number; - } = {} - ) { - const { randomizer, seed } = options; - - if (randomizer != null && seed != null) { - randomizer.seed(seed); - } - - this._randomizer = randomizer ?? generateMersenne53Randomizer(seed); + constructor(options?: FakerOptions) { + this.fakerCore = createFakerCore(options); } /** @@ -271,7 +248,7 @@ export class SimpleFaker { */ seed(seed?: number | number[]): number | number[]; seed(seed: number | number[] = randomSeed()): number | number[] { - this._randomizer.seed(seed); + this.fakerCore.randomizer.seed(seed); return seed; } diff --git a/test/all-functional.spec.ts b/test/all-functional.spec.ts index 7a6825e293a..2f24750fa3e 100644 --- a/test/all-functional.spec.ts +++ b/test/all-functional.spec.ts @@ -3,13 +3,7 @@ import type { Faker, allLocales } from '../src'; import { allFakers, fakerEN } from '../src'; import { keys } from '../src/internal/keys'; -const IGNORED_MODULES = new Set([ - 'rawDefinitions', - 'definitions', - 'helpers', - '_randomizer', - '_defaultRefDate', -]); +const IGNORED_MODULES = new Set(['definitions', 'helpers', 'fakerCore']); function getMethodNamesByModules(faker: Faker): { [module: string]: string[] } { return Object.fromEntries( diff --git a/test/core.spec.ts b/test/core.spec.ts new file mode 100644 index 00000000000..d6bc607f634 --- /dev/null +++ b/test/core.spec.ts @@ -0,0 +1,123 @@ +import { describe, expect, it, vi } from 'vitest'; +import type { FakerConfig } from '../src/config'; +import { createFakerCore } from '../src/core'; +import type { LocaleDefinition } from '../src/definitions/definitions'; +import type { Randomizer } from '../src/randomizer'; +import { generateMersenne53Randomizer } from '../src/utils/mersenne'; + +describe('createFakerCore', () => { + describe('locale', () => { + it('should handle missing options', () => { + const actual = createFakerCore(); + + expect(actual.locale).toEqual({}); + }); + + it('should handle empty options', () => { + const actual = createFakerCore({}); + + expect(actual.locale).toEqual({}); + }); + + it('should handle undefined locale options', () => { + const actual = createFakerCore({ locale: undefined }); + + expect(actual.locale).toEqual({}); + }); + + it('should handle empty locale array', () => { + const actual = createFakerCore({ locale: [] }); + + expect(actual.locale).toEqual({}); + }); + + it('should handle single locale', () => { + const locale: LocaleDefinition = { test: { test: 'test' } }; + const actual = createFakerCore({ locale }); + + expect(actual.locale).toEqual(locale); + }); + + it('should handle multiple locales', () => { + const locale1: LocaleDefinition = { test1: { test: 'test1' } }; + const locale2: LocaleDefinition = { test2: { test: 'test2' } }; + const actual = createFakerCore({ locale: [locale1, locale2] }); + + expect(actual.locale).toEqual({ ...locale1, ...locale2 }); + }); + }); + + describe('randomizer', () => { + it('should handle missing options', () => { + const actual = createFakerCore(); + + expect(actual.randomizer).toBeDefined(); + }); + + it('should handle undefined randomizer options', () => { + const actual = createFakerCore({ randomizer: undefined }); + + expect(actual.randomizer).toBeDefined(); + }); + + it('should use provided randomizer', () => { + const randomizer: Randomizer = { next: () => 0, seed: () => {} }; + const actual = createFakerCore({ randomizer }); + + expect(actual.randomizer).toBe(randomizer); + }); + }); + + describe('config', () => { + it('should handle missing options', () => { + const actual = createFakerCore(); + + expect(actual.config).toEqual({}); + }); + + it('should handle undefined config options', () => { + const actual = createFakerCore({ config: undefined }); + + expect(actual.config).toEqual({}); + }); + + it('should use provided config', () => { + const config: FakerConfig = { + defaultRefDate: () => new Date('2020-01-01'), + }; + const actual = createFakerCore({ config }); + + expect(actual.config).toBe(config); + }); + }); + + describe('seed', () => { + it('should not re-seed when only randomizer is provided', () => { + const randomizer = generateMersenne53Randomizer(0); + + const spy = vi.spyOn(randomizer, 'seed'); + + const actual = createFakerCore({ randomizer }); + + expect(spy).not.toHaveBeenCalled(); + expect(actual.randomizer.next()).toBe(0.5488135039273248); + }); + + it('should seed when only seed is provided', () => { + const actual = createFakerCore({ seed: 123 }); + + expect(actual.randomizer.next()).toBe(0.6964691855978616); + }); + + it('should re-seed when both are provided', () => { + const randomizer = generateMersenne53Randomizer(0); + + const spy = vi.spyOn(randomizer, 'seed'); + + const actual = createFakerCore({ randomizer, seed: 123 }); + + expect(spy).toHaveBeenCalledWith(123); + expect(actual.randomizer.next()).toBe(0.6964691855978616); + }); + }); +}); diff --git a/test/modules/number.spec.ts b/test/modules/number.spec.ts index 9e342637ef0..be663dd6f8f 100644 --- a/test/modules/number.spec.ts +++ b/test/modules/number.spec.ts @@ -874,8 +874,7 @@ describe('number', () => { describe('value range tests', () => { const customFaker = new SimpleFaker(); - // @ts-expect-error: access private member field - const randomizer = customFaker._randomizer; + const { randomizer } = customFaker.fakerCore; describe('int', () => { it('should be able to return 0', () => { randomizer.next = () => 0; diff --git a/test/modules/person.spec.ts b/test/modules/person.spec.ts index 36b09dc66c6..6101bcc7602 100644 --- a/test/modules/person.spec.ts +++ b/test/modules/person.spec.ts @@ -157,13 +157,13 @@ describe('person', () => { it('should return a female applicable name without firstName and lastName', () => { const female_applicable = [ - ...(fakerMK.rawDefinitions.person?.prefix?.female ?? []), - ...(fakerMK.rawDefinitions.person?.first_name?.female ?? []), - ...(fakerMK.rawDefinitions.person?.last_name?.female ?? []), - ...(fakerMK.rawDefinitions.person?.prefix?.generic ?? []), - ...(fakerMK.rawDefinitions.person?.first_name?.generic ?? []), - ...(fakerMK.rawDefinitions.person?.last_name?.generic ?? []), - // ...(fakerMK.rawDefinitions.person?.suffix ?? []), Not applicable + ...(fakerMK.fakerCore.locale.person?.prefix?.female ?? []), + ...(fakerMK.fakerCore.locale.person?.first_name?.female ?? []), + ...(fakerMK.fakerCore.locale.person?.last_name?.female ?? []), + ...(fakerMK.fakerCore.locale.person?.prefix?.generic ?? []), + ...(fakerMK.fakerCore.locale.person?.first_name?.generic ?? []), + ...(fakerMK.fakerCore.locale.person?.last_name?.generic ?? []), + // ...(fakerMK.fakerCore.locale.person?.suffix ?? []), Not applicable ]; const fullName = fakerMK.person.fullName({ sex: 'female' }); @@ -176,13 +176,13 @@ describe('person', () => { it('should return a male applicable name without firstName and lastName', () => { const male_applicable = [ - ...(fakerMK.rawDefinitions.person?.prefix?.male ?? []), - ...(fakerMK.rawDefinitions.person?.first_name?.male ?? []), - ...(fakerMK.rawDefinitions.person?.last_name?.male ?? []), - ...(fakerMK.rawDefinitions.person?.prefix?.generic ?? []), - ...(fakerMK.rawDefinitions.person?.first_name?.generic ?? []), - ...(fakerMK.rawDefinitions.person?.last_name?.generic ?? []), - // ...(fakerMK.rawDefinitions.person?.suffix ?? []), Not applicable + ...(fakerMK.fakerCore.locale.person?.prefix?.male ?? []), + ...(fakerMK.fakerCore.locale.person?.first_name?.male ?? []), + ...(fakerMK.fakerCore.locale.person?.last_name?.male ?? []), + ...(fakerMK.fakerCore.locale.person?.prefix?.generic ?? []), + ...(fakerMK.fakerCore.locale.person?.first_name?.generic ?? []), + ...(fakerMK.fakerCore.locale.person?.last_name?.generic ?? []), + // ...(fakerMK.fakerCore.locale.person?.suffix ?? []), Not applicable ]; const fullName = fakerMK.person.fullName({ sex: 'male' }); @@ -195,11 +195,11 @@ describe('person', () => { it('should return a female applicable name with given firstName and lastName', () => { const female_applicable = [ - ...(fakerMK.rawDefinitions.person?.prefix?.female ?? []), - ...(fakerMK.rawDefinitions.person?.prefix?.generic ?? []), + ...(fakerMK.fakerCore.locale.person?.prefix?.female ?? []), + ...(fakerMK.fakerCore.locale.person?.prefix?.generic ?? []), 'firstName', 'lastName', - // ...(fakerMK.rawDefinitions.person?.suffix ?? []), Not applicable + // ...(fakerMK.fakerCore.locale.person?.suffix ?? []), Not applicable ]; const fullName = fakerMK.person.fullName({ @@ -216,11 +216,11 @@ describe('person', () => { it('should return a male applicable name with given firstName and lastName', () => { const male_applicable = [ - ...(fakerMK.rawDefinitions.person?.prefix?.male ?? []), - ...(fakerMK.rawDefinitions.person?.prefix?.generic ?? []), + ...(fakerMK.fakerCore.locale.person?.prefix?.male ?? []), + ...(fakerMK.fakerCore.locale.person?.prefix?.generic ?? []), 'firstName', 'lastName', - // ...(fakerMK.rawDefinitions.person?.suffix ?? []), Not applicable + // ...(fakerMK.fakerCore.locale.person?.suffix ?? []), Not applicable ]; const fullName = fakerMK.person.fullName({ @@ -283,9 +283,9 @@ describe('person', () => { expect(prefix).toBeTypeOf('string'); const all_applicable = [ - ...(faker.definitions.person.prefix.generic ?? []), - ...(faker.definitions.person.prefix.female ?? []), - ...(faker.definitions.person.prefix.male ?? []), + ...(faker.fakerCore.locale.person?.prefix?.generic ?? []), + ...(faker.fakerCore.locale.person?.prefix?.female ?? []), + ...(faker.fakerCore.locale.person?.prefix?.male ?? []), ]; expect(all_applicable).toContain(prefix); });