Skip to content

feat: standalone module functions#3748

Draft
ST-DDT wants to merge 30 commits intonextfrom
feat/standalone-module-functions
Draft

feat: standalone module functions#3748
ST-DDT wants to merge 30 commits intonextfrom
feat/standalone-module-functions

Conversation

@ST-DDT
Copy link
Member

@ST-DDT ST-DDT commented Feb 28, 2026

Implements #2667


The goal of this PR is to convert our class/module based system to one, that uses standalone module functions and use them as the implementation of modules.

Non-goals of this PR:

  • create or expose nano bound module functions e.g. enFirstName (except for test purposes).

This PR will be created/achieve in the following steps:

  • Create FakerCore
  • Create transformation script (deleted before merge)
  • Apply transformation script (for early preview of code)
  • Fixup references to constants/types/enums, that would be to hard to implement via script
  • Run tree-shake tests faker.person.firstName() vs firstName(englishCore) vs firstName(nanoCore)
  • Create generate-module-tree script, that rebuilds the modules using the standalone functions
  • Update test suite, to use the standalone module function instead?
  • Update docs generation script to handle the new structure
  • Add faker vs standalone toggle to docs page for usage examples?

How to review:

  • Expect force pushes during the early stages of this PR (to keep the generated parts separate from manually written code/changes)
  • Review what the transform script does, not how it is implemented, as it gets deleted later on.
  • Revert the transform commit, apply the script and compare for deltas.
  • Check the transformed methods for issues (e.g. name conflict handling const vs imports)
  • Check the manual fixup commit for where stuff gets/should be moved
  • Run tests on your machine

@ST-DDT ST-DDT self-assigned this Feb 28, 2026
@ST-DDT ST-DDT added c: feature Request for new feature p: 1-normal Nothing urgent labels Feb 28, 2026
@netlify
Copy link

netlify bot commented Feb 28, 2026

Deploy Preview for fakerjs ready!

Name Link
🔨 Latest commit fe961f4
🔍 Latest deploy log https://app.netlify.com/projects/fakerjs/deploys/69b844b7dced7900074ed995
😎 Deploy Preview https://deploy-preview-3748.fakerjs.dev
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@ST-DDT
Copy link
Member Author

ST-DDT commented Feb 28, 2026

How should we distinguish between module functions and helper code for modules?

e.g. https://github.com/faker-js/faker/blob/next/src/modules/finance/bitcoin.ts

I considered prefixing the helper files with _.

So that:

  • src/modules/*/index.ts -> module index
  • src/modules/*/[a-z-]+.ts -> api
  • src/modules/*/_[a-z-]+.ts -> helpers

As an alternative, we could add an @api tag to the jsdocs.

What do you think?

@ST-DDT ST-DDT changed the title Standalone module functions feat: standalone module functions Feb 28, 2026
@ST-DDT ST-DDT force-pushed the feat/standalone-module-functions branch from 63d3440 to b1c0f00 Compare March 3, 2026 22:07
@ST-DDT
Copy link
Member Author

ST-DDT commented Mar 5, 2026

Which variant do you prefer?

A)

resolveLocaleData(fakerCore, 'commerce', 'product_name')

or B)

assertLocaleData(fakerCore.definitions.commerce?.product_name, 'commerce.product_name')

A is shorter, but you can click on B to navigate to the source definitions. Also auto completion works better with B.

@xDivisionByZerox
Copy link
Member

Which variant do you prefer?

A)

resolveLocaleData(fakerCore, 'commerce', 'product_name')

or B)

assertLocaleData(fakerCore.definitions.commerce?.product_name, 'commerce.product_name')

A is shorter, but you can click on B to navigate to the source definitions. Also auto completion works better with B.

I need some context here:

  • What would be the signature of both functions?
  • Why is the string required in version B?
  • Are both functions able to resolve entries that are nested more deeply than the standard module/entry way? (think person.firstname.female)

Also I have the strong feeling that the standalone module function PRs would get more traction from the core team if they could be done in smaller chunks. Like maybe start with one module and let everyone experience of it works. Since modules have to stay anyway, there would be no harm in simply adding one module at the time. We could even release them as "developer preview" to gather intel on how they "feel" in every day coding.

Also, I'm aware that the "transform-once" script exists, but:

  • its currently not reliable (see failing tests)
  • its hard to understand what is actually happening
  • I'll check the generated files anyway to check if they align with the expected code style

I'm not saying it doesn't work! Throwing it at the entire code base at once might be a bit overwhelming I'd say.
WE did the same thing for the locale data normalization for example.

@codecov
Copy link

codecov bot commented Mar 6, 2026

Codecov Report

❌ Patch coverage is 98.68871% with 23 lines in your changes missing coverage. Please review.
✅ Project coverage is 97.29%. Comparing base (e02536e) to head (fe961f4).

Files with missing lines Patch % Lines
src/modules/helpers/from-reg-exp.ts 94.21% 5 Missing and 2 partials ⚠️
src/modules/helpers/replace-credit-card-symbols.ts 88.00% 6 Missing ⚠️
src/modules/helpers/_eval.ts 95.71% 3 Missing ⚠️
src/faker-core.ts 75.00% 1 Missing and 1 partial ⚠️
src/modules/person/_select-definition.ts 84.61% 0 Missing and 2 partials ⚠️
src/modules/commerce/isbn.ts 96.29% 1 Missing ⚠️
src/modules/commerce/upc.ts 95.45% 1 Missing ⚠️
src/modules/helpers/weighted-array-element.ts 92.30% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             next    #3748      +/-   ##
==========================================
- Coverage   98.88%   97.29%   -1.60%     
==========================================
  Files         886     1184     +298     
  Lines        3062     3806     +744     
  Branches      556      676     +120     
==========================================
+ Hits         3028     3703     +675     
- Misses         30       98      +68     
- Partials        4        5       +1     
Files with missing lines Coverage Δ
src/internal/faker-to-core.ts 100.00% <100.00%> (ø)
src/internal/locale-proxy.ts 100.00% <100.00%> (ø)
src/module-registry.ts 100.00% <100.00%> (ø)
src/modules/airline/aircraft-type.ts 100.00% <100.00%> (ø)
src/modules/airline/airline.ts 100.00% <100.00%> (ø)
src/modules/airline/airplane.ts 100.00% <100.00%> (ø)
src/modules/airline/airport.ts 100.00% <100.00%> (ø)
src/modules/airline/flight-number.ts 100.00% <100.00%> (ø)
src/modules/airline/index.ts 100.00% <100.00%> (ø)
src/modules/airline/record-locator.ts 100.00% <100.00%> (ø)
... and 276 more

... and 49 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@ST-DDT
Copy link
Member Author

ST-DDT commented Mar 6, 2026

I need some context here:

/**
* Checks that the value is not null or undefined and throws an error if it is.
*
* @param value The value to check.
* @param path The path to the locale data.
*/
export function assertLocaleData<T>(

/**
* Resolves the locale data for the given category and entry.
*
* @template TCategory The category of the locale data to resolve.
* @template TEntry The entry of the locale data to resolve.
*
* @param fakerCore The FakerCore instance to get the locale data from.
* @param category The category of the locale data to resolve.
* @param entry The entry of the locale data to resolve.
*
* @returns The resolved locale data for the given category and entry.
*
* @throws {FakerError} If the category or entry is not defined in the locale data.
*
* @example
* arrayElements(fakerCore, resolveLocaleData(fakerCore, 'date', 'weekday')); // 'Sunday'
*
* @since 10.4.0
*/
export function resolveLocaleData<

It needs to the string to include it in the actual error message.

Are both functions able to resolve entries that are nested more deeply than the standard module/entry way? (think person.firstname.female)

Theoretically yes, but that's not what they are for.
The goal here is to extract the entries from the locale definitions and then pass them to the next process step usually arrayElement. Currently, all our locale data are structured that way.
Please note that definitions.person.first_name is the part that may be absent, everything below that we expect to follow the given type spec. We do not check whether only female is set and male is absent, we have a PersonEntryDefinition that we expect every locale to follow on that level.
If we users want to model their custom code differently, then they can do that either way.

The longer I think about this, the more I prefer B.


Also, I'm aware that the "transform-once" script exists, but its currently not reliable (see failing tests)

It was never meant to do all the work.

Also I have the strong feeling that the standalone module function PRs would get more traction from the core team if they could be done in smaller chunks. Like maybe start with one module and let everyone experience of it works.

#2667 (comment)
I would really love to see a working implementation that results in <<< 600 KiB like you proposed here, before actually rewriting the whole src code base.

This is kind of chicken and egg problem.
In order for showing that it works, I would like to show a real example e.g. the person module.
To implement that, I need the helpers module, that requires the number module, which requires some utils and the actual core.

Sounds simple, right?

The helper module contains fake. In the current implementation I mostly ignored how fake "knows" which methods exists.
We might have to add a module registry or something similar in the future.

Then there is this tiny detail called jsdocs verification tests, that I need to adjust as well.
I was able to dodge most of the api docs generation stuff for now.

Then there are naming conflicts with module methods e.g. lorem.word and word.word.

Should the module classes already start using the standalone methods?
This will only work if standalone fake works.

TLDR: Consider this a POC. I transformed the entire code base, to see which problems we need to handle.
This produces a working code base, that you can build youself locally and run any tests you would like to run with it.

For an in depth review, I can split this PR later into one per module, but we likely cannot do any releases once the first one lands until all of them are in (unless you don't make them public api).

FYI: Existing tests are passing now, but we need to consider how we change/duplicate them, to ensure both the standalone and the faker tree based ones have a desent coverage.

@ST-DDT
Copy link
Member Author

ST-DDT commented Mar 6, 2026

I did some tests with the current state of this PR to determine whether it results in smaller bundle sizes:

Just add the following exports to the faker's index.ts:

// Note this won't be the final export names
export { default as enPerson } from './locales/en/person';
export { avatar } from './modules/image/avatar';
export { urlLoremFlickr } from './modules/image/url-lorem-flickr';
export { firstName } from './modules/person/first-name';
export { lastName } from './modules/person/last-name';

Then I used the vue/vite-esm playground to test the bundle size.

FTF

<script setup lang="ts">
import { Faker, enPerson, FakerError } from "@faker-js/faker";
import { ref } from "vue";

const faker = new Faker({
  locale: { person: enPerson },
});

const fullName = ref(`${faker.person.firstName()} ${faker.person.lastName()}`);
const avatarUrl = ref(faker.image.avatar());
const natureImageUrl = ref(
  faker.image.urlLoremFlickr({ category: "nature" }) + new FakerError("test")
);
</script>

dist/assets/index-mO_qTbs-.js 182.21 kB │ gzip: 61.69 kB

grafik

SMF

<script setup lang="ts">
import { firstName, lastName, avatar, urlLoremFlickr, enPerson, FakerError, createFakerCore } from "@faker-js/faker";
import { ref } from "vue";

const fakerCore = createFakerCore({
  definitions: { person: enPerson },
});

const fullName = ref(`${firstName(fakerCore)} ${lastName(fakerCore)}`);
const avatarUrl = ref(avatar(fakerCore));
const natureImageUrl = ref(
  urlLoremFlickr(fakerCore, { category: "nature" }) + new FakerError("test")
);
</script>

dist/assets/index-GwB6agm0.js 136.59 kB │ gzip: 50.37 kB

grafik

Hard-Coded

<script setup lang="ts">
import { ref } from "vue";

const fullName = ref(`Firstname LastName`);
const avatarUrl = ref("https://cdn.jsdelivr.net/gh/faker-js/assets-person-portrait/female/512/75.jpg");
const natureImageUrl = ref(
  "https://loremflickr.com/2185/2550/nature?lock=7872953778781783Error:%20test"
);
</script>

dist/assets/index-DGlXtW4P.js 60.04 kB │ gzip: 24.07 kB

Result

The standalone module functions (SMF) take less space in the final bundle (-46 kB | gzip: -11 kB).
If you ignore vue and the other stuff, then its 30% smaller.
(And for whatever reason, we still ship extra parts of faker, maybe since definening a module class is a side effect?)

@ST-DDT
Copy link
Member Author

ST-DDT commented Mar 7, 2026

Interesting finding with createFakerCore:

return {
    definitions: Array.isArray(definitions)
      ? mergeLocales(definitions)
      : (definitions ?? {}),
    randomizer: randomizer ?? ({} as Randomizer),
    config: config ?? {},
  }

-> Some side effect, causes simpleFaker to be in the final bundle.

vs

return {
    definitions: Array.isArray(definitions)
      ? definitions[0]
      : (definitions ?? {}),
    randomizer: randomizer ?? ({} as Randomizer),
    config: config ?? {},
  }

-> Not included

mergeLocales doesn't even use/reference simpleFaker! (Same with generateMersenne53Randomizer)

🤔 WHY!?

@xDivisionByZerox
Copy link
Member

This is kind of chicken and egg problem.
In order for showing that it works, I would like to show a real example e.g. the person module.
To implement that, I need the helpers module, that requires the number module, which requires some utils and the actual core.

Sounds simple, right?

The helper module contains fake. In the current implementation I mostly ignored how fake "knows" which methods exists.
We might have to add a module registry or something similar in the future.

Then there is this tiny detail called jsdocs verification tests, that I need to adjust as well.
I was able to dodge most of the api docs generation stuff for now.

Then there are naming conflicts with module methods e.g. lorem.word and word.word.

Should the module classes already start using the standalone methods?
This will only work if standalone fake works.

TLDR: Consider this a POC. I transformed the entire code base, to see which problems we need to handle.
This produces a working code base, that you can build youself locally and run any tests you would like to run with it.

For an in depth review, I can split this PR later into one per module, but we likely cannot do any releases once the first one lands until all of them are in (unless you don't make them public api).

Thanks for the in depth explanation. I was already aware about the "person => helper => number" situation. That why I honestly had the number module in mind for this experiment. I was not aware of the faker.helper.fake situation.

@ST-DDT ST-DDT force-pushed the feat/standalone-module-functions branch 2 times, most recently from d802170 to 6fbd6d0 Compare March 8, 2026 20:45
@ST-DDT
Copy link
Member Author

ST-DDT commented Mar 8, 2026

I switched from resolveLocaleData to assertLocaleData.

@ST-DDT
Copy link
Member Author

ST-DDT commented Mar 16, 2026

I just noticed that my current implementation of a moduleRegistry for fake leads to a circular dependency.
The fake method references the global registry, , the global registry loads the methods via the module indexes, the helperEntries are defined in the index, but since the class is already initializing the helpers registry entry gets set to undefined and fake cannot call any helper methods directly. And if you execute the requests in a different order, the other modules are missing. I'm not exatly sure about my explanation, but I receive undefined modules in the registry.
I have to think about how to avoid that, maybe I will create a "registry.ts" file per module...

Depending on your point of view all methods that use fake experience a breaking change.

For now, I call all internal fake methods in the least breaking way, while still allowing for some level of tree shaking. Due to circular dependencies it is probably impossible to implememt in an entirely non-breaking way.

We might be able to relieve some of the pain points by switching from fake patterns to resolver functions internally. But that would be a breaking change and a separate PR for sure.

See also

I'll push the latest version once I get the fake issue resolved.

@ST-DDT ST-DDT force-pushed the feat/standalone-module-functions branch from 72e1d00 to c3b50eb Compare March 16, 2026 16:00
@ST-DDT ST-DDT force-pushed the feat/standalone-module-functions branch from c3b50eb to 61b4844 Compare March 16, 2026 17:32
@ST-DDT
Copy link
Member Author

ST-DDT commented Mar 16, 2026

It's working...

@Shinigami92
Copy link
Member

It's working...

fantastic 🚀

is it possible to clean this PR up and/or spliterate it into smaller PR-chunks? e.g. extract preparing chores which does not affect directly the implementation of this PR, but is required for it

e.g. I see some utilities like casing-utils (which I usually do by https://www.npmjs.com/package/change-case), also some file namings now with a prefixed underscore

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

c: feature Request for new feature p: 1-normal Nothing urgent

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants