storybookjs / storybook

Storybook is the industry standard workshop for building, documenting, and testing UI components in isolation
https://storybook.js.org
MIT License
83.76k stars 9.18k forks source link

TypeError: require.context is not a function #2487

Closed AlexanderTserkovniy closed 6 years ago

AlexanderTserkovniy commented 6 years ago

Hi guys,

Issue details

If you use require.context within a React component the storyshots will fail with TypeError: require.context is not a function.

Steps to reproduce

Just write require.context() in any of the component.

This is mine:

import React from 'react';
import PropTypes from 'prop-types';
import DefaultProps from '../../helpers/default-props';

const lineIcons = require.context('../../assets/icons/Line', true, /.+\.svg$/);
const solidIcons = require.context('../../assets/icons/Solid', true, /.+\.svg$/);
const requireAll = requireContext => requireContext.keys().map(requireContext);
const toObjectNames = (state, icon) => ({ ...state, [icon.default.id]: icon.default.id });
const icons = {
  Line: requireAll(lineIcons).reduce(toObjectNames, {}),
  Solid: requireAll(solidIcons).reduce(toObjectNames, {}),
};

const Icon = ({
  glyph, type = 'Line', width = 14, height = 14, className = 'icon', fill = 'currentColor', ...rest
}) => (
  <svg {...rest} className={className} width={width} fill={fill} height={height}>
    <use xlinkHref={`#${type} ${glyph}`} />
  </svg>
);

Icon.propTypes = {
  ...DefaultProps,
  /** icon name, just exactly how the file is named */
  glyph: PropTypes.string.isRequired,
  /** main folder where lays this file, could be `Line | Solid` */
  type: PropTypes.oneOf(['Line', 'Solid']),
  /** width, which is set to <svg> */
  width: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
  /** height, which is set to <svg> */
  height: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
  /** fill (color), which is set to <svg> */
  fill: PropTypes.string,
};

export default Icon;

export {
  icons,
};

Please specify which version of Storybook and optionally any affected addons that you're running

Affected platforms

Any OS any build.

Screenshots / Screencast / Code Snippets (Optional)

http://prntscr.com/hnhw8u

// content of storyshots.test.js

import initStoryshots from '@storybook/addon-storyshots';

global.window = global;
window.addEventListener = () => {};
window.requestAnimationFrame = () => {
  throw new Error('requestAnimationFrame is not supported in Node');
};

initStoryshots({ /* configuration options */ });
Hypnosphi commented 6 years ago

require.context is a webpack-specific feature, so it doesn't work in jest. You can try to mock it somehow

AlexanderTserkovniy commented 6 years ago

Yes I tried this mock, but then it gives syntax error of just running start-storybook -p 9001 -c .storybook. Some incorrect left-hand assignment. I prefer to keep main job working, tests are something additional.

If you can help mocking it in different way, that main storybook could be working, would be great.

ndelangen commented 6 years ago

You could try something like this: https://github.com/mzgoddard/jest-webpack

Hypnosphi commented 6 years ago

it gives syntax error of just running start-storybook -p 9001 -c .storybook

That's weird. Mocks should only be applied when running tests

ndelangen commented 6 years ago

@AlexanderTserkovniy Did you try to apply the mock in the storyshots.test.js file?

AlexanderTserkovniy commented 6 years ago

Yes, I have tried quite much approaches. Let me show you what I get.

1) If I put it in the .storybook/storyshots.test.js it just fails the same way. Basically this code is not get invoked before the component code is run.

import initStoryshots from '@storybook/addon-storyshots';

global.window = global;
window.addEventListener = () => {};
window.requestAnimationFrame = () => {
  throw new Error('requestAnimationFrame is not supported in Node');
};

// This condition actually should detect if it's an Node environment
if (typeof require.context === 'undefined') {
  const fs = require('fs');
  const path = require('path');

  require.context = (base = '.', scanSubDirectories = false, regularExpression = /\.js$/) => {
    const files = {};

    function readDirectory(directory) {
      fs.readdirSync(directory).forEach((file) => {
        const fullPath = path.resolve(directory, file);

        if (fs.statSync(fullPath).isDirectory()) {
          if (scanSubDirectories) readDirectory(fullPath);

          return;
        }

        if (!regularExpression.test(fullPath)) return;

        files[fullPath] = true;
      });
    }

    readDirectory(path.resolve(__dirname, base));

    function Module(file) {
      return require(file);
    }

    Module.keys = () => Object.keys(files);

    return Module;
  };
}

initStoryshots({ /* configuration options */ });
> jest && echo 'use jest after this is resolved https://github.com/storybooks/storybook/issues/2487'

 FAIL  .storybook/storyshots.test.js
  ● Test suite failed to run

    TypeError: require.context is not a function

      at Object.<anonymous> (src/components/Icon/Icon.js:5:25)
      at Object.<anonymous> (.storybook/stories/Icon.js:3:13)
      at node_modules/@storybook/addon-storyshots/dist/require_context.js:39:24
          at Array.forEach (<anonymous>)
      at requireModules (node_modules/@storybook/addon-storyshots/dist/require_context.js:34:9)
      at Function.newRequire.context (node_modules/@storybook/addon-storyshots/dist/require_context.js:90:5)
      at evalmachine.<anonymous>:14:19
      at runWithRequireContext (node_modules/@storybook/addon-storyshots/dist/require_context.js:102:3)
      at testStorySnapshots (node_modules/@storybook/addon-storyshots/dist/index.js:105:35)
      at Object.<anonymous> (.storybook/storyshots.test.js:45:31)
          at Generator.next (<anonymous>)
          at new Promise (<anonymous>)
          at Generator.next (<anonymous>)
          at <anonymous>
      at process._tickCallback (internal/process/next_tick.js:188:7)

  console.info node_modules/@storybook/react/dist/server/babel_config.js:73
    => Loading custom .babelrc

Test Suites: 1 failed, 1 total
Tests:       0 total
Snapshots:   0 total
Time:        3.978s
Ran all test suites.

2) When I put it into Icon component:

import React from 'react';
import PropTypes from 'prop-types';
import DefaultProps from '../../helpers/default-props';

// This condition actually should detect if it's an Node environment
if (typeof require.context === 'undefined') {
  const fs = require('fs');
  const path = require('path');

  require.context = (base = '.', scanSubDirectories = false, regularExpression = /\.js$/) => {
    const files = {};

    function readDirectory(directory) {
      fs.readdirSync(directory).forEach((file) => {
        const fullPath = path.resolve(directory, file);

        if (fs.statSync(fullPath).isDirectory()) {
          if (scanSubDirectories) readDirectory(fullPath);

          return;
        }

        if (!regularExpression.test(fullPath)) return;

        files[fullPath] = true;
      });
    }

    readDirectory(path.resolve(__dirname, base));

    function Module(file) {
      return require(file);
    }

    Module.keys = () => Object.keys(files);

    return Module;
  };
}

const lineIcons = require.context('../../assets/icons/Line', true, /.+\.svg$/);
const solidIcons = require.context('../../assets/icons/Solid', true, /.+\.svg$/);
const requireAll = requireContext => requireContext.keys().map(requireContext);
const toObjectNames = (state, icon) => ({ ...state, [icon.default.id]: icon.default.id });
const icons = {
  Line: requireAll(lineIcons).reduce(toObjectNames, {}),
  Solid: requireAll(solidIcons).reduce(toObjectNames, {}),
};

const Icon = ({
  glyph, type = 'Line', width = 14, height = 14, className = 'icon', fill = 'currentColor', ...rest
}) => (
  <svg {...rest} className={className} width={width} fill={fill} height={height}>
    <use xlinkHref={`#${type} ${glyph}`} />
  </svg>
);

Icon.propTypes = {
  ...DefaultProps,
  /** icon name, just exactly how the file is named */
  glyph: PropTypes.string.isRequired,
  /** main folder where lays this file, could be `Line | Solid` */
  type: PropTypes.oneOf(['Line', 'Solid']),
  /** width, which is set to <svg> */
  width: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
  /** height, which is set to <svg> */
  height: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
  /** fill (color), which is set to <svg> */
  fill: PropTypes.string,
};

export default Icon;

export {
  icons,
};

then tests are passed PASS .storybook/storyshots.test.js. And command jest just works.

BUT if I run the app (start-storybook -p 9001 -c .storybook), I see this:

@storybook/react v3.2.16

=> Loading custom .babelrc
=> Loading custom addons config.
=> Loading custom webpack config (extending mode).
webpack built 1657d28027f144d8ba93 in 15220ms                                              
Hash: 1657d28027f144d8ba93
Version: webpack 3.8.1
Time: 15220ms
                               Asset       Size  Chunks                    Chunk Names
92bbabfda96fb9e73100d90404d5383a.ttf     136 kB          [emitted]         
816c43ce4c83ecd53572f8ec92d85bc2.ttf     143 kB          [emitted]         
05e056450eab95f6c22a802ac56658be.svg     1.8 kB          [emitted]         
6e7eb8f5ed815b0f3865005bba91ccda.svg  721 bytes          [emitted]         
            static/preview.bundle.js    5.83 MB       0  [emitted]  [big]  preview
            static/manager.bundle.js    3.64 MB       1  [emitted]  [big]  manager
        static/preview.bundle.js.map    5.11 MB       0  [emitted]         preview
        static/manager.bundle.js.map     4.5 MB       1  [emitted]         manager
  [65] (webpack)/buildin/module.js 495 bytes {0} {1} [built]
  [97] (webpack)/buildin/harmony-module.js 572 bytes {0} [built]
  [98] ./node_modules/@storybook/react/dist/client/index.js 1.63 kB {0} [built]
 [255] ./node_modules/@storybook/react/dist/server/config/polyfills.js 113 bytes {0} {1} [built]
 [664] ./node_modules/@storybook/ui/dist/index.js 2.42 kB {1} [built]
 [727] multi ./node_modules/@storybook/react/dist/server/config/polyfills.js ./.storybook/addons.js ./node_modules/@storybook/react/dist/client/manager/index.js 52 bytes {1} [built]
 [728] ./.storybook/addons.js 171 bytes {1} [built]
 [730] ./node_modules/@storybook/addon-links/register.js 30 bytes {1} [built]
 [731] ./node_modules/@storybook/addon-options/register.js 131 bytes {1} [built]
 [734] ./node_modules/@storybook/addon-knobs/register.js 28 bytes {1} [built]
[1002] ./node_modules/@storybook/react/dist/client/manager/index.js 404 bytes {1} [built]
[1189] multi ./node_modules/@storybook/react/dist/server/config/polyfills.js ./node_modules/@storybook/react/dist/server/config/globals.js (webpack)-hot-middleware/client.js?reload=true ./.storybook/config.js 64 bytes {0} [built]
[1190] ./node_modules/@storybook/react/dist/server/config/globals.js 105 bytes {0} [built]
[1191] (webpack)-hot-middleware/client.js?reload=true 7.04 kB {0} [built]
[1203] ./.storybook/config.js 505 bytes {0} [built]
    + 3307 hidden modules

WARNING in ./src/components/Icon/Icon.js
12:11-18 Critical dependency: require function is used in a way in which dependencies cannot be statically extracted
 @ ./src/components/Icon/Icon.js
 @ ./.storybook/stories/Icon.js
 @ ./.storybook/stories .js$
 @ ./.storybook/config.js
 @ multi ./node_modules/@storybook/react/dist/server/config/polyfills.js ./node_modules/@storybook/react/dist/server/config/globals.js (webpack)-hot-middleware/client.js?reload=true ./.storybook/config.js

WARNING in ./src/components/Icon/Icon.js
16:2-9 Critical dependency: require function is used in a way in which dependencies cannot be statically extracted
 @ ./src/components/Icon/Icon.js
 @ ./.storybook/stories/Icon.js
 @ ./.storybook/stories .js$
 @ ./.storybook/config.js
 @ multi ./node_modules/@storybook/react/dist/server/config/polyfills.js ./node_modules/@storybook/react/dist/server/config/globals.js (webpack)-hot-middleware/client.js?reload=true ./.storybook/config.js

WARNING in ./src/components/Icon/Icon.js
42:13-26 Critical dependency: the request of a dependency is an expression
 @ ./src/components/Icon/Icon.js
 @ ./.storybook/stories/Icon.js
 @ ./.storybook/stories .js$
 @ ./.storybook/config.js
 @ multi ./node_modules/@storybook/react/dist/server/config/polyfills.js ./node_modules/@storybook/react/dist/server/config/globals.js (webpack)-hot-middleware/client.js?reload=true ./.storybook/config.js

ERROR in ./src/components/Icon/Icon.js
Module not found: Error: Can't resolve 'fs' in '/ui-components/src/components/Icon'
 @ ./src/components/Icon/Icon.js 13:11-24
 @ ./.storybook/stories/Icon.js
 @ ./.storybook/stories .js$
 @ ./.storybook/config.js
 @ multi ./node_modules/@storybook/react/dist/server/config/polyfills.js ./node_modules/@storybook/react/dist/server/config/globals.js (webpack)-hot-middleware/client.js?reload=true ./.storybook/config.js

and in browser it shows this: Uncaught ReferenceError: Invalid left-hand side in assignment http://prntscr.com/hnt9mu.

ReferenceError: http://prntscr.com/hnta5c

Compiled code:

// This condition actually should detect if it's an Node environment
if (typeof !(function webpackMissingModule() { var e = new Error("Cannot find module \".\""); e.code = 'MODULE_NOT_FOUND'; throw e; }()).context === 'undefined') {
  var fs = __webpack_require__(!(function webpackMissingModule() { var e = new Error("Cannot find module \"fs\""); e.code = 'MODULE_NOT_FOUND'; throw e; }()));
  var path = __webpack_require__(1307);

  !(function webpackMissingModule() { var e = new Error("Cannot find module \".\""); e.code = 'MODULE_NOT_FOUND'; throw e; }()).context = function () {
    var base = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '.';
    var scanSubDirectories = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
    var regularExpression = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : /\.js$/;

    var files = {};

    function readDirectory(directory) {
      fs.readdirSync(directory).forEach(function (file) {
        var fullPath = path.resolve(directory, file);

        if (fs.statSync(fullPath).isDirectory()) {
          if (scanSubDirectories) readDirectory(fullPath);

          return;
        }

        if (!regularExpression.test(fullPath)) return;

        files[fullPath] = true;
      });
    }

    readDirectory(path.resolve(__dirname, base));

    function Module(file) {
      return !(function webpackMissingModule() { var e = new Error("Cannot find module \".\""); e.code = 'MODULE_NOT_FOUND'; throw e; }());
    }

    Module.keys = function () {
      return Object.keys(files);
    };

    return Module;
  };
}

// Fails on !(function webpackMissingModule() { var e = new Error("Cannot find module \".\""); e.code = 'MODULE_NOT_FOUND'; throw e; }()).context = function () {

And I wish to keep such polyfills outside the component, so anyway this is bad way to fix.

Would you help?

Hypnosphi commented 6 years ago

The problem is that require.context is not actually a runtime function, it's rather a hint for webpack that gets replaced by an actual context object.

Did you try the first answer? Maybe combining the separate file approach from it with the stub function from second one https://stackoverflow.com/questions/38332094/how-can-i-mock-webpacks-require-context-in-jest/42439030#42439030

AlexanderTserkovniy commented 6 years ago

Will try and return with results, thanks. Just remember that it was not so neat to try it.

AlexanderTserkovniy commented 6 years ago

BTW @ndelangen (but thanks).

You could try something like this: https://github.com/mzgoddard/jest-webpack

Fails on https://github.com/mzgoddard/jest-webpack/issues/21.

Hypnosphi commented 6 years ago

By the way, how does your story for icon look like?

AlexanderTserkovniy commented 6 years ago
import React from 'react';
import { storiesOf } from '@storybook/react';
import * as Icon from '../../src/components/Icon/Icon';
import Colors from '../../src/common-styles/colors.pcss';

storiesOf('Icon', module)
  .add('Icons set', () => {
    const LineIcons = Object.keys(Icon.icons.Line)
      .map(iconName => <Icon.default type='Line' width='36' height='36' glyph={iconName.replace(/Line /, '')} />);
    const SolidIcons = Object.keys(Icon.icons.Solid)
      .map(iconName => <Icon.default type='Solid' width='36' height='36' glyph={iconName.replace(/Solid /, '')} />);
    return (
      <section style={{ display: 'flex' }}>
        <section style={{ flex: 1 }}>
          <h2>Line icons</h2>
          <ul>
            {LineIcons.map(icon =>
              <li key={Math.random()} style={{ display: 'flex', alignItems: 'center' }}>
                {icon} {icon.props.glyph}
              </li>)}
          </ul>
        </section>
        <section style={{ flex: 1 }}>
          <h2>Solid icons</h2>
          <ul>
            {SolidIcons.map(icon =>
              <li key={Math.random()} style={{ display: 'flex', alignItems: 'center' }}>
                {icon} {icon.props.glyph}
              </li>)}
          </ul>
        </section>
      </section>
    )
  })
  .add('Default', () => <Icon.default glyph='Duck' />)
  .add('With params', () => <Icon.default glyph='Servers' fill='red' width='34' height='34' />)
  .add('With custom class', () => <Icon.default glyph='Barrel' className={Colors.G300} width='64' height='64' />)
  .add('Solid icon', () => <Icon.default glyph='Barrel' className={Colors.G300} width='64' height='64' type='Solid' />);
Hypnosphi commented 6 years ago

So, icons.Line is an object of this form?

{
  'Line arrow': 'Line arrow',
  'Line circle': 'Line circle',
  ...
}
AlexanderTserkovniy commented 6 years ago

Yes

Hypnosphi commented 6 years ago

You can mock it like this then:

https://gist.github.com/Hypnosphi/043d6f138c3dd2ad88472ea74f7bc12f

icons.js gets executed in browser, and __mocks__/icons.js in node

And you'll need to add this line to your Icon.js file:

export { default as icons } from './icons';
AlexanderTserkovniy commented 6 years ago

Thanks, looks good. But I believe I need require.context for webpack to actually bundle all svgs. So if I go with fs, webpack will not add all svg files to the bundle.

Hypnosphi commented 6 years ago

files under __mocks__ directory are only for jest. You shouldn't require them yourself, so they shouldn't affect webpack in any way

AlexanderTserkovniy commented 6 years ago

Yes, got it, I mean this part require.context('../../assets/icons/Line', true, /.+\.svg$/) is used by webpack to actually add files to bundle.

Hypnosphi commented 6 years ago

Sure, and it's preserved in icons.js

Hypnosphi commented 6 years ago

BTW you actually add those SVGs to bundle only when you call requireAll

For storyshots you don't need it because they don't perform actual browser rendering

stale[bot] commented 6 years ago

Hi everyone! Seems like there hasn't been much going on in this issue lately. If there are still questions, comments, or bugs, please feel free to continue the discussion. Unfortunately, we don't have time to get to every issue. We are always open to contributions so please send us a pull request if you would like to help. Inactive issues will be closed after 60 days. Thanks!

ezidio commented 6 years ago

Based on this stackOverflow answer, i created a transformer that add the polyfill only in files that use require.context

https://gist.github.com/ezidio/f64c59d46b19a3fe671a9ded6441de18

It work's for me

Jest v22.4.2

AlexanderTserkovniy commented 6 years ago

Hi @ezidio at the glance looks good. Thanks!

ndelangen commented 6 years ago

You can use this package to resolve the issue: https://github.com/smrq/babel-plugin-require-context-hook

Here's how we are using it: https://github.com/storybooks/storybook/blob/babel-7/scripts/jest.init.js#L10

adamchenwei commented 5 years ago

@ndelangen I just had same issue, but it just suddenly happned without a indicator how. also your 2nd link is broken. only happen when I tried to start setup storyshot with jest and storybook

cyrus-za commented 5 years ago

I had the same issue. Using CRA v3 (not ejected) with ts and I could not figure a way out to get it to work with the babel plugin or anything. Followed various solutions on different git issues but nothing worked for me, so I hacked it with these 3 lines.

This seems to solve it. No .babelrc or jest.config or setupTest.js or anything. I just add the above to replace the original line in .storybook/config.js

BEFORE

// .storybook.config.js
const req = require.context('../src/components', true, /.stories.tsx$/)

AFTER

// .storybook.config.js
import registerRequireContextHook from 'babel-plugin-require-context-hook/register';
registerRequireContextHook();

const req = global.__requireContext(__dirname, '../src/components', true, /.stories.tsx$/)
hongbo-miao commented 4 years ago

The tutorial at https://storybook.js.org/docs/testing/structural-testing/ helped me.

First run

yarn add --dev babel-plugin-macros

in terminal.

Then add this code in .babelrc

{
  "plugins": ["macros"]
}

In .storybook/config.js, update to

import { configure } from '@storybook/react';
import requireContext from 'require-context.macro'; // <- add this

import '../src/index.css';

const req = requireContext('../src/components', true, /\.stories\.js$/); // <- change to this

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

configure(loadStories, module);
cyrus-za commented 4 years ago

I stumbled upon require-context.macro a few days after my initial post up. Like @Hongbo-Miao said, that also works, but requires a .babelrc file inside .storybook

bastian-hidalgo commented 3 years ago

The tutorial at https://storybook.js.org/docs/testing/structural-testing/ helped me.

First run

yarn add --dev babel-plugin-macros

in terminal.

Then add this code in .babelrc

{
  "plugins": ["macros"]
}

In .storybook/config.js, update to

import { configure } from '@storybook/react';
import requireContext from 'require-context.macro'; // <- add this

import '../src/index.css';

const req = requireContext('../src/components', true, /\.stories\.js$/); // <- change to this

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

configure(loadStories, module);

I think there's no way to add a .babelrc into CRA

dingqianwen commented 2 years ago

Thank you.