faker-js / faker

Generate massive amounts of fake data in the browser and node.js
https://fakerjs.dev
Other
12.68k stars 911 forks source link

Export individual API modules on their own #1118

Open Cephyric-gh opened 2 years ago

Cephyric-gh commented 2 years ago

Clear and concise description of the problem

When using the package, even if you only ever needed 1 specific function you have to import the entire thing into your project in order to use it, which leaves it at the mercy of tree-shaking (assuming you're bundling the project that makes use of faker, which I'd assume is most use-cases).

Suggested solution

Instead of exporting everything under the to-level faker class, instead export each API module separately to allow only importing the things that are actually being used

// Current way to import, meaning we have to hope that the specific
// bundler being used (rollup, webpack, esbuild, etc) tree-shakes everything else away
import { faker } from '@faker-js/faker';

const noun = faker.word.noun();

vs

// Proposed solution, guarantees that at most the only thing that is
// bundled in is all the underlying functions for the "word" module
import { word } from '@faker-js/faker';

const noun = word.noun();

Alternative

The APIs could be exported alongside the main export, which would remov th breaking change

import { faker, word } from '@faker-js/faker';

Or the APIs could be exported in their own nested namespace (the same as locales are, though that may make locales funky)

import { faker } from '@faker-js/faker';
import { word } from '@faker-js/faker/word';
import { address } from '@faker-js/faker/locale/de/word';

Additional context

No response

ST-DDT commented 1 year ago

Team decision

We will implement this as part of our tree shaking.

Shinigami92 commented 1 year ago

@Cephyric-gh I would like to hear your feedback about if you would like / prefer this kind of import style:

import { faker, wordModule, loremModule, word, wordSample, loremWord } from '@faker-js/faker';

// These equals are theoretically and maybe not really the same instance, we will see
wordModule  === faker.word
loremModule === faker.lorem
word        === faker.lorem.word  === loremModule.word
wordSample  === faker.word.sample === wordModule.sample
loremWord   === faker.lorem.word  === loremModule.word === word 
Cephyric-gh commented 1 year ago

The module part aside (as I don't know the contextual difference between word and wordModule), that's definitely how I imagined it looking.

Do you have a concept of how this would work with the different language exports, i.e. if I wanted to use the Dutch translations are all the modules available in each translation, allowing imports to look something like this?

import { faker, wordModule, loremModule, word, wordSample, loremWord } from '@faker-js/faker/locale/de';
ST-DDT commented 1 year ago

Do you have a concept of how this would work with the different language exports

This is currently being discussed. Any input is welcome.

Here is one of our ideas (early draft, names and signatures are likely to change):

FFR: FakerCore = { mersenne, locale } without any modules or methods

// User code
import { word, fakerCoreDE } from '@faker-js/faker';

export const wordDE = word.bind(fakerCoreDE);
// Alternatively
export const wordDE = (...args) => word(...args, fakerCoreDE);

wordDE() // "Hello"

With that you could even do stuff like this:

const mersenne = mersenne()
export const fakerCoreDE = new FakerCore(de, mersenne);
export const fakerCoreEN = new FakerCore(en, mersenne);
// Both instances share their "progress" in the mersenne seed

Or even:

const fakerCoreDE_nouns = new FakerCore({
  word: {
    nouns: de.word.nouns
  }
});
export const nounsDE_tiny = nouns.bind(fakerCoreDE_nouns);
// So you don't have to package the entire German locale but only the parts that you actually need.

For now, we don't have plans to ship these tiny methods ourselves (~250 methods x ~60 locales is quite a lot). We will likely do that for only the default en ones (word === wordEN_tiny), but not for the other locales. We might do so later depending on user interest.

All of this needs to be tested whether it actually work with tree-shaking as expected.

Cephyric-gh commented 1 year ago

I skimmed through what the structure looks like in the next branch, and my personal take of your draft for locales would probably be to restrict locales to modules rather than the non-class structures, as that allows for the option of exposing every locale part in their own namespace which can be manually bound to the class, e.g. something like

import { WordModule } from '@faker-js/faker';
import { wordFormats } from '@faker-js/faker/locales/de';
// or
import { formats } from '@faker-js/faker/locales/de/word';

const wordDe = new WordModule(formats);
// or
const wordDe = new WordModule();
wordDe.registerLocale(format);

That'd allow for minimal inclusion in a project if only a specific part is needed (which would suit my specific use-case, as I only need the address portion for a project but need the en-GB locale), whilst also allowing the same architecture for users who are using the entirety of faker in a different language:

import { Faker } from '@faker-js/faker';
import { formats } from '@faker-js/faker/locales/de';

const fakerDe = new Faker(formats);
// or
const fakerDe = new Faker();
fakerDe.registerLocale(format);