Skip to content

Commit 5d4f5de

Browse files
ST-DDTShinigami92xDivisionByZerox
authored
feat: add first standalone module functions (#3818)
Co-authored-by: Shinigami <chrissi92@hotmail.de> Co-authored-by: DivisionByZero <leyla.jaehnig@gmx.de>
1 parent 2b32d6e commit 5d4f5de

11 files changed

Lines changed: 314 additions & 34 deletions

File tree

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ function initRefresh(): Element[] {
2828
// Keep in sync with ref scripts/shared/refreshable-code.ts
2929
if (
3030
domLines[lineIndex]?.children.length === 0 ||
31-
!/^\w*faker\w*\.|^distributor\(/i.test(
31+
!/^\w*faker\w*\.|^\w+\(fakerCore|^distributor\(/i.test(
3232
domLines[lineIndex]?.textContent ?? ''
3333
)
3434
) {

scripts/apidocs/output/page.ts

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,18 +28,30 @@ editLink: false
2828
* @param pages The pages to write.
2929
*/
3030
export async function writePages(pages: RawApiDocsPage[]): Promise<void> {
31-
await Promise.all(pages.map(writePage));
31+
const registryHints: Record<string, string> = Object.fromEntries(
32+
pages.flatMap((page) =>
33+
page.methods.map((method) => [
34+
method.name,
35+
`${page.camelTitle}.${method.name}`,
36+
])
37+
)
38+
);
39+
await Promise.all(pages.map((page) => writePage(page, registryHints)));
3240
}
3341

3442
/**
3543
* Writes the api docs page and data for the given module to the correct location.
3644
*
3745
* @param page The page to write.
46+
* @param registryHints Hints for accessing standalone functions via module registry.
3847
*/
39-
async function writePage(page: RawApiDocsPage): Promise<void> {
48+
async function writePage(
49+
page: RawApiDocsPage,
50+
registryHints: Record<string, string> = {}
51+
): Promise<void> {
4052
try {
4153
await writePageMarkdown(page);
42-
await writePageData(page);
54+
await writePageData(page, registryHints);
4355
} catch (error) {
4456
throw new Error(`Error writing page ${page.title}`, { cause: error });
4557
}
@@ -103,20 +115,34 @@ async function writePageMarkdown(page: RawApiDocsPage): Promise<void> {
103115
* Writes the api docs data for the given module to correct location.
104116
*
105117
* @param page The page to write.
118+
* @param registryHints Hints for accessing standalone functions via module registry.
106119
*/
107-
async function writePageData(page: RawApiDocsPage): Promise<void> {
120+
async function writePageData(
121+
page: RawApiDocsPage,
122+
registryHints: Record<string, string> = {}
123+
): Promise<void> {
108124
const { camelTitle, methods } = page;
109125
const pageData: Record<string, ApiDocsMethod> = Object.fromEntries(
110126
await Promise.all(
111127
methods.map(async (method) => [method.name, await toMethodData(method)])
112128
)
113129
);
130+
const prioritizedRegistryHints = {
131+
...registryHints,
132+
// own module > other modules
133+
...Object.fromEntries(
134+
methods.map((method) => [method.name, `${camelTitle}.${method.name}`])
135+
),
136+
// utils always win
137+
getDefaultRefDate: 'defaultRefDate',
138+
setDefaultRefDate: 'setDefaultRefDate',
139+
};
114140

115141
const refreshFunctions: Record<string, string> = Object.fromEntries(
116142
await Promise.all(
117143
methods.map(async (method) => [
118144
method.name,
119-
await toRefreshFunction(method),
145+
await toRefreshFunction(method, prioritizedRegistryHints),
120146
])
121147
)
122148
);
@@ -218,12 +244,13 @@ export function extractSummaryDefault(description: string): string | undefined {
218244
}
219245

220246
export async function toRefreshFunction(
221-
method: RawApiDocsMethod
247+
method: RawApiDocsMethod,
248+
registryHints: Record<string, string> = {}
222249
): Promise<string> {
223250
const { name, signatures } = method;
224251
const signatureData = required(signatures.at(-1), 'method signature');
225252
const { examples } = signatureData;
226253

227254
const exampleCode = examples.join('\n');
228-
return await toRefreshableCode(name, exampleCode);
255+
return await toRefreshableCode(name, exampleCode, registryHints);
229256
}

scripts/shared/refreshable-code.ts

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,33 @@ 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> {
78
const exampleLines = exampleCode
89
.replaceAll(/ ?\/\/.*$/gm, '') // Remove comments
910
.replaceAll(/^import .*$/gm, '') // Remove imports
1011
.replaceAll(
11-
// record results of relevant calls
1212
// Keep in sync with docs/.vitepress/components/api-docs/refreshable-code.vue
13-
/^(\w*faker\w*\..+(?:(?:.|\n..)*\n[^ ])?\)(?:\.\w+)?|distributor\(.+\));?$/gim,
14-
`try { result.push($1); } catch (error: unknown) { result.push(error instanceof Error ? error.name : 'Error'); }\n`
13+
/\b(?<!\.)(\w+)\((faker\.)?fakerCore,?/g,
14+
(_, methodName) => {
15+
// We only have access to the main index imports, so we call the functions on the main faker object instead.
16+
// e.g.: firstName(fakerCore) -> faker.person.firstName()
17+
const hint = moduleHints[methodName];
18+
if (hint != null) {
19+
return `faker.${hint}(`;
20+
}
21+
22+
throw new Error(
23+
`Unable to find module hint for ${methodName} in example code for ${name}`
24+
);
25+
}
26+
)
27+
.replaceAll(
28+
// Record results of relevant calls
29+
// Keep in sync with docs/.vitepress/components/api-docs/refreshable-code.vue
30+
/^((?<callBase>\w*faker\w*\.|distributor\()(?<consumeToEOL>.+)(?<multiline>(?<consumeIndented>\n +.*)+(?<finalLine>\n[^ \n]+))?\)(?<nestedProperty>\.\w+)?);?$/gim,
31+
`try { result.push($1); } catch (error: unknown) { result.push(error instanceof Error ? error.name : 'Error'); console.log('Error in example for ${name}:', error); }\n`
1532
);
1633

1734
if (!exampleLines.includes('try { result.push(')) {
@@ -22,9 +39,12 @@ export async function toRefreshableCode(
2239
const fullMethod = `async (): Promise<unknown[]> => {
2340
await enableFaker();
2441
const result: unknown[] = [];
42+
${/(?<!\.)fakerCore/.test(exampleCode) ? 'const { fakerCore } = faker;' : ''}
2543
2644
${exampleLines}
2745
46+
${exampleCode.includes('setDefaultRefDate(') ? 'faker.setDefaultRefDate(); // Reset' : ''}
47+
2848
return result;
2949
}`;
3050
try {

src/core.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@ export interface FakerOptions {
6161
/**
6262
* Helper function to create a FakerCore instance.
6363
*
64+
* @remark The `config`, `randomizer` and (single) `locale` instances can be shared between multiple cores.
65+
* When shared, changing them will affect the other cores as well.
66+
*
6467
* @param options The options to create the FakerCore instance with.
6568
* @param options.locale The locale definitions to use.
6669
* 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.

src/simple-faker.ts

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ import { SimpleHelpersModule } from './modules/helpers';
77
import { SimpleLocationModule } from './modules/location';
88
import { NumberModule } from './modules/number';
99
import { StringModule } from './modules/string';
10-
11-
export const DEFAULT_REF_DATE_SOURCE: () => Date = () => new Date();
10+
import { getDefaultRefDate as utilsGetDefaultRefDate } from './utils/get-default-ref-date';
11+
import { setDefaultRefDate as utilsSetDefaultRefDate } from './utils/set-default-ref-date';
1212

1313
/**
1414
* This is a simplified Faker class that doesn't need any localized data to generate its output.
@@ -42,7 +42,7 @@ export class SimpleFaker {
4242
* Gets a new reference date used to generate relative dates.
4343
*/
4444
get defaultRefDate(): () => Date {
45-
return this.fakerCore.config.defaultRefDate ?? DEFAULT_REF_DATE_SOURCE;
45+
return () => utilsGetDefaultRefDate(this.fakerCore);
4646
}
4747

4848
/**
@@ -56,17 +56,17 @@ export class SimpleFaker {
5656
* @see faker.seed(): For generating reproducible values.
5757
*
5858
* @example
59-
* faker.seed(1234);
60-
*
61-
* // Default behavior
59+
* // Default
60+
* faker.seed(1234); // Keep `past()` offset consistent for example runs
6261
* // faker.setDefaultRefDate();
6362
* faker.date.past(); // Changes based on the current date/time
64-
*
65-
* // Use a static ref date
63+
* @example
64+
* // Fixed
65+
* faker.seed(1234);
6666
* faker.setDefaultRefDate(new Date('2020-01-01'));
6767
* faker.date.past(); // Reproducible '2019-07-03T08:27:58.118Z'
68-
*
69-
* // Use a ref date that changes every time it is used
68+
* @example
69+
* // Tick on use
7070
* let clock = new Date("2020-01-01").getTime();
7171
* faker.setDefaultRefDate(() => {
7272
* clock += 1000; // +1s
@@ -81,11 +81,7 @@ export class SimpleFaker {
8181
setDefaultRefDate(
8282
dateOrSource: string | Date | number | (() => Date) = () => new Date()
8383
): void {
84-
if (typeof dateOrSource === 'function') {
85-
this.fakerCore.config.defaultRefDate = dateOrSource;
86-
} else {
87-
this.fakerCore.config.defaultRefDate = () => new Date(dateOrSource);
88-
}
84+
utilsSetDefaultRefDate(this.fakerCore, dateOrSource);
8985
}
9086

9187
readonly datatype: DatatypeModule = new DatatypeModule(this);

src/utils/get-default-ref-date.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import type { FakerCore } from '../core';
2+
3+
const DEFAULT_REF_DATE_SOURCE: () => Date = () => new Date();
4+
5+
/**
6+
* Gets a new reference date used to generate relative dates.
7+
*
8+
* If `fakerCore.config.defaultRefDate` is defined, it will be used to get the default reference date. Otherwise, the current date will be used.
9+
*
10+
* @param fakerCore The FakerCore instance to get it from.
11+
*
12+
* @returns The newly created default reference date.
13+
*
14+
* @example
15+
* // Default
16+
* fakerCore.randomizer.seed(1234); // Keep `past()` offset consistent for example runs
17+
* // setDefaultRefDate(fakerCore);
18+
* past(fakerCore); // Changes based on the current date/time
19+
* @example
20+
* // Fixed
21+
* fakerCore.randomizer.seed(1234);
22+
* setDefaultRefDate(fakerCore, new Date('2020-01-01'));
23+
* past(fakerCore); // Reproducible '2019-07-03T08:27:58.118Z'
24+
* @example
25+
* // Tick on use
26+
* let clock = new Date("2020-01-01").getTime();
27+
* setDefaultRefDate(fakerCore, () => {
28+
* clock += 1000; // +1s
29+
* return new Date(clock);
30+
* });
31+
*
32+
* getDefaultRefDate(fakerCore) // 2020-01-01T00:00:01Z
33+
* getDefaultRefDate(fakerCore) // 2020-01-01T00:00:02Z
34+
*
35+
* @since 10.5.0
36+
*/
37+
export function getDefaultRefDate(fakerCore: FakerCore): Date {
38+
return (fakerCore.config.defaultRefDate ?? DEFAULT_REF_DATE_SOURCE)();
39+
}

src/utils/set-default-ref-date.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import type { FakerCore } from '../core';
2+
3+
/**
4+
* Sets the `refDate` source to use if no `refDate` date is passed to the date methods.
5+
*
6+
* @param fakerCore The FakerCore instance to use.
7+
* @param dateOrSource The function or the static value used to generate the `refDate` date instance.
8+
* The function must return a new valid `Date` instance for every call.
9+
* Defaults to `() => new Date()`.
10+
*
11+
* @see [Reproducible Results](https://fakerjs.dev/guide/usage.html#reproducible-results)
12+
* @see faker.seed(): For generating reproducible values.
13+
*
14+
* @example
15+
* // Default
16+
* fakerCore.randomizer.seed(1234); // Keep `past()` offset consistent for example runs
17+
* // setDefaultRefDate(fakerCore);
18+
* past(fakerCore); // Changes based on the current date/time
19+
* @example
20+
* // Fixed
21+
* fakerCore.randomizer.seed(1234);
22+
* setDefaultRefDate(fakerCore, new Date('2020-01-01'));
23+
* past(fakerCore); // Reproducible '2019-07-03T08:27:58.118Z'
24+
* @example
25+
* // Tick on use
26+
* let clock = new Date("2020-01-01").getTime();
27+
* setDefaultRefDate(fakerCore, () => {
28+
* clock += 1000; // +1s
29+
* return new Date(clock);
30+
* });
31+
*
32+
* getDefaultRefDate(fakerCore) // 2020-01-01T00:00:01Z
33+
* getDefaultRefDate(fakerCore) // 2020-01-01T00:00:02Z
34+
*
35+
* @since 10.5.0
36+
*/
37+
export function setDefaultRefDate(
38+
fakerCore: FakerCore,
39+
dateOrSource: string | Date | number | (() => Date) = () => new Date()
40+
): void {
41+
if (typeof dateOrSource === 'function') {
42+
fakerCore.config.defaultRefDate = dateOrSource;
43+
} else {
44+
fakerCore.config.defaultRefDate = () => new Date(dateOrSource);
45+
}
46+
}

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

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ exports[`toRefreshFunction > should handle multiline calls 1`] = `
1414
);
1515
} catch (error: unknown) {
1616
result.push(error instanceof Error ? error.name : 'Error');
17+
console.log('Error in example for test:', error);
1718
}
1819
1920
return result;
@@ -29,12 +30,14 @@ exports[`toRefreshFunction > should handle multiple calls 1`] = `
2930
result.push(faker.number.int());
3031
} catch (error: unknown) {
3132
result.push(error instanceof Error ? error.name : 'Error');
33+
console.log('Error in example for test:', error);
3234
}
3335
3436
try {
3537
result.push(faker.number.int());
3638
} catch (error: unknown) {
3739
result.push(error instanceof Error ? error.name : 'Error');
40+
console.log('Error in example for test:', error);
3841
}
3942
4043
return result;
@@ -50,6 +53,7 @@ exports[`toRefreshFunction > should handle properties after calls 1`] = `
5053
result.push(faker.airline.airport().name);
5154
} catch (error: unknown) {
5255
result.push(error instanceof Error ? error.name : 'Error');
56+
console.log('Error in example for test:', error);
5357
}
5458
5559
return result;
@@ -65,6 +69,7 @@ exports[`toRefreshFunction > should handle single line calls with semicolon 1`]
6569
result.push(faker.number.int());
6670
} catch (error: unknown) {
6771
result.push(error instanceof Error ? error.name : 'Error');
72+
console.log('Error in example for test:', error);
6873
}
6974
7075
return result;
@@ -80,6 +85,31 @@ exports[`toRefreshFunction > should handle single line calls without semicolon 1
8085
result.push(faker.number.int());
8186
} catch (error: unknown) {
8287
result.push(error instanceof Error ? error.name : 'Error');
88+
console.log('Error in example for test:', error);
89+
}
90+
91+
return result;
92+
}"
93+
`;
94+
95+
exports[`toRefreshFunction > should handle standalone functions calls 1`] = `
96+
"async (): Promise<unknown[]> => {
97+
await enableFaker();
98+
const result: unknown[] = [];
99+
const { fakerCore } = faker;
100+
101+
try {
102+
result.push(faker.defaultRefDate());
103+
} catch (error: unknown) {
104+
result.push(error instanceof Error ? error.name : 'Error');
105+
console.log('Error in example for test:', error);
106+
}
107+
108+
try {
109+
result.push(faker.date.past());
110+
} catch (error: unknown) {
111+
result.push(error instanceof Error ? error.name : 'Error');
112+
console.log('Error in example for test:', error);
83113
}
84114
85115
return result;

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,9 @@ exports[`check docs completeness > all modules and methods are present 1`] = `
3838
[
3939
"generateMersenne32Randomizer",
4040
"generateMersenne53Randomizer",
41+
"getDefaultRefDate",
4142
"mergeLocales",
43+
"setDefaultRefDate",
4244
],
4345
],
4446
[

0 commit comments

Comments
 (0)