Search code examples
npmwebpackreact-typescriptcss-modules

How to set up an npm library with TSX components and CSS modules?


The goal is to create an npm module with React components and functions that can be imported in a NextJS project. The problem is that the JS is bundled in one file instead of maintaining the src/ folder structure, and when the module is imported the functions are not working (undefined).

The folder structure is like this:

- src
  index.ts
  - components
    testComponent.module.css
    testComponent.tsx
    testReturn.ts

The src/index.ts exports everything from components/. In the module I have these files:

tsconfig.json

{
  "compilerOptions": {
    "jsx": "react-jsx",
    "target": "ESNext",
    "module": "CommonJS",
    "outDir": "dist",
    "sourceMap": true,
    "strict": true,
    "declaration": true,
    "esModuleInterop": true,
    "noEmit": false,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "lib": ["ESNext", "DOM", "DOM.Iterable"],
    "moduleResolution": "node",
    "plugins": [{ "name": "typescript-plugin-css-modules" }],
    "baseUrl": "./src"
  },
  "include": ["src/**/*"]
}

webpack.config.js

const path = require("path");

module.exports = {
  mode: process.env.NODE_ENV || "development",
  entry: {
    index: "./src/index.ts",
  },
  output: {
    path: path.resolve(__dirname, "dist"),
  },
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        exclude: /node_modules/,
        loader: "ts-loader",
        options: {
          configFile: "tsconfig.json",
        },
      },
      {
        test: /\.css$/i,
        use: [
          "style-loader",
          {
            loader: "css-loader",
            options: {
              modules: {
                localIdentName: "[name]__[local]__[hash:base64:5]",
              },
            },
          },
        ],
      },
    ],
  },
  resolve: {
    extensions: [".tsx", ".ts", ".js"],
  }

In package.json I have these settings:

{
  "name": "testmodule",
  "scripts": {
    "prebuild": "rimraf dist",
    "build": "webpack"
  },
  "main": "dist/index.js",
  "files": [
    "dist"
  ],
  "dependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0"
  },
  "devDependencies": {
    "@types/react": "^18.2.22",
    "@types/react-dom": "^18.2.7",
    "@types/css-modules": "^1.0.3",
    "ts-loader": "^9.4.4",
    "typescript": "^5.2.2",
    "webpack-cli": "^5.1.4",
    "rimraf": "^5.0.4",
    "css-loader": "^6.8.1",
    "style-loader": "^3.3.3",
    "typescript-plugin-css-modules": "^5.0.1",
    "webpack": "^5.88.2"
  }
}

Now I seem to have 2 issues:

  1. All JS is bundled in 1 file in dist/index.js while I want it to maintain the src/ folder structure. The folder structure exists, but only d.ts files are in there.
  2. When I try to import this library (I use yarn link to locally link the library) the functions do import without errors in VS Code but in the dev server (a clean NextJS setup) they give an error that they are undefined.

Can anyone shed some light on this? Maybe Rollup is more suited for this?


Solution

  • After hours of trying to get Webpack do the job I found out that rollup just did it with just some small configuration:

    import commonjs from "@rollup/plugin-commonjs";
    import nodeResolve from "@rollup/plugin-node-resolve";
    import postcss from "rollup-plugin-postcss";
    import tsConfigPaths from "rollup-plugin-tsconfig-paths";
    import typescript from "rollup-plugin-typescript2";
    import pkg from "./package.json";
    
    const config = [
      {
        input: "src/index.ts",
        output: {
          preserveModules: true,
          preserveModulesRoot: "src",
          dir: "./dist",
          format: "es",
        },
        external: [...Object.keys(pkg.dependencies || {}), "react/jsx-runtime"],
        plugins: [
          tsConfigPaths(),
          nodeResolve({ extensions: [".tsx", ".ts", ".jsx", ".js", ".json"] }),
          typescript({
            typescript: require("typescript"),
          }),
          commonjs(),
          postcss(),
        ],
      },
    ];
    export default config;
    

    Maybe Webpack is up to it to, but this worked for me.