How to separate a package into modules? (Like Firebase SDK)

Viewed 71

If you download Firebase JS SDK you only need to add one dependency: firebase

From there you can access all different modules based on your import attributes:

import { initializeApp } from 'firebase/app';
import { getFirestore, collection, getDocs } from 'firebase/firestore/lite';

And then any library that won't be used is automatically removed if you use any tree-shaking tool.

How can I achieve something similar? I'm looking to build a collection of packages for our organization so that, instead of having everyone working and updating 15 packages, having a single package that can be divided into modules/imports so a project only imports the necessary modules.

I have been looking for some days and I don't even know how to name this approach.

If anyone could link me to a tutorial, a document or anything to be able to start my investigation, I would appreciate it a lot.

Thank you.

3 Answers

If you check out the Firebase JS SDK repository, you can see that they use a monorepo to manage all of this, and they are actually all separate packages. They use Lerna to manage their monorepo - but there are several monorepo tools out there (npm workspaces, yarn). I like npm workspaces myself since it's native.

What they do is they make heavy use of exported modules in npm. You can see the documentation for that here: https://nodejs.org/api/packages.html#exports

So they have a repository that looks vaguely like this:

my-project
├── packages
│   ├── pkg-1
│   │   ├── package.json
│   │   └── index.js
│.  ├── pkg-2
│   │   ├── package.json
│   │   └── index.js
│   └── all
│.      ├── pkg-1
│       │   └── index.js
│.      ├── pkg-2
│       │   └── index.js
│       ├── package.json
│       └── index.js
└── package.json

The key part of the root package.json is the declaration of packages:

{
  ...
  packages: [
    "packages/*",
  ],
  ...
}

Sample package.json from a package:

{
  "name": "@my-project/pkg-1",
  "main": "index.js",
  ...
}

The trick here is where they have an extra package which just exports all of the other packages in the repo:

my-project/packages/all/package.json:

{
  "name": "my-project"
  "exports": {
    "./pkg-1": "./pkg-1/index.js",
    "./pkg-2": "./pkg-2/index.js",
  }
  "dependencies": [
    "@my-project/pkg-1": "*",
    "@my-project/pkg-2": "*",
  ],
  ...
}

example /my-project/packages/all/pkg-1/index.js:

export * from "@my-project/pkg-1"

Now when someone tries to install your package all, they will get all of the other packages aliased.

import * as pkg1 from "all/pkg-1"

They also bundle together ES modules and CommonJS so you can use "import" or "require". Take a long hard look at their package.json files in their repository, it's a work of art! https://github.com/firebase/firebase-js-sdk

TLDR; In your package.json add the files property. This will expose the directories that you specify. This will also make them accessible by path as you seek.

{
  "name": "mybase",
  "main": "index.js",
  "files": ["app/**", "store/**"]
}

With a config like above you would be able to access a file like this:

import { initializeApp } from 'mybase/app';
import { getFirestore, collection, getDocs } from 'mybase/store/lite';

Just make sure that the files exist as @KaKi87 mentioned. Specifying the files property in your package.json is key to exposing your file structure to client apps.

There's nothing particular to do : it's just natural.

For example, the following structure :

foo
├── bar
│   ├── baz
│   │   └── index.js
│   └── index.js
├── fum
│   ├── index.js
│   └── qux
│       └── index.js
├── index.js
└── package.json

Allows the following usage :

import foo from './foo';
import foobar from './foo/bar';
import foobarbaz from './foo/bar/baz';
import foofum from './foo/fum';
import foofumqux from './foo/fum/qux';

For testing purposes, consider each js file exports a string containing the concatenated value of all its parent folder names, e.g. ./foo/bar/baz/index.js contains export default 'foobarbaz';, while package.json contains { "type": "module" }.

Then, the following code :

console.log({
    foo,
    foobar,
    foobarbaz,
    foofum,
    foofumqux
});

Outputs :

{
  foo: 'foo',
  foobar: 'foobar',
  foobarbaz: 'foobarbaz',
  foofum: 'foofum',
  foofumqux: 'foofumqux'
}
Related