Gatsby: Storybook v5, TypeScript and SVG loader

Created on 13 Mar 2019  路  12Comments  路  Source: gatsbyjs/gatsby

Description

I have been struggling to get Storybook v5, Gatsby working with SVG loader and TS to work nicely together.

Steps to reproduce

Install the gatsby-plugin-svgr plugin.
Import an SVG into a .tsx file. Gatsby should load this fine.
Run Storybook.

Expected result

Storybook should run and import the SVG.

Actual result

Storybook does not load. The error is:

[tsl] ERROR in ...../src/components/Header/Header.tsx(3,30)
      TS2307: Cannot find module './header-background.svg'.
Child HtmlWebpackCompiler:
                          Asset     Size               Chunks  Chunk Names
    __child-HtmlWebpackPlugin_0  559 KiB  HtmlWebpackPlugin_0  HtmlWebpackPlugin_0
    Entrypoint HtmlWebpackPlugin_0 = __child-HtmlWebpackPlugin_0
    [./node_modules/@storybook/core/node_modules/html-webpack-plugin/lib/loader.js!./node_modules/@storybook/core/dist/server/templates/index.ejs] 1.69 KiB {HtmlWebpackPlugin_0} [built]
    [./node_modules/@storybook/core/node_modules/webpack/buildin/global.js] (webpack)/buildin/global.js 472 bytes {HtmlWebpackPlugin_0} [built]
    [./node_modules/@storybook/core/node_modules/webpack/buildin/module.js] (webpack)/buildin/module.js 497 bytes {HtmlWebpackPlugin_0} [built]
    [./node_modules/lodash/lodash.js] 527 KiB {HtmlWebpackPlugin_0} [built]

Environment

  System:
    OS: macOS 10.14.3
    CPU: (8) x64 Intel(R) Core(TM) i7-4870HQ CPU @ 2.50GHz
    Shell: 3.2.57 - /bin/bash
  Binaries:
    Node: 10.1.0 - ~/.nvm/versions/node/v10.1.0/bin/node
    Yarn: 1.13.0 - ~/.nvm/versions/node/v10.1.0/bin/yarn
    npm: 6.4.1 - ~/.nvm/versions/node/v10.1.0/bin/npm
  Languages:
    Python: 2.7.10 - /usr/local/bin/python
  Browsers:
    Chrome: 72.0.3626.121
    Firefox: 64.0.2
    Safari: 12.0.3
  npmPackages:
    gatsby: ^2.1.30 => 2.1.30 
    gatsby-link: ^2.0.15 => 2.0.15 
    gatsby-plugin-emotion: ^4.0.6 => 4.0.6 
    gatsby-plugin-glamor: ^2.0.9 => 2.0.9 
    gatsby-plugin-google-analytics: ^2.0.17 => 2.0.17 
    gatsby-plugin-manifest: 2.0.23 => 2.0.23 
    gatsby-plugin-offline: ^2.0.25 => 2.0.25 
    gatsby-plugin-sharp: ^2.0.27 => 2.0.27 
    gatsby-plugin-svgr: ^2.0.1 => 2.0.1 
    gatsby-plugin-typescript: ^2.0.0-rc.4 => 2.0.11 
    gatsby-remark-autolink-headers: ^2.0.16 => 2.0.16 
    gatsby-remark-copy-linked-files: ^2.0.10 => 2.0.10 
    gatsby-remark-images: ^3.0.9 => 3.0.9 
    gatsby-remark-prismjs: ^3.2.5 => 3.2.5 
    gatsby-source-filesystem: ^2.0.24 => 2.0.24 
    gatsby-transformer-json: ^2.1.10 => 2.1.10 
    gatsby-transformer-remark: ^2.3.2 => 2.3.2 
    gatsby-transformer-sharp: ^2.1.16 => 2.1.16 
  npmGlobalPackages:
    gatsby-cli: 2.4.15
awaiting author response

Most helpful comment

@elie222 looks like it worked. I'm going to break down the answer into smaller bits so that you can make adjustments as you go along.
Going to start off with the typescript part and then move onto the storybook part.

  • When i was working on your issue, one thing kept bugging me while i was trying to make the reproduction work and it's that when you import a svg file into any component a squigly line keeps popping up below, saying it can't resolve it. And i was seeing it in your code aswell.
    So, to make typescript and svg "play nice", in the root of the project i've created a file called custom.d.ts with the following code:
declare module "*.svg" {
    const content: any;
    export default content;
}
  • Modified the tsconfig.json to the following:
{
    "compilerOptions": {
        "outDir": "./dist/",
        "sourceMap": true,
        "noImplicitAny": true,
        "module": "commonjs",
        "target": "esnext",
        "jsx": "react",
        "lib": ["dom", "esnext"]
    },
    "include": [
        "./src/**/*"
    ],
    "files": [
        "custom.d.ts"
    ]
}

No more issues with svg and typescript inside the components.

  • I saw that you have copied over the image files and svg files to the static folder and have some of them replicated inside the some of the project's components folders.
    I understood your reasoning, when you issue start-storybook -p 9001 -c .storybook -s ./static you're expecting that based on the arguments you supplied it will pick up the contents of the static folder and will automatically serve them. Based on your current setup that's not the case, because the exact folder structure will be served as is. And inside a component, for instance ArticleItem.tsx you have the following import import favorite from "./favorite.svg and with that the contents weren't being shown for me. What i did was was move all of the svg files to src/assets/icons and the images to src/assets/images. and updated the mentioned import to import Favorite from "../../assets/icons/favorite.svg";. And you'll understand why later in the comment.
    Also in that "department"(pardon the bad pun), i saw that you're making the folder available to graphql and if you absolutely need it, fine by me, just ignore this part of the comment. But as a good practice with gatsby, items inside the folder are already available to you directly, so you shouldn't need to add a extra layer to your code. More on that here.

Onto the question at hand, to make storybook and gatsby "play nice".

  • The first thing i've done while looking at your code, was to remove @storybook/react": "^5.1.0-alpha.10 and revert it back to @storybook/react": "^5.0.3, reason why, basically it's an alpha and i'm seeing some chatter that it's still a bit brittle.
  • Then @coreyward's comment made me take a step back and think about the webpack config. And he was right, i know for sure that storybook has support for svgs out of the box. But this, this is a special case, you're treating a svg like a React component. And the @svgr/webpack loader was not being added.
  • With that in mind i made a few tweaks to the webpack.config.js file for storybook. Transforming the code into:
 const path= require('path');
 const pathToInlineSvg = path.resolve(__dirname, '../src/assets/icons/');

 module.exports = ({ config }) => {
     // Transpile Gatsby module because Gatsby includes un-transpiled ES6 code.
     config.module.rules[0].exclude = [/node_modules\/(?!(gatsby)\/)/]

     // use installed babel-loader which is v8.0-beta (which is meant to work with @babel/core@7)
     config.module.rules[0].use[0].loader = require.resolve("babel-loader")

     // use @babel/preset-react for JSX and env (instead of staged presets)
     config.module.rules[0].use[0].options.presets = [
       require.resolve("@babel/preset-react"),
       require.resolve("@babel/preset-env"),
     ]

     // use @babel/plugin-proposal-class-properties for class arrow functions
     config.module.rules[0].use[0].options.plugins = [
       require.resolve("@babel/plugin-proposal-class-properties"),
     ]

     // Prefer Gatsby ES6 entrypoint (module) over commonjs (main) entrypoint
     config.resolve.mainFields = ["browser", "module", "main"]
     config.module.rules.push({
         test: /\.(ts|tsx)$/,
         loader: require.resolve('babel-loader'),
         options: {
           presets: [['react-app', { flow: false, typescript: true }]],
         },
     });
     config.resolve.extensions.push('.ts', '.tsx');
     // svg with @svgr
     const fileLoaderRule = config.module.rules.find(rule => rule.test.test('.svg'));
     fileLoaderRule.exclude = pathToInlineSvg;
     config.module.rules.push({
       test: /\.svg$/,
       include: pathToInlineSvg,
       use: [{
         loader: '@svgr/webpack',
         options: {
           icon: true,
         },
       }],
     });
     //
     return config
   }

Focusing on the important parts of the code here:

  • The following line, const pathToInlineSvg = path.resolve(__dirname, '../src/assets/icons/');, like i said above, as i moved all the svg files inside /src/assets/icons/, so this const will hold an array with all of them.
  • The following bit:
 const fileLoaderRule = config.module.rules.find(rule => rule.test.test('.svg'));
     fileLoaderRule.exclude = pathToInlineSvg;
     config.module.rules.push({
       test: /\.svg$/,
       include: pathToInlineSvg,
       use: [{
         loader: '@svgr/webpack',
         options: {
           icon: true,
         },
       }],
     });

When it reaches here, it will exclude the out of the box svg support for the svgs in the above array and adds a new rule for loading svgs, but this time with @svgr/webpack.

  • All that is missing is change the components accordingly. For instance your component ArticleItem.tsx is modified to the following, i left out the majority of the code to focus on the important parts:
// import favorite from "./favorite.svg"
import Favorite from "../../assets/icons/favorite.svg";

export default (props: ArticleItemProps) => {
  return (
    <Wrapper href={props.link} target="_blank">
       ......
      <Main>
        <Bottom>
          <Likes>
            {props.likes || 0}
            {/* <LikesIcon src={favorite} /> */}
            <Favorite/>
          </Likes>
          <Tags>{props.tags.map((tag) => `#${tag.toLowerCase()}`).join(", ")}</Tags>
        </Bottom>
      </Main>
    </Wrapper>
  )
}
  • Issuing npm run storybook now yelds the following:

ellie_build_ok

Sorry for the extremely long post, hope i could shed some insights and helpd solve your issue.

All 12 comments

I have tried to update the Storybook webpack.config.js file in lots of different ways, but have not had any luck getting it working.

I have tried both the Storybook v4 and v5 notes in the Gatsby readme.

Attempt with Gatsby Storybook v5 instructions:

const path = require("path");

module.exports = ({ config }) => {
  // Transpile Gatsby module because Gatsby includes un-transpiled ES6 code.
  config.module.rules[0].exclude = [/node_modules\/(?!(gatsby)\/)/]

  // use installed babel-loader which is v8.0-beta (which is meant to work with @babel/core@7)
  config.module.rules[0].use[0].loader = require.resolve("babel-loader")

  // use @babel/preset-react for JSX and env (instead of staged presets)
  config.module.rules[0].use[0].options.presets = [
    require.resolve("@babel/preset-react"),
    require.resolve("@babel/preset-env"),
  ]

  // use @babel/plugin-proposal-class-properties for class arrow functions
  config.module.rules[0].use[0].options.plugins = [
    require.resolve("@babel/plugin-proposal-class-properties"),
  ]

  // Prefer Gatsby ES6 entrypoint (module) over commonjs (main) entrypoint
  config.resolve.mainFields = ["browser", "module", "main"]

  // Add typescript loader
  config.module.rules.push({
    test: /\.(ts|tsx)$/,
    include: path.resolve(__dirname, "../src"),
    loader: require.resolve("ts-loader"),
    options: {
      configFile: ".storybook/tsconfig.json"
    }
  });
  config.resolve.extensions.push(".ts", ".tsx");

  // Add markdown loader
  config.module.rules.push({
    test: /\.md$/,
    include: path.resolve(__dirname, "../src"),
    loader: require.resolve("raw-loader")
  });
  config.resolve.extensions.push(".md");

  // Add svg loader

  // modify storybook's file-loader rule to avoid conflicts with svgr
  const fileLoaderRule = config.module.rules.find(rule => rule.test.test('.svg'));
  fileLoaderRule.exclude = path.resolve(__dirname, "../src");

  config.module.rules.push({
    test: /\.svg$/,
    include: path.resolve(__dirname, "../src"),
    use: [{
      loader: '@svgr/webpack',
      options: {
        icon: true,
      },
    }],
  });

  return config
}

@elie222 i've picked up on your issue and based on the information you supplied i have circunvented that issue and i'm able to get build with storybook and gatsby.
I'm going to break down my answer into smaller parts for better understanding:

  • Created a new website based on a template that uses typescript. I used gatsby-casper.
  • Installed the plugin in question, following the instructions here.
  • Added the plugin to gatsby-config.js without any options, to keep it simple.
  • To be safe, added the @svgr/cli package aswell to cover my basis.
  • Added storybook via instructions here
  • Waited for the process to finish, updated the .config.js inside .storybook folder as per Gatsby documentation here for v5.
    Originating the following:
import { configure } from '@storybook/react';

// automatically import all files ending in *.stories.js
const req = require.context('../src', true, /.stories.js$/);
function loadStories() {
  req.keys().forEach(filename => req(filename));
}
// Gatsby's Link overrides:
// Gatsby defines a global called ___loader to prevent its method calls from creating console errors you override it here
global.___loader = {
  enqueue: () => {},
  hovering: () => {},
}
// Gatsby internal mocking to prevent unnecessary errors in storybook testing environment
global.__PATH_PREFIX__ = ""
// This is to utilized to override the window.___navigate method Gatsby defines and uses to report what path a Link would be taking us to if it wasn't inside a storybook
window.___navigate = pathname => {
  action("NavigateTo:")(pathname)
}

configure(loadStories, module);

  • Created a webpack.config.js inside .storybook folder and added the Gatsby configuration used for v5 mentioned in here and as Gatsby uses babel under the hood and you're using typescript i added also the configuration mentioned here for typescript.
    Making the following the contents of the file:
module.exports = ({ config }) => {
    // Transpile Gatsby module because Gatsby includes un-transpiled ES6 code.
    config.module.rules[0].exclude = [/node_modules\/(?!(gatsby)\/)/]

    // use installed babel-loader which is v8.0-beta (which is meant to work with @babel/core@7)
    config.module.rules[0].use[0].loader = require.resolve("babel-loader")

    // use @babel/preset-react for JSX and env (instead of staged presets)
    config.module.rules[0].use[0].options.presets = [
      require.resolve("@babel/preset-react"),
      require.resolve("@babel/preset-env"),
    ]

    // use @babel/plugin-proposal-class-properties for class arrow functions
    config.module.rules[0].use[0].options.plugins = [
      require.resolve("@babel/plugin-proposal-class-properties"),
    ]

    // Prefer Gatsby ES6 entrypoint (module) over commonjs (main) entrypoint
    config.resolve.mainFields = ["browser", "module", "main"]
    config.module.rules.push({
        test: /\.(ts|tsx)$/,
        loader: require.resolve('babel-loader'),
        options: {
          presets: [['react-app', { flow: false, typescript: true }]],
        },
      });

      config.resolve.extensions.push('.ts', '.tsx');
    return config
  }
  • Grabbed a random svg, for this case it was a file called Five-pointed_star.svg, that's nothing more, nothing less than well...a five points star, saved it inside the components folder for simplicity.
  • Created a new functional component named StarComponentContainer.tsx inside that folder with the following code:
import * as React from 'react'; 
import starUrl, { ReactComponent as Star } from './Five-pointed_star.svg' 

const StarComponentContainer:React.FunctionComponent=()=>(
    <div>
        <h4>this is a star</h4>
        <Star/>
    </div>
);
export default StarComponentContainer;
  • Created a story for that component with the following code:
import React from 'react';
import { storiesOf } from '@storybook/react';
import StarComponentContainer from './StarComponentContainer';

storiesOf('Star', module)
  .add('StarComponent default',()=><StarComponentContainer/>)
  • Issued npm run storybook. The build goes fine as you can see below:
    elliesuccessbuild
  • It's when i open http://localhost:6006 that the problem pops up.
    I'm presented with the following:
    error_load_storybook

  • Created a new page called test under pages folder and a storybook story file to test it out
    Page code:

import * as React from 'react';
import StarComponentContainer from '../components/StarComponentContainer';

const TestPage:React.FunctionComponent=()=>(
    <div>
        <h3>
            This is a test page
        </h3>
        <StarComponentContainer/>
    </div>
);
export default TestPage;

Storybook story code:

import React from 'react';
import { storiesOf } from '@storybook/react';

import TestPage from './test';

storiesOf('Test Page',module)
.add('default',()=><TestPage/>)
  • Issued npm run storybook with the same outcome, the build went ok, but the same result as above.
  • I thought...ok something must be missing here. I tested a couple of variants to see if that error went away and the svg was loaded with storybook. Even went to modifying my webpack.config.js to accomodate the code inside the library's gatsby-node file here, same result as above.

There might be something here i'm not seeing and something else in terms of configuration might be needed. Sorry i could not be more of assistance.

What an awesome response! Thanks for taking the time. Will take a look at this soon!

@elie222 no need to thank, glad i could be of some assistance. Sorry that i could not fix the issue entirely.

Just read through it all now. Thanks for the help.

This is my attempt at getting things working:

https://github.com/elie222/elie-tech/tree/storybook

But no luck either. To see the issue at hand:

git clone [email protected]:elie222/elie-tech.git
cd elie-tech
git checkout storybook
yarn storybook

@elie222 It doesn't look like you're declaring @svgr/webpack in your dependencies; is it being loaded by Storybook? Can you go ahead and add it and see if that resolves your issue? For what it's worth, gatsby-plugin-svgr 2.0.2 resolves an incorrect (overly narrow) peer dependency and could be related.

@coreyward @svgr/webpack is added to the project's dependencies, as you can see here. @elie222 i'm currently making some tests to see if i have figured out is actually the problem here. I'll post my findings as soon as i can. Sounds good?

Sounds awesome 鉂わ笍 . This may be a problem with the latest version of Storybook. v5 just came out and there were a fair number of issues related to importing svgs/pngs on SB repo. I have posted there too:

https://github.com/storybooks/storybook/issues/6188

@elie222 looks like it worked. I'm going to break down the answer into smaller bits so that you can make adjustments as you go along.
Going to start off with the typescript part and then move onto the storybook part.

  • When i was working on your issue, one thing kept bugging me while i was trying to make the reproduction work and it's that when you import a svg file into any component a squigly line keeps popping up below, saying it can't resolve it. And i was seeing it in your code aswell.
    So, to make typescript and svg "play nice", in the root of the project i've created a file called custom.d.ts with the following code:
declare module "*.svg" {
    const content: any;
    export default content;
}
  • Modified the tsconfig.json to the following:
{
    "compilerOptions": {
        "outDir": "./dist/",
        "sourceMap": true,
        "noImplicitAny": true,
        "module": "commonjs",
        "target": "esnext",
        "jsx": "react",
        "lib": ["dom", "esnext"]
    },
    "include": [
        "./src/**/*"
    ],
    "files": [
        "custom.d.ts"
    ]
}

No more issues with svg and typescript inside the components.

  • I saw that you have copied over the image files and svg files to the static folder and have some of them replicated inside the some of the project's components folders.
    I understood your reasoning, when you issue start-storybook -p 9001 -c .storybook -s ./static you're expecting that based on the arguments you supplied it will pick up the contents of the static folder and will automatically serve them. Based on your current setup that's not the case, because the exact folder structure will be served as is. And inside a component, for instance ArticleItem.tsx you have the following import import favorite from "./favorite.svg and with that the contents weren't being shown for me. What i did was was move all of the svg files to src/assets/icons and the images to src/assets/images. and updated the mentioned import to import Favorite from "../../assets/icons/favorite.svg";. And you'll understand why later in the comment.
    Also in that "department"(pardon the bad pun), i saw that you're making the folder available to graphql and if you absolutely need it, fine by me, just ignore this part of the comment. But as a good practice with gatsby, items inside the folder are already available to you directly, so you shouldn't need to add a extra layer to your code. More on that here.

Onto the question at hand, to make storybook and gatsby "play nice".

  • The first thing i've done while looking at your code, was to remove @storybook/react": "^5.1.0-alpha.10 and revert it back to @storybook/react": "^5.0.3, reason why, basically it's an alpha and i'm seeing some chatter that it's still a bit brittle.
  • Then @coreyward's comment made me take a step back and think about the webpack config. And he was right, i know for sure that storybook has support for svgs out of the box. But this, this is a special case, you're treating a svg like a React component. And the @svgr/webpack loader was not being added.
  • With that in mind i made a few tweaks to the webpack.config.js file for storybook. Transforming the code into:
 const path= require('path');
 const pathToInlineSvg = path.resolve(__dirname, '../src/assets/icons/');

 module.exports = ({ config }) => {
     // Transpile Gatsby module because Gatsby includes un-transpiled ES6 code.
     config.module.rules[0].exclude = [/node_modules\/(?!(gatsby)\/)/]

     // use installed babel-loader which is v8.0-beta (which is meant to work with @babel/core@7)
     config.module.rules[0].use[0].loader = require.resolve("babel-loader")

     // use @babel/preset-react for JSX and env (instead of staged presets)
     config.module.rules[0].use[0].options.presets = [
       require.resolve("@babel/preset-react"),
       require.resolve("@babel/preset-env"),
     ]

     // use @babel/plugin-proposal-class-properties for class arrow functions
     config.module.rules[0].use[0].options.plugins = [
       require.resolve("@babel/plugin-proposal-class-properties"),
     ]

     // Prefer Gatsby ES6 entrypoint (module) over commonjs (main) entrypoint
     config.resolve.mainFields = ["browser", "module", "main"]
     config.module.rules.push({
         test: /\.(ts|tsx)$/,
         loader: require.resolve('babel-loader'),
         options: {
           presets: [['react-app', { flow: false, typescript: true }]],
         },
     });
     config.resolve.extensions.push('.ts', '.tsx');
     // svg with @svgr
     const fileLoaderRule = config.module.rules.find(rule => rule.test.test('.svg'));
     fileLoaderRule.exclude = pathToInlineSvg;
     config.module.rules.push({
       test: /\.svg$/,
       include: pathToInlineSvg,
       use: [{
         loader: '@svgr/webpack',
         options: {
           icon: true,
         },
       }],
     });
     //
     return config
   }

Focusing on the important parts of the code here:

  • The following line, const pathToInlineSvg = path.resolve(__dirname, '../src/assets/icons/');, like i said above, as i moved all the svg files inside /src/assets/icons/, so this const will hold an array with all of them.
  • The following bit:
 const fileLoaderRule = config.module.rules.find(rule => rule.test.test('.svg'));
     fileLoaderRule.exclude = pathToInlineSvg;
     config.module.rules.push({
       test: /\.svg$/,
       include: pathToInlineSvg,
       use: [{
         loader: '@svgr/webpack',
         options: {
           icon: true,
         },
       }],
     });

When it reaches here, it will exclude the out of the box svg support for the svgs in the above array and adds a new rule for loading svgs, but this time with @svgr/webpack.

  • All that is missing is change the components accordingly. For instance your component ArticleItem.tsx is modified to the following, i left out the majority of the code to focus on the important parts:
// import favorite from "./favorite.svg"
import Favorite from "../../assets/icons/favorite.svg";

export default (props: ArticleItemProps) => {
  return (
    <Wrapper href={props.link} target="_blank">
       ......
      <Main>
        <Bottom>
          <Likes>
            {props.likes || 0}
            {/* <LikesIcon src={favorite} /> */}
            <Favorite/>
          </Likes>
          <Tags>{props.tags.map((tag) => `#${tag.toLowerCase()}`).join(", ")}</Tags>
        </Bottom>
      </Main>
    </Wrapper>
  )
}
  • Issuing npm run storybook now yelds the following:

ellie_build_ok

Sorry for the extremely long post, hope i could shed some insights and helpd solve your issue.

Thanks so much for the help. Working through this now, but just fixing the TS import and reverting to SB 5.0.3 has got SB loading again at least!

Can I buy you a coffee (or 3), on something like: https://www.buymeacoffee.com/? Or send you a tip in Bitcoin/Eth?

Also, I didn鈥檛 quite understand what you meant about me exposing items to graphql. I do load the data on the homepage using graphql so that I can use gatsby image. Is there another place I鈥檓 doing this for no need?

@elie222 i'm glad i could be of assistance. No need for the coffee but thanks for the offer. Also ok...i didn't go through the code with a fine tooth comb, just focused on the storybook part, so disregard the exposing items to graphl part of the comment.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

hobochild picture hobochild  路  3Comments

kalinchernev picture kalinchernev  路  3Comments

jimfilippou picture jimfilippou  路  3Comments

magicly picture magicly  路  3Comments

dustinhorton picture dustinhorton  路  3Comments