Skip to content

Commit dc7cd28

Browse files
committed
feat: unproxy locale access
1 parent e765b6b commit dc7cd28

12 files changed

Lines changed: 292 additions & 46 deletions

File tree

docs/.vitepress/components/api-docs/refreshable-code.vue

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,13 @@ function initRefresh(): Element[] {
2525
let lineIndex = 0;
2626
const result: Element[] = [];
2727
while (lineIndex < domLines.length) {
28-
// Skip empty and preparatory lines (no '^faker.' invocation)
28+
// Skip empty and preparatory lines (no recorded invocation)
29+
// Keep in sync with ref scripts/shared/refreshable-code.ts
2930
if (
3031
domLines[lineIndex]?.children.length === 0 ||
31-
!/^\w*faker\w*\./i.test(domLines[lineIndex]?.textContent ?? '')
32+
!/^\w*faker\w*\.|^\w+\((faker\.)?fakerCore/i.test(
33+
domLines[lineIndex]?.textContent ?? ''
34+
)
3235
) {
3336
lineIndex++;
3437
continue;

scripts/apidocs/output/page.ts

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,18 +23,27 @@ editLink: false
2323
* @param pages The pages to write.
2424
*/
2525
export async function writePages(pages: RawApiDocsPage[]): Promise<void> {
26-
await Promise.all(pages.map(writePage));
26+
const registryHints: Record<string, string> = Object.fromEntries(
27+
pages.flatMap((page) =>
28+
page.methods.map((method) => [method.name, page.camelTitle])
29+
)
30+
);
31+
await Promise.all(pages.map((page) => writePage(page, registryHints)));
2732
}
2833

2934
/**
3035
* Writes the api docs page and data for the given module to the correct location.
3136
*
3237
* @param page The page to write.
38+
* @param registryHints Hints for accessing SMF via module registry.
3339
*/
34-
async function writePage(page: RawApiDocsPage): Promise<void> {
40+
async function writePage(
41+
page: RawApiDocsPage,
42+
registryHints: Record<string, string> = {}
43+
): Promise<void> {
3544
try {
3645
await writePageMarkdown(page);
37-
await writePageData(page);
46+
await writePageData(page, registryHints);
3847
} catch (error) {
3948
throw new Error(`Error writing page ${page.title}`, { cause: error });
4049
}
@@ -98,20 +107,29 @@ async function writePageMarkdown(page: RawApiDocsPage): Promise<void> {
98107
* Writes the api docs data for the given module to correct location.
99108
*
100109
* @param page The page to write.
110+
* @param registryHints Hints for accessing SMF via module registry.
101111
*/
102-
async function writePageData(page: RawApiDocsPage): Promise<void> {
112+
async function writePageData(
113+
page: RawApiDocsPage,
114+
registryHints: Record<string, string> = {}
115+
): Promise<void> {
103116
const { camelTitle, methods } = page;
104117
const pageData: Record<string, ApiDocsMethod> = Object.fromEntries(
105118
await Promise.all(
106119
methods.map(async (method) => [method.name, await toMethodData(method)])
107120
)
108121
);
122+
const priorizedRegistryHints = {
123+
...registryHints,
124+
// own module > other modules
125+
...Object.fromEntries(methods.map((method) => [method.name, camelTitle])),
126+
};
109127

110128
const refreshFunctions: Record<string, string> = Object.fromEntries(
111129
await Promise.all(
112130
methods.map(async (method) => [
113131
method.name,
114-
await toRefreshFunction(method),
132+
await toRefreshFunction(method, priorizedRegistryHints),
115133
])
116134
)
117135
);
@@ -213,12 +231,13 @@ export function extractSummaryDefault(description: string): string | undefined {
213231
}
214232

215233
export async function toRefreshFunction(
216-
method: RawApiDocsMethod
234+
method: RawApiDocsMethod,
235+
registryHints: Record<string, string> = {}
217236
): Promise<string> {
218237
const { name, signatures } = method;
219238
const signatureData = required(signatures.at(-1), 'method signature');
220239
const { examples } = signatureData;
221240

222241
const exampleCode = examples.join('\n');
223-
return await toRefreshableCode(name, exampleCode);
242+
return await toRefreshableCode(name, exampleCode, registryHints);
224243
}

scripts/shared/refreshable-code.ts

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,38 @@ import { formatTypescript } from '../shared/format';
22

33
export async function toRefreshableCode(
44
name: string,
5-
exampleCode: string
5+
exampleCode: string,
6+
moduleHints: Record<string, string> = {}
67
): Promise<string> {
7-
if (!/^\w*faker\w*\./im.test(exampleCode)) {
8-
// No recordable faker calls in examples
9-
return 'undefined';
10-
}
11-
128
const exampleLines = exampleCode
139
.replaceAll(/ ?\/\/.*$/gm, '') // Remove comments
1410
.replaceAll(/^import .*$/gm, '') // Remove imports
11+
.replaceAll(/\b(?<!\.)(\w+)\((faker\.)?fakerCore/g, (match, p1) => {
12+
// Access SMF via module registry if possible
13+
if (moduleHints[p1]) {
14+
return `fakerRegistry.${moduleHints[p1]}.${match}`;
15+
}
16+
17+
throw new Error(
18+
`Unable to find module hint for ${p1} in example code for ${name}`
19+
);
20+
})
1521
.replaceAll(
16-
// record results of faker calls
17-
/^(\w*faker\w*\..+(?:(?:.|\n..)*\n[^ ])?\)(?:\.\w+)?);?$/gim,
22+
// record results of relevant calls
23+
// Keep in sync with docs/.vitepress/components/api-docs/refreshable-code.vue
24+
/^((?<callBase>\w*faker\w*)\.(?<consumeToEOL>.+)(?<multiline>(?<consumeIndented>\n +.*)+(?<finalLine>\n[^ \n]+))?\)(?<nestedProperty>\.\w+)?);?$/gim,
1825
`try { result.push($1); } catch (error: unknown) { result.push(error instanceof Error ? error.name : 'Error'); }\n`
1926
);
2027

28+
if (!exampleLines.includes('try { result.push(')) {
29+
// No recordable calls in examples
30+
return 'undefined';
31+
}
32+
2133
const fullMethod = `async (): Promise<unknown[]> => {
2234
await enableFaker();
2335
const result: unknown[] = [];
36+
${/(?<!\.)fakerCore/.test(exampleCode) ? 'const fakerCore = faker.fakerCore;' : ''}
2437
2538
${exampleLines}
2639

src/definitions/definitions.ts

Lines changed: 45 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -29,29 +29,49 @@ export type LocaleEntry<TCategoryDefinition extends Record<string, unknown>> = {
2929
} & Record<string, unknown>; // Unsupported & custom entries
3030

3131
/**
32-
* The definitions as used by the translations/locales.
32+
* The internal types for the definitions.
3333
*/
34-
export type LocaleDefinition = {
35-
metadata?: MetadataDefinition;
36-
airline?: AirlineDefinition;
37-
animal?: AnimalDefinition;
38-
book?: BookDefinition;
39-
color?: ColorDefinition;
40-
commerce?: CommerceDefinition;
41-
company?: CompanyDefinition;
42-
database?: DatabaseDefinition;
43-
date?: DateDefinition;
44-
finance?: FinanceDefinition;
45-
food?: FoodDefinition;
46-
hacker?: HackerDefinition;
47-
internet?: InternetDefinition;
48-
location?: LocationDefinition;
49-
lorem?: LoremDefinition;
50-
music?: MusicDefinition;
51-
person?: PersonDefinition;
52-
phone_number?: PhoneNumberDefinition;
53-
science?: ScienceDefinition;
54-
system?: SystemDefinition;
55-
vehicle?: VehicleDefinition;
56-
word?: WordDefinition;
57-
} & Record<string, Record<string, unknown>>;
34+
type RawLocaleDefinition = {
35+
metadata: MetadataDefinition;
36+
airline: AirlineDefinition;
37+
animal: AnimalDefinition;
38+
book: BookDefinition;
39+
color: ColorDefinition;
40+
commerce: CommerceDefinition;
41+
company: CompanyDefinition;
42+
database: DatabaseDefinition;
43+
date: DateDefinition;
44+
finance: FinanceDefinition;
45+
food: FoodDefinition;
46+
hacker: HackerDefinition;
47+
internet: InternetDefinition;
48+
location: LocationDefinition;
49+
lorem: LoremDefinition;
50+
music: MusicDefinition;
51+
person: PersonDefinition;
52+
phone_number: PhoneNumberDefinition;
53+
science: ScienceDefinition;
54+
system: SystemDefinition;
55+
vehicle: VehicleDefinition;
56+
word: WordDefinition;
57+
};
58+
59+
/**
60+
* Helper type to undo `LocaleEntry<T>` wrapping.
61+
*/
62+
type DefinedLocaleEntry<T> = T extends LocaleEntry<infer U> ? U : never;
63+
64+
/**
65+
* Helper type containing the well known locale definitions as used by this library, assuming all values are present.
66+
*
67+
* This type is mainly used for `resolveLocaleData()` to enable auto completion.
68+
*/
69+
export type DefinedLocaleDefinition = {
70+
[K in keyof RawLocaleDefinition]: DefinedLocaleEntry<RawLocaleDefinition[K]>;
71+
};
72+
73+
/**
74+
* The extensible definitions as used by the translations/locales.
75+
*/
76+
export type LocaleDefinition = Partial<RawLocaleDefinition> &
77+
Record<string, Record<string, unknown>>;

src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,9 +84,11 @@ export type { SystemModule } from './modules/system';
8484
export type { VehicleModule } from './modules/vehicle';
8585
export type { WordModule } from './modules/word';
8686
export type { Randomizer } from './randomizer';
87+
export { fakerRegistry } from './registry';
8788
export { SimpleFaker, simpleFaker } from './simple-faker';
8889
export { mergeLocales } from './utils/merge-locales';
8990
export {
9091
generateMersenne32Randomizer,
9192
generateMersenne53Randomizer,
9293
} from './utils/mersenne';
94+
export { resolveLocaleData } from './utils/resolve-locale-data';

src/registry.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import type { FakerCore } from '.';
2+
import { utilsModule as utils } from './utils/registry';
3+
4+
// TODO @ST-DDT 2026-04-12: This file will be auto generated in a future PR.
5+
6+
/**
7+
* Global Registry for the Faker library, containing all (SMF-aware) module registries.
8+
*/
9+
type FakerRegistry = Record<
10+
string,
11+
Record<string, (fakerCore: FakerCore, ...args: unknown[]) => unknown>
12+
>;
13+
14+
/**
15+
* Global Registry for the Faker library, containing all (SMF-aware) module registries.
16+
*/
17+
export const fakerRegistry = {
18+
utils,
19+
} satisfies FakerRegistry;

src/utils/registry.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { resolveLocaleData } from './resolve-locale-data';
2+
3+
// TODO @ST-DDT 2026-04-12: Add future SMF utils
4+
/**
5+
* Registry Module containing (SMF-aware) utility functions for the Faker library.
6+
*/
7+
export const utilsModule = {
8+
resolveLocaleData,
9+
};

src/utils/resolve-locale-data.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import type { FakerCore } from '../core';
2+
import type { DefinedLocaleDefinition } from '../definitions/definitions';
3+
import { assertLocaleData } from '../internal/locale-proxy';
4+
5+
/**
6+
* Returns the locale data for the given category and entry.
7+
*
8+
* If the category or entry is missing or explicitly absent, an error is thrown.
9+
*
10+
* @template TCategory The category to get the locale data from.
11+
* @template TEntry The entry to get the locale data from.
12+
*
13+
* @param core The core to get the locale data from.
14+
* @param category The category to get the locale data from.
15+
* @param entry The entry to get the locale data from.
16+
*
17+
* @example
18+
* resolveLocaleData(fakerCore, 'food', 'fruit'); // ['apple', 'apricot', ...]
19+
* faker.helpers.arrayElement(resolveLocaleData(fakerCore, 'food', 'fruit')); // 'cherry'
20+
*
21+
* @since 10.5.0
22+
*/
23+
export function resolveLocaleData<
24+
const TCategory extends keyof DefinedLocaleDefinition,
25+
const TEntry extends keyof DefinedLocaleDefinition[TCategory],
26+
>(
27+
core: FakerCore,
28+
category: TCategory,
29+
entry: TEntry
30+
): DefinedLocaleDefinition[TCategory][TEntry];
31+
/**
32+
* Returns the locale data for the given category and entry.
33+
*
34+
* If the category or entry is missing or explicitly absent, an error is thrown.
35+
*
36+
* @param core The core to get the locale data from.
37+
* @param category The category to get the locale data from.
38+
* @param entry The entry to get the locale data from.
39+
*
40+
* @example
41+
* resolveLocaleData(fakerCore, 'food', 'fruit'); // ['apple', 'apricot', ...]
42+
* faker.helpers.arrayElement(resolveLocaleData(fakerCore, 'food', 'fruit')); // 'cherry'
43+
*
44+
* @since 10.5.0
45+
*/
46+
export function resolveLocaleData(
47+
core: FakerCore,
48+
category: string,
49+
entry: string
50+
): unknown;
51+
export function resolveLocaleData(
52+
core: FakerCore,
53+
category: string,
54+
entry: string
55+
): unknown {
56+
const value = core.locale[category]?.[entry];
57+
assertLocaleData(value, category, entry);
58+
return value;
59+
}

test/scripts/apidocs/__snapshots__/page.spec.ts.snap

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,23 @@
11
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
22

3+
exports[`toRefreshFunction > should handle SMF calls 1`] = `
4+
"async (): Promise<unknown[]> => {
5+
await enableFaker();
6+
const result: unknown[] = [];
7+
const fakerCore = faker.fakerCore;
8+
9+
try {
10+
result.push(
11+
fakerRegistry.utils.resolveLocaleData(fakerCore, 'food', 'fruit')
12+
);
13+
} catch (error: unknown) {
14+
result.push(error instanceof Error ? error.name : 'Error');
15+
}
16+
17+
return result;
18+
}"
19+
`;
20+
321
exports[`toRefreshFunction > should handle multiline calls 1`] = `
422
"async (): Promise<unknown[]> => {
523
await enableFaker();

test/scripts/apidocs/__snapshots__/verify-jsdoc-tags.spec.ts.snap

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ exports[`check docs completeness > all modules and methods are present 1`] = `
3232
"generateMersenne32Randomizer",
3333
"generateMersenne53Randomizer",
3434
"mergeLocales",
35+
"resolveLocaleData",
3536
],
3637
],
3738
[

0 commit comments

Comments
 (0)