Gediminas Bublys
Gediminas Bublys

Reputation: 463

How to propperly build react modular library

I'm trying to create a react components library which is based on Typescript and SASS. The components library will be used in multiple other typescript projects, so type exports are needed as well. Ideally I want to mimic something like "Material-UI"/"React-Bootrap" libraries dist output solutions.

Example project structure:

|Tabs
+--Tabs.tsx
+--Tabs.scss 
+--index.tsx
index.tsx

index.tsx

export { Tabs } from './Tabs/Tabs';

Tabs/index.tsx

import React from 'react';
import './Tabs.scss';

interface TabsProps {
    ...
}

export const Tabs: React.FC<TabsProps> = (props) => <div>...</div>

Tabs/index.tsx

export { Tabs } from './Tabs';

Expected built dist structure should mimic the src structure:

|Tabs
+--Tabs.js
+--Tabs.d.ts
+--index.js
+--index.d.ts
index.js
index.tsx

I tried analyzing open source projects and see how they are building the libraries, however I could not find libraries using the same approaches that I could reuse.

The solutions I've tried:

Webpack: While I could compile typescript and sass files the webpack would always emit only one file specified in the output section, which usually would be bundled and I would loose the ability to import single component from a specific component's module. I know I can specify multiple entry points, but the project will be having a lot of exports and manually specifying them is not an option...

Example config I tried:

const path = require('path');
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');

module.exports = {
    entry: './src/index.tsx',
    module: {
        rules: [
            // sass-loader is not used here yet, but should be once desired structure can be reached
            {
                test: /\.tsx?$/,
                loader: 'babel-loader',
            },
            // All output '.js' files will have any sourcemaps re-processed by 'source-map-loader'.
            { test: /\.js$/, loader: "source-map-loader" }
        ]
    },
    // Enable sourcemaps for debugging webpack's output.
    devtool: "source-map",
    resolve: {
        extensions: [".tsx", ".ts", ".js"],
        plugins: [
            new TsconfigPathsPlugin({ configFile: "./tsconfig.build.json" })
        ]
    },
    output: {
        filename: '[name].js',
        path: path.resolve(__dirname, 'dist')
    }
};

Rollup: Similar situation as webpack

Example config that I tried:

// rollup.config.js
import babel from 'rollup-plugin-babel';
import sass from 'rollup-plugin-sass';
import nodeResolve from 'rollup-plugin-node-resolve';
import commonjs from 'rollup-plugin-commonjs';

import react from 'react';
import reactDom from 'react-dom';

const babelOptions = {
    exclude: /node_modules/,
    // We are using @babel/plugin-transform-runtime
    runtimeHelpers: true,
    extensions: ['.js', '.ts', '.tsx'],
    configFile: './babel.config.js',
};

const nodeOptions = {
    extensions: ['.js', '.tsx', '.ts'],
};

const commonjsOptions = {
    ignoreGlobal: true,
    include: /node_modules/,
    namedExports: {
        react: Object.keys(react),
        'react-dom': Object.keys(reactDom)
    },
};

export default {
    input: 'src/index.tsx',
    output: {
        name: '[name].js',
        dir: 'dist',
        format: 'umd',
        sourcemap: true,
    },
    plugins: [
        nodeResolve(nodeOptions),
        sass(),
        commonjs(commonjsOptions),
        babel(babelOptions)
    ],
};

Babel: I managed to compile the typescript code however once I came close to transpiling SASS files I would end up with suggestions to use webpack for that...

TSC: I successfully could run the typescript compiler and it would compile all the files without problems and would maintain the same structure. However TSC does not support other transpiling options so after a lot of searches I would end up with suggestions to use webpack and "ts-loader" or "babel-loader"..

tsconfig:

{
  "extends": "../../tsconfig.build.json",

  "compilerOptions": {
    "lib": [ "es2015", "dom" ],
    "outDir": "dist",
    "baseUrl": ".",
    "declaration": true,
    "composite": true,
    "module": "commonjs",
    "target": "es5"
  },

  "include": [
    "src/**/*"
  ],
  "exclude": [
    "node_modules",
    "dist"
  ]
}

Desirced solution: I should be able after compiling the library and installing it in another project be able to run the following:

import { Tabs } from 'my-lib/Tabs';
import { Tabs } from 'my-lib';

Upvotes: 2

Views: 1644

Answers (2)

21paradox
21paradox

Reputation: 9

You can checkout this repo. I made some changes for building a lib.

https://github.com/21paradox/react-webpack-typescript-starter

To use a library like below:

import Input from 'my-custom-ui/entry/Input';
import { Input } from 'my-custom-ui';

After doing a lot of searching, I ended up writing a plugin to manually generate the webpack entry code that webpack needed (for building a ui library).

The multiple entry + manualy generated entry file seems to be working for component seperate & no redudant code. This is also very helpful if you want to build a vue based libray.

Upvotes: 0

Gediminas Bublys
Gediminas Bublys

Reputation: 463

After a lot of playing around I managed to produce the wanted result with rollup. The only downside of the current configuration is that it does not support newly added files in the --watch mode. The magic setting is under the output.preserveModules

Config:

// rollup.config.js
import commonjs from '@rollup/plugin-commonjs';
import typescript from '@rollup/plugin-typescript';
import postcss from 'rollup-plugin-postcss';
import postcssUrl from 'postcss-url';
import resolve from "@rollup/plugin-node-resolve";

import peerDepsExternal from "rollup-plugin-peer-deps-external";

export default {
    input: 'src/index.tsx',
    output: {
        dir: 'dist',
        format: 'es',
        preserveModules: true,
        sourcemap: true,
    },
    plugins: [
        resolve(),
        peerDepsExternal(),
        commonjs(),

        typescript({
            tsconfig: 'tsconfig.build.json'
        }),
        postcss({
            minimize: true,
            modules: {
                generateScopedName: "[hash:base64:5]"
            },
            plugins: [
                postcssUrl({
                    url: "inline"
                })
            ]
        }),

    ],
};

I hope this config can help others as well

Upvotes: 2

Related Questions