How to correctly export SVGs in a React component library with TypeScript

Viewed 6330

I am in the process of extracting the core components of our React application into a separate component library for use in other client applications. These components use SVG icons, which already works in the original application by using babel-loader.

However, since the components are written in Typescript, as far as I have understood it, I need ts-loader for the library to work properly, even though I could not test this, because I'm not even able to compile the libary. To be able to export SVGs at all, I additionally included babel-loader with the babel-plugin-named-asset-import.

This is my project structure:

.
├── config
│   └── webpack.config.js
├── global.d.ts
├── index.ts
├── package.json
├── package-lock.json
├── src
│   ├── assets
│   │   └── icons
│   │       ├── index.d.ts
│   │       ├── index.ts
│   │       └── upload.svg
│   ├── bar
│   │   └── index.ts
│   └── foo
│       ├── index.ts
│       └── Stuff.ts
└── tsconfig.json

webpack.config.json:

const path = require("path");

module.exports = {
  entry: "./index.ts",
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: {
          loader: "ts-loader",
        },
        exclude: /node_modules/,
      },
      {
        test: /\.(js|mjs|jsx|ts|tsx)$/,
        loader: require.resolve("babel-loader"),
        options: {
          customize: require.resolve(
            "babel-preset-react-app/webpack-overrides"
          ),

          plugins: [
            [
              require.resolve("babel-plugin-named-asset-import"),
              {
                loaderMap: {
                  svg: {
                    ReactComponent: "@svgr/webpack?-svgo,+ref![path]",
                  },
                },
              },
            ],
          ],
        },
      },
    ],
  },
  resolve: {
    extensions: [".tsx", ".ts", ".js"],
  },
};

tsconfig.json:

{
  "include": ["./global.d.ts", "./index.ts", "src/**/*"],

  "compilerOptions": {
    "allowJs": false,
    "jsx": "react",
    "esModuleInterop": true,
    "downlevelIteration": true,
    "declaration": true,
    "noEmit": false,
    "outDir": "dist",
    "moduleResolution": "node",
    "module": "commonjs",
    "target": "es5",
    "lib": ["es5", "es6", "es7", "es2017", "dom"],
    "sourceMap": true,
    "typeRoots": ["./node_modules/@types", "./@types"],
    "resolveJsonModule": true
  }
}

global.d.ts - I found out that the declare module part here has exactly no effect, I get the same result with or without it.

import 'jest-dom/extend-expect';

declare module '*.svg' {
  const content: string;
  export default content;
}

./src/assets/icons/index.ts should be responsible for exporting all the available icons for other files to import them.

export { ReactComponent as Add } from "./add.svg";

The ./src/demo/index.ts contains code that "uses" this SVG:

import { Add } from "../assets/icons";

export default {
  Add,
};

The index.ts in the root directory contains the same content to demonstrate the inconsistency of my problem:

import { Add } from "../assets/icons";

export default {
  Add,
};

So far so good, I thought, so I ran webpack with npx webpack --config ./config/webpack.config.js, only for the whole thing to fail with this error message:

ERROR in C:\work\tstest\src\demo\index.ts
[tsl] ERROR in C:\work\tstest\src\demo\index.ts(1,21)
      TS2306: File 'C:/work/tstest/src/assets/icons/index.ts' is not a module.

This leaves me completely confused with several questions:

  1. How is the file in question not a module? Would be great to know what's preventing ts-loader from recognizing it correctly
  2. ./src/demo/index.ts and ./index.ts are 100% identical in content. Still, compilation works for the one file, but not the other. Why is that and where is this behavior documented?
  3. What am I doing wrong?
  4. What is the correct way to pull this off?

I suppose this is happening because after babel-loader is done doing its thing, the ./src/assets/icons/index.ts does not look the way ts-loader wants it, but that doesn't quite explain how two files with the same content are processed differently.

1 Answers

This was a typical case of me not understanding the tooling used. The problem was in the tsconfig.json at:

"include": ["./global.d.ts", "./index.ts", "src/**/*"]

Which includes all the source files of the project when tsc is started. This resulted in tsc trying to compile all the source files at once whenever ts-loader was invoked by webpack, including those files that have not been processed yet by @svgr/webpack, leading to the error.

The solution was to only

"include": ["./global.d.ts", "src/**/*.d.ts"]

This way, webpack is responsible for compiling the individual source files included from the entry point according to its configured logic and tsc only seeing the definition files of those that have been processed by webpack.

Related