Storybook: create-react-app + jsconfig.json paths: modules/scripts not found

Created on 26 Jun 2019  路  7Comments  路  Source: storybookjs/storybook

Describe the bug
When using CRA and "jsconfig.json": if I set "path" property to solve imports alias, the app works as expected running CRA, but Storybook crashes saying it can't resolve imports.

To Reproduce
Steps to reproduce the behavior:

  1. Start a new CRA app
  2. Create some containers and import them using aliases.
  3. Fill jsconfig.json file with paths/alias and make sure it works ok when building using CRA
  4. Try running Storybook: it crashes
  5. See modules not found error

Expected behavior
Shouldn't Storybook take the paths from jsconfig.json file - or solve it someway with full control mode, where I could solve the imports using "resolve" with Webpack?

Code snippets

This is my current jsconfig.json file (Using it with Create React App it works perfectly and the app builds with no errors):

// jsconfig.json
{
  "compilerOptions": {
    "baseUrl": "./src",
    "paths": {
      "@mycompany/app/*": ["./packages/app/*"],
      "@mycompany/http/*": ["./packages/http/*"],
      "@mycompany/icons/*": ["./packages/icons/*"],
      "@mycompany/ui/*": ["./packages/ui/*"]
      // etc...
    }
  },
  "exclude": ["node_modules", "build", "coverage", "dist", "lib"]
}

This is the .storybook/config.js file.

// .storybook/config.json
import { addDecorator, configure } from '@storybook/react';
import { withKnobs } from '@storybook/addon-knobs/react';
import JssProvider from 'react-jss/lib/JssProvider';
import MuiThemeProvider from '@material-ui/core/styles/MuiThemeProvider';
import React from 'react';

import TooltipProvider from '../src/packages/ui/src/Tooltip/TooltipProvider';
import theme from '../src/packages/app/src/theme';

addDecorator(withKnobs);

const generateClassName = (rule, styleSheet) =>
  `${styleSheet.options.classNamePrefix}-${rule.key}`;

addDecorator(story => (
  <JssProvider generateClassName={generateClassName}>
    <MuiThemeProvider theme={theme}>
      <TooltipProvider>
        <div
          style={{
            position: 'absolute',
            top: 0,
            right: 0,
            bottom: 0,
            left: 0,
            width: '100%',
            height: '100%'
          }}
        >
          {story()}
        </div>
      </TooltipProvider>
    </MuiThemeProvider>
  </JssProvider>
));

const req = require.context('../src', true, /.*.stories.js$/);

const loadStories = () => {
  req.keys().forEach(filename => req(filename));
};

configure(loadStories, module);

Also, I've tried to use the full control mode, with this config:

// .storybook/webpack.config.js
const path = require('path');

module.exports = async ({ config }) => {

  config.resolve = Object.assign(config.resolve, {
    alias: {
      "@mycompany/app": path.resolve(__dirname, "../src/packages/app/"),
      "@mycompany/http": path.resolve(__dirname, "../src/packages/http"),
      "@mycompany/icons": path.resolve(__dirname, "../src/packages/icons"),
      // etc...
    }
  });

  return config;
}

System:

  • OS: MacOS 10.14.5
  • Version: "@storybook/react": "^5.0.10"
cra

Most helpful comment

Hey, @mrmckeb.
Thank you for your reply.

Actually I have to say I was completely WRONG when said that CRA was assuming jsconfig.json file and using it to resolve the paths/alias. No no. Not, at least, not YET

So, I think we can close this issue.

For those who is interested in having CRA working with paths (or, if you prefer alias) to your folders/packages, here it is what I did to make it work (without eject CRA)

Step 1

I prefer not to eject CRA, so I went with some plugin: I used the well known create-react-app-rewired

Step 2

After install it, I created a config-overrides.js in the root of the app and put this inside:

const path = require("path");

const resolve = dir => path.resolve(__dirname, dir);

module.exports = function(config, env) {
  config.resolve.alias = Object.assign(config.resolve.alias, {
    "@mycompany/app": resolve("src/packages/app"),
    "@mycompany/core": resolve("src/packages/core"),
    "@mycompany/http": resolve("src/packages/http"),
    // etc...
  });

  return config;
};

Step 3

Then, I created the jsconfig.json file with these lines:

{
  "compilerOptions": {
    "baseUrl": "./src",
    "paths": {
      "@mycompany/app/*": ["packages/app/*"],
      "@mycompany/core/*": ["packages/core/*"],
      "@mycompany/http/*": ["packages/http/*"],
      // etc... you got the idea...
    }
  },
  "exclude": ["node_modules", "build", "coverage", "dist", "lib"]
}

And then you would ask: "But why use this file if you already did the rewired webpack in step 2?". Because jsconfig.json file helps your IDE to find your modules and files, etc. At least, my VSCode works better on this way.

Step 4: JEST

JEST: now, you must run your tests, right? So, add these lines to your package.json also:

// etc...
"jest": {
    "moduleNameMapper": {
      "^@mycompany/app(/?)(.*?)$": "packages/app$1$2",
      "^@mycompany/core(/?)(.*?)$": "packages/core$1$2",
      "^@mycompany/http(/?)(.*?)$": "packages/http$1$2",
    }
  },

Pretty nasty, ahn? The keys are RegExp that Jest uses to find your files. And "Why did you use 2 groups in your regex, if they are at the end of the string anyway?". Well, this is how Webpack (or Jest, I'm not sure) will match your files:

So, these both will work:

import MyAwesomeModule from '@mycompany/app'
import MyOtherAwesomeModule from '@mycompany/app/folder1/folder2/MyOtherAwesomeModule';

Step 5: Storybook

Well, let's put Storybook in full-control mode, so we can apply the same concepts to Storybook as well. To do that, you must create a webpack.config.js file inside .storybook folder and add there your packages "binds" too:

const path = require("path");

const resolve = dir => path.resolve(__dirname, dir);

module.exports = async ({ config }) => {
  config.resolve = Object.assign(config.resolve, {
    alias: {
      "@mycompany/app": resolve("../src/packages/app"),
      "@mycompany/core": resolve("../src/packages/core"),
      "@mycompany/http": resolve("../src/packages/http"),
    }
  });

  return config;
};

Note that, since this file is inside .storybook folder, the path needs the ../ before all src/etc folder.

Bottom Line:

I don't know if there is a better strategy here. Maybe there is a really better way. Keep NODE_PATH in .env file and ignore the CRA warning? Maybe eject CRA? Maybe use NextJS?

As I put in the link in the beginning, I think CRA will soon have it's "way" of offering path out of the box. But, for now, I don't think it's worth to do all these configurations. For me, it's too much effort. Anyway, maybe it can help someone.

cheers

All 7 comments

Hey @ppalmeida, sorry you're facing this issue. Could you try updating your Storybook version to the latest stable versions - if that doesn't help, please let me know.

Also, keep in mind that Create React App doesn't support paths officially, and we've not tested that in Storybook.

Can you also share a test repository that I could have a look at? A basic reproduction only.

PS: Sorry for the slow reply, I've been away.

Hey, @mrmckeb.
Thank you for your reply.

Actually I have to say I was completely WRONG when said that CRA was assuming jsconfig.json file and using it to resolve the paths/alias. No no. Not, at least, not YET

So, I think we can close this issue.

For those who is interested in having CRA working with paths (or, if you prefer alias) to your folders/packages, here it is what I did to make it work (without eject CRA)

Step 1

I prefer not to eject CRA, so I went with some plugin: I used the well known create-react-app-rewired

Step 2

After install it, I created a config-overrides.js in the root of the app and put this inside:

const path = require("path");

const resolve = dir => path.resolve(__dirname, dir);

module.exports = function(config, env) {
  config.resolve.alias = Object.assign(config.resolve.alias, {
    "@mycompany/app": resolve("src/packages/app"),
    "@mycompany/core": resolve("src/packages/core"),
    "@mycompany/http": resolve("src/packages/http"),
    // etc...
  });

  return config;
};

Step 3

Then, I created the jsconfig.json file with these lines:

{
  "compilerOptions": {
    "baseUrl": "./src",
    "paths": {
      "@mycompany/app/*": ["packages/app/*"],
      "@mycompany/core/*": ["packages/core/*"],
      "@mycompany/http/*": ["packages/http/*"],
      // etc... you got the idea...
    }
  },
  "exclude": ["node_modules", "build", "coverage", "dist", "lib"]
}

And then you would ask: "But why use this file if you already did the rewired webpack in step 2?". Because jsconfig.json file helps your IDE to find your modules and files, etc. At least, my VSCode works better on this way.

Step 4: JEST

JEST: now, you must run your tests, right? So, add these lines to your package.json also:

// etc...
"jest": {
    "moduleNameMapper": {
      "^@mycompany/app(/?)(.*?)$": "packages/app$1$2",
      "^@mycompany/core(/?)(.*?)$": "packages/core$1$2",
      "^@mycompany/http(/?)(.*?)$": "packages/http$1$2",
    }
  },

Pretty nasty, ahn? The keys are RegExp that Jest uses to find your files. And "Why did you use 2 groups in your regex, if they are at the end of the string anyway?". Well, this is how Webpack (or Jest, I'm not sure) will match your files:

So, these both will work:

import MyAwesomeModule from '@mycompany/app'
import MyOtherAwesomeModule from '@mycompany/app/folder1/folder2/MyOtherAwesomeModule';

Step 5: Storybook

Well, let's put Storybook in full-control mode, so we can apply the same concepts to Storybook as well. To do that, you must create a webpack.config.js file inside .storybook folder and add there your packages "binds" too:

const path = require("path");

const resolve = dir => path.resolve(__dirname, dir);

module.exports = async ({ config }) => {
  config.resolve = Object.assign(config.resolve, {
    alias: {
      "@mycompany/app": resolve("../src/packages/app"),
      "@mycompany/core": resolve("../src/packages/core"),
      "@mycompany/http": resolve("../src/packages/http"),
    }
  });

  return config;
};

Note that, since this file is inside .storybook folder, the path needs the ../ before all src/etc folder.

Bottom Line:

I don't know if there is a better strategy here. Maybe there is a really better way. Keep NODE_PATH in .env file and ignore the CRA warning? Maybe eject CRA? Maybe use NextJS?

As I put in the link in the beginning, I think CRA will soon have it's "way" of offering path out of the box. But, for now, I don't think it's worth to do all these configurations. For me, it's too much effort. Anyway, maybe it can help someone.

cheers

@ppalmeida - what about _ESLINT_?
what about globbing paths (**) for importing deeply nested files in those aliases?

Hey, @yairEO.

About the **, they are not much useful I think. Since all alias just ends up with packages/app/* (a single * will do the trick). Maybe on Jest aliases, that uses those two matching groups like this one:

"^@mycompany/app(/?)(.*?)$": "packages/app$1$2",

But I'm not sure. didn't test it. I'll take a look.

Talking about ESLint, what's the problem you're facing? Because the ESLint works well here without any other additional configuration. Do you want to share some particular problem you're facing at?

@xppalmeida - I was facing problems where eslint complained about not being able to resolve my absolute paths but I've managed to fix them!

Using this resolver: eslint-import-resolver-node

eslint configuration file (relevant part)

settings: {
    'import/resolver': {
        'node': {
            'paths': ['./', './src']
    }
}

.env

...
NODE_PATH=./

jsconfig.json

{
    "compilerOptions": {
        "jsx": "react",
        "baseUrl": "./",
        "paths": {
            "stories/*": ["stories/*"],
            "shared/*": ["src/shared/*"],
        },
    },
    "include": ["src/**/*", "stories/**/*"]
}

only with 'paths': ['./', './src'] in my eslint config things the linter worked resolving the paths correctly.

Storybook seems to work well with the above configuration and nothing else.

Really good to know! Thank you!

Was this page helpful?
0 / 5 - 0 ratings

Related issues

p3k picture p3k  路  61Comments

bpeab picture bpeab  路  70Comments

tycho01 picture tycho01  路  76Comments

joeruello picture joeruello  路  79Comments

ChucKN0risK picture ChucKN0risK  路  74Comments