'Rollup & React- How to separate component bundles?

I currently am trying to build a UI Library for React and I am having a little bit of trouble. Currently I am using typescript and rollup, and I am able to bundle a single index.js and I am able to import those components but it is importing the whole library.

Currently:

File structure:

src
--components
-----button
-------button.tsx
-------button.types.ts
-----input
-------input.tsx
-------input.types.ts
-----index.ts
rollup.js

My rollup targets index.ts which has everything exported like so:

export { default as Button} from './button/button'
export { default as Input } from './input/input'

And I am able to import in a react project like so:

import { Button, Input } from 'my-library'

What I would Like to do I would like that each component is bundled separately and they would be imported like so

import { Input } from 'my-library/input'
import { Button } from 'my-library/button'

What I've Tried: After reading the docs, it seemed that the preserveModule: true is what I was looking for but then I tried importing as above but it started to complain that nothing was found.

My current rollup.js looks like this:

export default {
    input: 'src/index.ts',
    output: [
        {
            exports: 'named',
            dir: 'build/',
            format: 'esm',
            sourcemap: true,
            preserveModules: true,
        },
    ],
    plugins: [
        cleaner({ targets: ['./build'] }),
        peerDepsExternal(),

        resolve(),
        commonjs(),
        terser(),
        typescript({
            exclude: ['**/*.stories.tsx', '**/*.test.tsx'],
        }),
    ],
};


Solution 1:[1]

I tried using preserveModules but it doesnt generate an index.js file for each Components such that I can import like so :

import Button from 'lib/Button'

Hence I came up with a work around to make rollup loop through my src folders to generate a folder with entrypoint for every Component folder i had in src at rootDir

  1. Maintain a strict folder structure with entry point for every Component folder. Do not have loose files, other than index.ts in src folder that have no folders. Name your folders properly like how you want users to import it

src folder structure:

rollup.config.js
src
??? Accordion
?   ??? Accordion.tsx
?   ??? AccordionBody.tsx
?   ??? AccordionButton.tsx
?   ??? AccordionCollapse.tsx
?   ??? AccordionContext.ts
?   ??? AccordionHeader.tsx
?   ??? AccordionItem.tsx
?   ??? AccordionItemContext.ts
?   ??? index.ts
??? Alert
?   ??? Alert.tsx
?   ??? index.ts
??? Badge
?   ??? Badge.tsx
?   ??? index.ts
??? Breadcrumb
?   ??? Breadcrumb.tsx
?   ??? BreadcrumbItem.tsx
?   ??? index.ts
??? Button
?   ??? Button.tsx
?   ??? index.ts
??? ButtonGroup
?   ??? ButtonGroup.tsx
?   ??? index.ts
...
??? Tooltip
?   ??? Tooltip.tsx
?   ??? TooltipBox.tsx
?   ??? index.ts
??? index.ts

Its crucial for this case to maintain an entry point for each Component folder. I still maintained an entry point for src folder so that users can still import multiple components from the library with one line i.e. import {Button, Accordion, ...} from 'lib'

  1. Rollup config
  • getFolders returns an array of Folder names that are meant for export

  • loop through getFolders to generate the rollup obj per folder.

  • For typescript projects, rollup outputs the typings file with preserved folder structure already, so I realised that the folders Accordion, Button etc. were already there with typings file only. Now we need to add the index.js file to it!

import peerDepsExternal from 'rollup-plugin-peer-deps-external';
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import typescript from 'rollup-plugin-typescript2';
import replace from '@rollup/plugin-replace';
import { terser } from 'rollup-plugin-terser';
const packageJson = require('./package.json');
import { getFolders } from './scripts/buildUtils';

const plugins =  [
    peerDepsExternal(),
    resolve(),
    commonjs(),
    typescript({
      tsconfig: './tsconfig.json',
      useTsconfigDeclarationDir: true,
    }),
    terser()
  ]

const getFolders = (entry) => {
   // get the names of folders and files of the entry directory
   const dirs = fs.readdirSync(entry)
   // do not include folders not meant for export and do not process index.ts
   const dirsWithoutIndex = dirs.filter(name => name !== 'index.ts').filter(name => name !== 'utils')
   // ['Accordion', 'Button'...]
   return dirsWithoutIndex
}

//loop through your folders and generate a rollup obj per folder
const folderBuilds = getFolders('./src').map(folder=> {
  return {
    input: `src/${folder}/index.ts`, 
    output: {
      // ensure file destination is same as where the typings are
      file: `dist/${folder}/index.js`,
      sourcemap: true,
      exports: 'named',
    },
    plugins,
    external: ['react', 'react-dom'],
  }
})


export default [
  {
    input: ['src/index.ts'],
    output: [
      {
        file: packageJson.module,
        format: 'esm',
        sourcemap: true,
        exports: 'named',
      },
    ],
    plugins,
    external: ['react', 'react-dom'],
  },
  ...folderBuilds,
  {
    input: ['src/index.ts'],
    output: [
      {
        file: packageJson.main,
        format: 'cjs',
        sourcemap: true,
        exports: 'named',
      },
    ],
    plugins,
    external: ['react', 'react-dom'],
  },
];

  1. CJS file
  • finally i also added the rollup config to generate the cjs file. I did not bother to code split the cjs file since most users are using es6 imports
  1. "frank" build

Post build, I run a script to copy paste package.json, Readme to the ./dist folder

/* eslint-disable no-console */
const { resolve, join, basename } = require('path');
const { readFile, writeFile, copy } = require('fs-extra');

const packagePath = process.cwd();
const distPath = join(packagePath, './dist');

const writeJson = (targetPath, obj) =>
  writeFile(targetPath, JSON.stringify(obj, null, 2), 'utf8');

async function createPackageFile() {
  const packageData = await readFile(
    resolve(packagePath, './package.json'),
    'utf8'
  );
  const { scripts, devDependencies, ...packageOthers } =
    JSON.parse(packageData);
  const newPackageData = {
    ...packageOthers,
    private: false,
    typings: './index.d.ts',
    main: './main.js',
    module: './index.js',
  };

  const targetPath = resolve(distPath, './package.json');

  await writeJson(targetPath, newPackageData);
  console.log(`Created package.json in ${targetPath}`);
}

async function includeFileInBuild(file) {
  const sourcePath = resolve(packagePath, file);
  const targetPath = resolve(distPath, basename(file));
  await copy(sourcePath, targetPath);
  console.log(`Copied ${sourcePath} to ${targetPath}`);
}

async function run() {
  try {
    await createPackageFile();
    await includeFileInBuild('./README.md');
    // await includeFileInBuild('../../LICENSE');
  } catch (err) {
    console.error(err);
    process.exit(1);
  }
}

run();

  1. finally from root npm publish ./dist

This is how my dist folder looks like finally

dist
??? Accordion
?   ??? Accordion.d.ts
?   ??? AccordionBody.d.ts
?   ??? AccordionButton.d.ts
?   ??? AccordionCollapse.d.ts
?   ??? AccordionContext.d.ts
?   ??? AccordionHeader.d.ts
?   ??? AccordionItem.d.ts
?   ??? AccordionItemContext.d.ts
?   ??? index.d.ts
?   ??? index.js
?   ??? index.js.map
??? Alert
?   ??? Alert.d.ts
?   ??? index.d.ts
?   ??? index.js
?   ??? index.js.map
??? Badge
?   ??? Badge.d.ts
?   ??? index.d.ts
?   ??? index.js
?   ??? index.js.map
??? Breadcrumb
?   ??? Breadcrumb.d.ts
?   ??? BreadcrumbItem.d.ts
?   ??? index.d.ts
?   ??? index.js
?   ??? index.js.map
??? Button
?   ??? Button.d.ts
?   ??? index.d.ts
?   ??? index.js
?   ??? index.js.map
??? ButtonGroup
?   ??? ButtonGroup.d.ts
?   ??? index.d.ts
?   ??? index.js
?   ??? index.js.map
...
??? Tooltip
?   ??? Tooltip.d.ts
?   ??? TooltipBox.d.ts
?   ??? index.d.ts
?   ??? index.js
?   ??? index.js.map
??? index.d.ts
??? index.js
??? index.js.map
??? main.js
??? main.js.map
??? package.json

I got my solutions after much research from rollup issue thread on gh. Here are some references:

  1. Franking the build : https://stackoverflow.com/questions/62518396/importing-from-subfolders-for-a-javascript-package#:~:text=Votes-,13,-This%20is%20possible
  2. folder structuring : https://github.com/ezolenko/rollup-plugin-typescript2/issues/136#issuecomment-792383946
  3. inspiration for getFolders() that i wrote was from this author's getFiles() https://www.codefeetime.com/post/rollup-config-for-react-component-library-with-typescript-scss/

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 hairy25