diff --git a/src/core.ts b/src/core.ts index b661f6490cf..6fa3ae0b828 100644 --- a/src/core.ts +++ b/src/core.ts @@ -1,5 +1,7 @@ import type { FakerConfig } from './config'; import type { LocaleDefinition } from './definitions'; +import type { LocaleProxy } from './internal/locale-proxy'; +import { createLocaleProxy } from './internal/locale-proxy'; import type { Randomizer } from './randomizer'; import { mergeLocales } from './utils/merge-locales'; import { generateMersenne53Randomizer } from './utils/mersenne'; @@ -13,7 +15,7 @@ export interface FakerCore { * * Always present, but it might be empty if the locale data is not available. */ - readonly locale: LocaleDefinition; + readonly locale: LocaleProxy; /** * The randomizer used to generate random values. @@ -32,7 +34,7 @@ export interface FakerOptions { * * @default {} */ - locale?: LocaleDefinition | LocaleDefinition[]; + locale?: LocaleProxy | LocaleDefinition | LocaleDefinition[]; /** * The randomizer used to generate random values. * @@ -95,7 +97,9 @@ export function createFakerCore(options: FakerOptions = {}): FakerCore { } return { - locale: Array.isArray(locale) ? mergeLocales(locale) : locale, + locale: createLocaleProxy( + Array.isArray(locale) ? mergeLocales(locale) : locale + ), randomizer, config, }; diff --git a/src/faker.ts b/src/faker.ts index 8bc4256357b..16a80384eee 100644 --- a/src/faker.ts +++ b/src/faker.ts @@ -2,7 +2,6 @@ import type { FakerOptions } from './core'; import type { LocaleDefinition, MetadataDefinition } from './definitions'; import { FakerError } from './errors/faker-error'; import type { LocaleProxy } from './internal/locale-proxy'; -import { createLocaleProxy } from './internal/locale-proxy'; import { AirlineModule } from './modules/airline'; import { AnimalModule } from './modules/animal'; import { BookModule } from './modules/book'; @@ -57,8 +56,6 @@ import { SimpleFaker } from './simple-faker'; * customFaker.music.genre(); // throws Error as this data is not available in `es` */ export class Faker extends SimpleFaker { - readonly definitions: LocaleProxy; - readonly airline: AirlineModule = new AirlineModule(this); readonly animal: AnimalModule = new AnimalModule(this); readonly book: BookModule = new BookModule(this); @@ -85,6 +82,10 @@ export class Faker extends SimpleFaker { readonly word: WordModule = new WordModule(this); get rawDefinitions(): LocaleDefinition { + return this.fakerCore.locale.raw; + } + + get definitions(): LocaleProxy { return this.fakerCore.locale; } @@ -136,8 +137,6 @@ export class Faker extends SimpleFaker { 'The locale option must contain at least one locale definition.' ); } - - this.definitions = createLocaleProxy(this.fakerCore.locale); } /** @@ -152,6 +151,6 @@ export class Faker extends SimpleFaker { * @since 8.1.0 */ getMetadata(): MetadataDefinition { - return this.fakerCore.locale.metadata ?? {}; + return this.fakerCore.locale.raw.metadata ?? {}; } } diff --git a/src/internal/locale-proxy.ts b/src/internal/locale-proxy.ts index 3e77a93a4cd..88e6557b22a 100644 --- a/src/internal/locale-proxy.ts +++ b/src/internal/locale-proxy.ts @@ -1,12 +1,28 @@ import type { LocaleDefinition } from '../definitions'; import { FakerError } from '../errors/faker-error'; +const LOCALE_PROXY_TAG = Symbol('FakerLocaleProxy'); + /** * A proxy for LocaleDefinition that marks all properties as required and throws an error when an entry is accessed that is not defined. */ -export type LocaleProxy = Readonly<{ - [key in keyof LocaleDefinition]-?: LocaleProxyCategory; -}>; +export type LocaleProxy = Readonly< + { + [key in keyof LocaleDefinition]-?: LocaleProxyCategory< + LocaleDefinition[key] + >; + } & { + /** + * The raw locale definition used to create this proxy. + * This can be useful to check if a category/entry exists without triggering the proxy's error. + */ + raw: LocaleDefinition; + /** + * Marker to identify a `LocaleProxy`. + */ + [LOCALE_PROXY_TAG]: true; + } +>; type LocaleProxyCategory = Readonly<{ [key in keyof T]-?: LocaleProxyEntry; @@ -18,13 +34,35 @@ const throwReadOnlyError: () => never = () => { throw new FakerError('You cannot edit the locale data on the faker instance'); }; +/** + * Checks if the given value is a LocaleProxy. + * + * @param value The value to check. + * + * @returns True if the value is a LocaleProxy, false otherwise. + */ +function isLocaleProxy(value: unknown): value is LocaleProxy { + return ( + value != null && + typeof value === 'object' && + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (value as any)?.[LOCALE_PROXY_TAG] === true + ); +} + /** * Creates a proxy for LocaleDefinition that throws an error if an undefined property is accessed. * * @param locale The locale definition to create the proxy for. */ -export function createLocaleProxy(locale: LocaleDefinition): LocaleProxy { - const proxies = {} as LocaleDefinition; +export function createLocaleProxy( + locale: LocaleDefinition | LocaleProxy +): LocaleProxy { + if (isLocaleProxy(locale)) { + return locale; + } + + const proxies = { raw: locale } as LocaleDefinition; return new Proxy(locale, { has(): true { // Categories are always present (proxied), that's why we return true. @@ -33,17 +71,21 @@ export function createLocaleProxy(locale: LocaleDefinition): LocaleProxy { get( target: LocaleDefinition, - categoryName: keyof LocaleDefinition - ): LocaleDefinition[keyof LocaleDefinition] { - if (typeof categoryName === 'symbol' || categoryName === 'nodeType') { + categoryName: keyof LocaleProxy + ): LocaleProxy[keyof LocaleProxy] { + if (typeof categoryName === 'symbol') { + if (categoryName === LOCALE_PROXY_TAG) { + return true; + } + return target[categoryName]; } - if (categoryName in proxies) { - return proxies[categoryName]; + if (categoryName === 'nodeType') { + return target[categoryName]; } - return (proxies[categoryName] = createCategoryProxy( + return (proxies[categoryName] ??= createCategoryProxy( categoryName, target[categoryName] )); @@ -51,7 +93,7 @@ export function createLocaleProxy(locale: LocaleDefinition): LocaleProxy { set: throwReadOnlyError, deleteProperty: throwReadOnlyError, - }) as LocaleProxy; + }) as unknown as LocaleProxy; } /** diff --git a/src/modules/helpers/eval.ts b/src/modules/helpers/eval.ts index bf3a07cdbd0..1167639cde9 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.fakerCore.locale] + entrypoints: ReadonlyArray = [faker, faker.definitions.raw] ): unknown { if (expression.length === 0) { throw new FakerError('Eval expression cannot be empty.'); diff --git a/src/modules/person/index.ts b/src/modules/person/index.ts index 0b905632d1d..ef5b0274bbc 100644 --- a/src/modules/person/index.ts +++ b/src/modules/person/index.ts @@ -137,7 +137,7 @@ export class PersonModule extends ModuleBase { * @since 8.0.0 */ lastName(sex?: SexType): string { - const patterns = this.faker.fakerCore.locale.person?.last_name_pattern; + const patterns = this.faker.definitions.raw.person?.last_name_pattern; if (patterns != null) { const pattern = this.faker.helpers.weightedArrayElement( selectDefinition(this.faker, sex, patterns) diff --git a/test/core.spec.ts b/test/core.spec.ts index d6bc607f634..943c1ebefd2 100644 --- a/test/core.spec.ts +++ b/test/core.spec.ts @@ -2,6 +2,7 @@ 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 { createLocaleProxy } from '../src/internal/locale-proxy'; import type { Randomizer } from '../src/randomizer'; import { generateMersenne53Randomizer } from '../src/utils/mersenne'; @@ -45,6 +46,16 @@ describe('createFakerCore', () => { expect(actual.locale).toEqual({ ...locale1, ...locale2 }); }); + + it('should handle LocaleProxy', () => { + const locale: LocaleDefinition = { test1: { test: 'test1' } }; + const proxy = createLocaleProxy(locale); + const actual = createFakerCore({ locale: proxy }); + + expect(actual.locale).toBe(proxy); + expect(actual.locale).toEqual(locale); + expect(actual.locale.raw).toBe(locale); + }); }); describe('randomizer', () => { diff --git a/test/internal/locale-proxy.spec.ts b/test/internal/locale-proxy.spec.ts index d13b04dd3ad..c93d1ec9101 100644 --- a/test/internal/locale-proxy.spec.ts +++ b/test/internal/locale-proxy.spec.ts @@ -14,6 +14,20 @@ describe('LocaleProxy', () => { it('should be possible to use not equals on locale', () => { expect(locale).not.toEqual(createLocaleProxy({})); }); + + it('should be possible to pass a LocaleProxy to createLocaleProxy', () => { + const proxy = createLocaleProxy(locale); + + expect(proxy).toBe(locale); + }); + + it('should be possible to access raw without throwing', () => { + expect(locale.raw.missing?.missing).toBeUndefined(); + }); + + it('should expose the original locale definition via raw', () => { + expect(locale.raw).toBe(en); + }); }); describe('category', () => {