javascriptnode.jstypescriptwebpackrollup

Understanding the bundles, lib, lib-esm and iife folders


Some libraries/frameworks when build/prepare the application for publishing.

They generate a folder structure in the dist folder with the folders: bundles, lib, lib-esm or iife.

Here's the first question: What is the purpose of these folders?

Reading about it, I understood that the outputs are generated in this folder structure for greater compatibility of use. But I can't say with absolute certainty if this is really the case (It's impressive the number of posts/tutorials that say you must and how to generate these folders, but don't explain why).

Now, the second question: How does the project that imports this package know which folder structure to use? Example: How does a browser know that it has to use the bundles folder and not lib-esm? Or, how does Nodejs know that it has to use lib and not lib-esm?

Finally, thinking about using Typescript and debugging the package. The last question: When the package is published (production environment), should the type definition files and source maps be included in the output? If so, where should they be generated? In bundles? Lib? Lib-esm? In all folders?


Solution

  • Originally JavaScript did not have a module system. There were only scripts, not modules. Everything declared was on the global scope. In order to avoid leaking variables to the global scope, people write an iife (immediately invoked function expression). Typically this iife assigns something to the global scope. It looks something like this:

    (function() {
      /** Some example internal utility */
      function internalUtility() {}
    
      function publicApi() {
        internalUtility()
      }
    
      // Expose the public API.
      window.publicApi = publicApi
    })()
    

    Later people came up with different module systems: AMD and CJS (CommonJS). UMD is a format that is compatible with both AMD and CJS, and sometimes also assigns to the global scope. I’m not too familiar with the exact history, but the important thing to know is AMD, CJS, and UMD all exist to solve roughly the same problem.

    When Node.js was released, it supported CJS, and it still does.

    Later, a module system was added to JavaScript: ESM (ECMAScript modules). Most people love writing ESM, but Node.js didn’t support it. So people write ESM, but compiled it to CJS. This has led to many problems. ESM is asynchronous, but CJS is synchronous. ESM supports exporting multiple members, but CJS only has one (module.exports).

    Eventually Node.js caught up and added support for ESM. However, since people had been compiling ESM to CJS before there was consensus on interoperability, this lead to even more compatibility issues.

    What is the purpose of these folders?

    Nowadays people still love writing ESM, and some people still believe they need to support various module systems. This is the reason many projects ship all those files/folders. In reality you don’t really need to. It can even be hurtful and lead to the dual package hazard. You should typically publish either ESM or CJS. Most of the JavaScript ecosystem is slowly moving towards ESM.

    Also many tools emit incorrect TypeScript type definitions. Most people lack a deeper understanding of how exports work in TypeScript. I recommend reading all of the TypeScript modules documentation.

    How does the project that imports this package know which folder structure to use?

    When you import a package, Node.js knows which file to resolve based on the package.json "exports" field. There is also the "main". This existed before "exports", but it is less powerful. If neither "exports" nor "main" exists, Node.js will fallback to index.js, but this behaviour has been deprecated.

    In some projects you also see the "module" and "browser" fields. These are non-standard fields, but they are respected by some bundlers. You should not use them. If you want to publish a different entrypoint for browsers and Node.js, use an export condition.

    When the package is published (production environment), should the type definition files and source maps be included in the output? If so, where should they be generated? In bundles? Lib? Lib-esm? In all folders?

    TypeScript is made with the idea that you use tsc (the CLI that comes with the typescript package) to build your package. Let’s say you have a TypeScript file named module.ts, then TypeScript can generate the following files:

    A library should definitely publish the JavaScript output and type definitions. There isn’t really consensus of whether a package should also ship the source files, source maps, and declaration maps. Even within the TypeScript team people disagree.

    Your package.json should look something like this:

    {
      "name": "my-lib",
      "version": "1.0.0",
      "exports": "./dist/index.js",
      // Specify this field if you want to support TypeScript’s legacy "node10" module resolution algorithm.
      "main": "./dist/index.js",
      "files": [
        "dist",
        "src",
        "!test*"
      ],
      "scripts": {
        "prepack": "tsc --build",
        "pretest": "tsc --build",
        "test": "node --test"
      }
    }
    

    And your tsconfig.json should look something like this:

    {
      "compilerOptions": {
        "composite": true,
        "declaration": true,
        "declarationMap": true,
        "module": "node16",
        "outDir": "dist",
        "rootDir": "src",
        "sourceMap": true,
        "strict": true,
        "target": "es2021"
      }
    }