1

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?

3 Answers 3

1

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);

Sign up to request clarification or add additional context in comments.

Comments

1

I found create-react-library extremely helpful

1 Comment

One more tool, explained in this article
1

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.

3 Comments

Your solution should have worked, but I tried both options and their combination, but I am still getting the same error about not using an 'appropriate loader'.
And if you comment out the context and resolve property from your webpack config?
also, did you add both @babel/preset-typescript and @babel/preset-react to your babel configuration? Babel cannot transpile typescript without the typescript preset.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.