homam
homam

Reputation: 1975

Creating a React component library

I am creating a modular component library with React and TypeScript with Babel 7.

I want the user of my library to import the components by a syntax similar to this:

import SomeComponent from "my-awesome-lib/SomeComponent"

SomeComponent is a TSX module in my-awesome-lib package:

import * as React from "react";

export default function () {
  return <h1>SomeComponent</h1>
}

And main field of the package.json file of my-awesome-component is:

"main": "src/index.ts"

I do not want to publish compiled version of my component library, because I am importing CSS and other assets in my components and I expect all the users of my package to use Webpack with some specific configs.

Now my problem is that `import SomeComponent from "my-awesome-lib/SomeComponent" fails with a parse error:

ERROR in ../node_modules/wtf/index.tsx 4:9
Module parse failed: Unexpected token (4:9)
You may need an appropriate loader to handle this file type.
| 
| export default function () {
>   return <h1>WTF</h1>
| }

It seems Webpack does not load or transform the TSX files in node_modules.

I use this tsconifg.json at the root of my user app (which imports my-awesome-lib):

{
  "compilerOptions": {
    "outDir": "./dist",
    "module": "commonjs",
    "target": "es5",
    "jsx": "react",
    "allowSyntheticDefaultImports": true,
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "esModuleInterop": true,
    "downlevelIteration": true,
    "lib": ["es5", "es2015", "dom", "scripthost"], 
    "typeRoots": ["node_modules/@types", "src/@types"],
  },
  "include": ["src/**/*", "node_modules/**/*"],
  "exclude": []
}

And the relevant configurations of Webpack are:

const tsModules = {
  test: /\.(js|jsx|ts|tsx)$/,
  include: [path.resolve('src')],
  exclude: /node_modules/,
  loader: 'babel-loader'
}

const resolve = {
  alias: {
  },
  modules: [
    'node_modules'
  ],
  extensions: ['.tsx', '.ts', '.js']
}

module.exports = {
   ...
   context: resolve('src'),
   resolve: resolve,
   module: {
     rules: [
       tsModules,
       ...
     ]
   }
}

How can I make Webpack to load and transform TSX modules of my-awesome-lib from node_modules?

Upvotes: 1

Views: 4146

Answers (3)

axm__
axm__

Reputation: 2673

You are excluding your node_modules directory (which generally is a good thing):

const tsModules = {
  test: /\.(js|jsx|ts|tsx)$/,
  include: [path.resolve('src')],
  exclude: /node_modules/,
  loader: 'babel-loader'
}

Besides explicitely excluding the node_modules folder, you also only allow the content of the src folder to be processed by babel-loader because of your include property in tsModules. So this error:

ERROR in ../node_modules/wtf/index.tsx 4:9 Module parse failed: Unexpected token (4:9)

makes sense.

You can still exclude node_modules except for a single folder if you remove the tsModules.include property and change your regular expression in tsModules.exclude:

const tsModules = {
  // ...
  exclude: /node_modules\/(?!my-awesome-lib)\/*/
  // ...
}

Or you could, but I haven't tested it yet, add the my-awesome-lib dir to the include array:

const tsModules = {
  // ...
  include: [
    path.resolve(__dirname, './src'),
    path.resolve(__dirname, './node-modules/my-awesome-lib')
  ]
  // ...
}

Then files in your node_modules/my-awesome-lib directory will pass the babel-loader which will transform the typescript code.

Edit: I think you confusion is coming from your tsconfig.json file with "include": ["src/**/*", "node_modules/**/*"],. Babel is transpiling your code, not typescript. So having a tsconfig.json file in your root directory may help your IDE (especially if you are using Microsoft's VScode), but has no effect on how babel and @babel/preset-typescript transform your code.

Upvotes: 1

idan
idan

Reputation: 484

I found create-react-library extremely helpful

Upvotes: 1

Shanon Jackson
Shanon Jackson

Reputation: 6581

This setup assuming you are using styled-components and have no css/scss.

What's important here is that you have "module": commonJS in your tsconfig and libraryTarget: "commonJS" in your webpack config. Externals tells webpack not bundle you're library with React, or React-DOM or styled-components and instead to look for those packages within the project you're importing into.

you're also going to need to take React, react-dom and styled-components out of your package.json dependencies and put those package in your peer-dependencies

   const path = require("path");
    const fs = require("fs");
    const TerserPlugin = require('terser-webpack-plugin');
    const appIndex = path.join(__dirname, "../src/main.tsx");
    const appBuild = path.join(__dirname, "../storybook-static");
    const { TsConfigPathsPlugin } = require('awesome-typescript-loader');

    module.exports = {
        context: fs.realpathSync(process.cwd()),
        mode: "production",
        bail: true,
        devtool: false,
        entry: appIndex,
        output: {
            path: appBuild,
            filename: "dist/Components.bundle.js",
            publicPath: "/",
            libraryTarget: "commonjs"
        },
        externals: {
            react: {
                root: 'React',
                commonjs2: 'react',
                commonjs: 'react',
                amd: 'react'
            },
            'react-dom': {
                root: 'ReactDOM',
                commonjs2: 'react-dom',
                commonjs: 'react-dom',
                amd: 'react-dom'
            },
            "styled-components": {
                root: "styled-components",
                commonjs2: "styled-components",
                commonjs: "styled-components",
                amd: "styled-components"
            }
        },
        optimization: {
            minimizer: [
                new TerserPlugin({
                    terserOptions: {
                        parse: {
                            ecma: 8,
                        },
                        compress: {
                            ecma: 5,
                            warnings: false,
                            comparisons: false,
                            inline: 2,
                        },
                        mangle: {
                            safari10: true,
                        },
                        output: {
                            ecma: 5,
                            comments: false,
                            ascii_only: true,
                        },
                    },
                    parallel: true,
                    cache: true,
                    sourceMap: false,
                })
            ],
        },
        resolve: {
            extensions: [".web.js", ".mjs", ".js", ".json", ".web.jsx", ".jsx", ".ts", ".tsx"],
            alias: {
                "react-native": "react-native-web",
            },
        },
        module: {
            strictExportPresence: true,
            rules: [
                { parser: { requireEnsure: false } },
                {
                    test: /\.(ts|tsx)$/,
                    loader: require.resolve("tslint-loader"),
                    enforce: "pre",
                },
                {
                    oneOf: [
                        {
                            test: /\.(tsx?)$/,
                            loader: require.resolve('awesome-typescript-loader'),
                            options: {
                                configFileName: 'tsconfig.prod.json'
                            }
                        },
                    ],
                },
            ],
        },
        plugins: [
            new TsConfigPathsPlugin()
        ],
        node: {
            dgram: "empty",
            fs: "empty",
            net: "empty",
            tls: "empty",
            child_process: "empty",
        },
        performance: false,
    };

note: it's important that you target a point in you're application as an entry that ONLY has the components you want to export.

I.E For me it's Main.tsx and inside Main.tsx it looks like this.

export { Checkbox } from "./components/Checkbox/Checkbox";
export { ColorUtils } from "./utils/color/color";
export { DataTable } from "./components/DataTable/DataTable";
export { DatePicker } from "./components/DateTimePicker/DatePicker/DatePicker";
export { DateTimePicker } from "./components/DateTimePicker/DateTimePicker/DateTimePicker";
export { Disclosure } from "./components/Disclosure/Disclosure";

This means webpack won't bundle things you're not meaning to export. To test you're bundle works try importing something with require syntax from the bundle to get around typescript typings and turn allowJS true in tsconfig.

something like const Button = require("../path/to/js/bundle").Button console.log(Button);

Upvotes: 1

Related Questions