reactjsnpmopen-source

A fool-proof tsup config for a React component library


I've never published an NPM package before. All these details to generate a package seem way too complicated to my level. The only tool, that was beginner friendly, that I could find is create-react-library which recommended to switch to tsup instead.

I'm asking here to know if there's a batteries-included, most-cases-met, setup for tsup or any other tool of your recommendation for this kind of project (and I think this is a common scenario):


Solution

  • Here is an example setup.

    // tsup.config.ts
    defineConfig([
      {
      clean: true,
      sourcemap: true,
      tsconfig: path.resolve(__dirname, "./tsconfig.build.json"),
      entry: ["./components/core/!(index).ts?(x)"],
      format: ["esm"],
      outDir: "dist/",
      esbuildOptions(options, context) {
        // the directory structure will be the same as the source
        options.outbase = "./";
        },
      },
    
    // index.ts
    // the actual file is "Button.tsx" but we still want a ".js" here
    export { Button } from "./components/core/Button.js";
    

    Notice the .js extension. ESM expects explicit extensions so it's needed in the final build.

    Adding the .js doesn't seem to bother TypeScript, which stills correctly recognize the type of "Button" from Button.tsx. At this point I am not sure why it works, but it does.

    Transpile this index, without bundling.

    // tsup.config.ts
      {
      clean: true,
      sourcemap: true,
      tsconfig: path.resolve(__dirname, "./tsconfig.build.json"),
      entry: ["index.ts", "./components/core/index.ts"],
      bundle: false,
      format: ["esm"],
      outDir: "dist",
      esbuildOptions(options, context) {
        options.outbase = "./";
        },
      },
    ])
    
      "sideEffects": false,
      "type": "module",
      "exports": {
        ".": "./dist/index.js"
      },
    

    sideEffects is a non-standard property targeting application bundlers like Webpack and Rollup. Setting it to false tells them that the package is safe for tree-shaking.

    Now import { Button } from "my-package" should work as you expect, and tree-shaking and dynamic loading at app-level become possible because "Button" is bundled as its own ES module Button.js, and the package is marked as being side-effect free.

    This is confirmed by my Webpack Bundle Analyzer in a Next app:

    Before (a single bundled index.js):

    enter image description here

    After (separate files means I can select more precisely my imports):

    enter image description here

    Final config available here (might be improved in the future)