Storybook: Use storybook for electron app

Created on 8 Jul 2017  Â·  11Comments  Â·  Source: storybookjs/storybook

versions:

"@storybook/addon-actions": "^3.1.8",
    "@storybook/react": "^3.1.8",

I'm trying to use Storybook with an electron app. The components are vanilla JS, but some of them import modules from electron

In my app, I configure webpack to target: 'electron-renderer', and this works fine

However, in storybook, doing so results in the error below.

Has anyone configured storybook to work correctly with electron?

external "crypto"?ef49:1 Uncaught ReferenceError: require is not defined
    at Object.REACT_STATICS.childContextTypes (external "crypto"?ef49:1)
    at __webpack_require__ (bootstrap 25805c1…?42ca:661)
    at fn (bootstrap 25805c1…?42ca:87)
    at Object.byteToHex (rng.js?025f:4)
    at __webpack_require__ (bootstrap 25805c1…?42ca:661)
    at fn (bootstrap 25805c1…?42ca:87)
    at Object.<anonymous> (v1.js?2980:1)
    at __webpack_require__ (bootstrap 25805c1…?42ca:661)
    at fn (bootstrap 25805c1…?42ca:87)
    at Object.<anonymous> (preview.js?2318:26)
  | REACT_STATICS.childContextTypes | @ | external "crypto"?ef49:1
  | __webpack_require__ | @ | bootstrap 25805c1…?42ca:661
  | fn | @ | bootstrap 25805c1…?42ca:87
  | byteToHex | @ | rng.js?025f:4
  | __webpack_require__ | @ | bootstrap 25805c1…?42ca:661
  | fn | @ | bootstrap 25805c1…?42ca:87
  | (anonymous) | @ | v1.js?2980:1
  | __webpack_require__ | @ | bootstrap 25805c1…?42ca:661
  | fn | @ | bootstrap 25805c1…?42ca:87
  | (anonymous) | @ | preview.js?2318:26
  | __webpack_require__ | @ | bootstrap 25805c1…?42ca:661
  | fn | @ | bootstrap 25805c1…?42ca:87
  | canDefineProperty | @ | index.js?7950:16
  | __webpack_require__ | @ | bootstrap 25805c1…?42ca:661
  | fn | @ | bootstrap 25805c1…?42ca:87
  | (anonymous) | @ | register.js:1
  | __webpack_require__ | @ | bootstrap 25805c1…?42ca:661
  | fn | @ | bootstrap 25805c1…?42ca:87
  | (anonymous) | @ | addons.js:3
  | __webpack_require__ | @ | bootstrap 25805c1…?42ca:661
  | fn | @ | bootstrap 25805c1…?42ca:87
  | (anonymous) | @ | ReactPropTypesSecret.js:14
  | __webpack_require__ | @ | bootstrap 25805c1…?42ca:661
  | validateFormat | @ | bootstrap 25805c1…?42ca:707
  | (anonymous) | @ | manager.bundle.js:711


compatibility with other tools feature request todo

Most helpful comment

OK, got it working! 😜
require is not available in the iframe but it is accessible in the parent document, using top.require (provided Electron is running with low security settings, which should be the case when it's called using the electron binary, using electron http://localhost:9001).
The trick is to overwrite default externals for webpack's electron-renderer target and any node-related require that is called directly or indirectly by your components.

Here is my webpack.config.js:

module.exports = {
  target: 'electron-renderer',
  externals: {
    'fs': 'top.require(\'fs\')',
    'path': 'top.require(\'path\')',
    'events': 'top.require(\'events\')',
    'electron': 'top.require(\'electron\')',
    'util': 'top.require(\'util\')',
    'querystring': 'top.require(\'querystring\')',
  },

  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'babel-loader',
        options: {
          cacheDirectory: true,
          babelrc: false,
          presets: ['@babel/react', '@babel/flow',
            ['@babel/preset-env', {
              'targets': {
                'electron': '3.0.4'
              }
            }]
          ],
          plugins: [
            '@babel/plugin-proposal-class-properties', 
            '@babel/plugin-proposal-object-rest-spread',
            ['@babel/plugin-transform-modules-commonjs', {
              'lazy': (module) => {
                // console.log(module)
                return true
              }
            }]
          ],
        }
      },
      {
        test: /(\.less|\.css)$/,
        use: ['style-loader', 'css-loader', 'less-loader'],
      },
      {
        test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/,
        loader: 'url-loader?limit=10000&mimetype=application/font-woff'
      },
      { test: /\.(ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/, loader: 'file-loader' }]
  },
}

All 11 comments

the problem is the iframe that displays the content doesn't inherit require, globals, et al.

some insight on how to deal with this?
I need to use both storybook and the module that I require from the component

Has anyone been able to get this working with Electron? Would love to use Storybook for our Electron based project.

Just a thought. If Storybook – when used in Electron – could use the special webview tags (with node integration) instead of iframes, I think it would work.

OK, got it working! 😜
require is not available in the iframe but it is accessible in the parent document, using top.require (provided Electron is running with low security settings, which should be the case when it's called using the electron binary, using electron http://localhost:9001).
The trick is to overwrite default externals for webpack's electron-renderer target and any node-related require that is called directly or indirectly by your components.

Here is my webpack.config.js:

module.exports = {
  target: 'electron-renderer',
  externals: {
    'fs': 'top.require(\'fs\')',
    'path': 'top.require(\'path\')',
    'events': 'top.require(\'events\')',
    'electron': 'top.require(\'electron\')',
    'util': 'top.require(\'util\')',
    'querystring': 'top.require(\'querystring\')',
  },

  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'babel-loader',
        options: {
          cacheDirectory: true,
          babelrc: false,
          presets: ['@babel/react', '@babel/flow',
            ['@babel/preset-env', {
              'targets': {
                'electron': '3.0.4'
              }
            }]
          ],
          plugins: [
            '@babel/plugin-proposal-class-properties', 
            '@babel/plugin-proposal-object-rest-spread',
            ['@babel/plugin-transform-modules-commonjs', {
              'lazy': (module) => {
                // console.log(module)
                return true
              }
            }]
          ],
        }
      },
      {
        test: /(\.less|\.css)$/,
        use: ['style-loader', 'css-loader', 'less-loader'],
      },
      {
        test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/,
        loader: 'url-loader?limit=10000&mimetype=application/font-woff'
      },
      { test: /\.(ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/, loader: 'file-loader' }]
  },
}

Hi, I may have solution to this issue. As @dperetti mentioned, it would be useful to have stories that running inside webview tag.
So, I've created small library that start separate electron instance just to show stories which it are served on 9002 port.
Check it out, maybe you'll find it useful: https://github.com/web-pal/electronic-stories

OK, got it working! 😜
require is not available in the iframe but it is accessible in the parent document, using top.require (provided Electron is running with low security settings, which should be the case when it's called using the electron binary, using electron http://localhost:9001).
The trick is to overwrite default externals for webpack's electron-renderer target and any node-related require that is called directly or indirectly by your components.

Here is my webpack.config.js:

module.exports = {
  target: 'electron-renderer',
  externals: {
    'fs': 'top.require(\'fs\')',
    'path': 'top.require(\'path\')',
    'events': 'top.require(\'events\')',
    'electron': 'top.require(\'electron\')',
    'util': 'top.require(\'util\')',
    'querystring': 'top.require(\'querystring\')',
  },

  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'babel-loader',
        options: {
          cacheDirectory: true,
          babelrc: false,
          presets: ['@babel/react', '@babel/flow',
            ['@babel/preset-env', {
              'targets': {
                'electron': '3.0.4'
              }
            }]
          ],
          plugins: [
            '@babel/plugin-proposal-class-properties', 
            '@babel/plugin-proposal-object-rest-spread',
            ['@babel/plugin-transform-modules-commonjs', {
              'lazy': (module) => {
                // console.log(module)
                return true
              }
            }]
          ],
        }
      },
      {
        test: /(\.less|\.css)$/,
        use: ['style-loader', 'css-loader', 'less-loader'],
      },
      {
        test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/,
        loader: 'url-loader?limit=10000&mimetype=application/font-woff'
      },
      { test: /\.(ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/, loader: 'file-loader' }]
  },
}

Attempting to get Storybook working with electron v7.1.7 & electron-renderer components as per your comment. Am currently running:

Uncaught ReferenceError: global is not defined

Any ideas would be much appreciated!

Cheers :)

Try to add this before the module section!

  plugins: [
    new webpack.DefinePlugin({
      // Workaround for has-symbol failing to find global in iframe.
      global: '(typeof(global) == "undefined" ? top.global : global)',
      process: '(typeof(process) == "undefined" ? top.process : process)',
      'window.require': 'top.window.require', 
    }),
  ],

Also, as for the externals, there is a new nodeIntegrationInSubFrames option for BrowserWindow that I haven't tried but that's probably the solution. I don't think it would fix the "globals" issue though.

Try to add this before the module section!

  plugins: [
    new webpack.DefinePlugin({
      // Workaround for has-symbol failing to find global in iframe.
      global: '(typeof(global) == "undefined" ? top.global : global)',
      process: '(typeof(process) == "undefined" ? top.process : process)',
      'window.require': 'top.window.require', 
    }),
  ],

Thanks for your help!

I gave it a try, but ended up with Cannot read property 'Symbol' of undefined, where global was again just returning as undefined.

Regardless, I think my knowledge of webpack / storybook is not currently sufficient to fix this issue, so I'll leave this for now and return when i've done enough study to properly troubleshoot rather than bandaid.

Also had a brief look at nodeIntegrationInSubFrames, but that looks to be something i'd do in the main process when instantiating my BrowserWindow? Unsure as to how that would help in Storybook, as as far as I understand Storybook simply renders my component and has no knowledge of BrowserWindows or the like? Unless there is a way to supply that config option to the 'electron-renderer' target?

Are you sure the plugins are applied?

My webpack.config.js looks like this:

const options = {
  target: 'electron-renderer', // https://github.com/webpack/webpack/pull/1467

  externals: {
    'fs': 'top.require(\'fs\')',
    'os': 'top.require(\'os\')',
    'stream': 'top.require(\'stream\')',
    'assert': 'top.require(\'assert\')',
    'constants': 'top.require(\'constants\')',
    'process': 'top.require(\'process\')',
    'crypto': 'top.require(\'crypto\')',
    'path': 'top.require(\'path\')',
    'events': 'top.require(\'events\')',
    'electron': 'top.require(\'electron\')',
    'util': 'top.require(\'util\')',
    'querystring': 'top.require(\'querystring\')',

    'worker_threads': {}, // shut up annoying warning triggered by node_modules/write-file-atomic/index.js
  },

  resolve: { // http://webpack.github.io/docs/configuration.html#resolve
    alias: {
    },
    extensions: ['.mjs', ".ts", ".tsx", ".js"]
  },
  plugins: [

    new webpack.DefinePlugin({
      // Workaround for has-symbol failing to find global in iframe.
      global: '(typeof(global) == "undefined" ? top.global : global)',
      process: '(typeof(process) == "undefined" ? top.process : process)',
      'window.require': 'top.window.require',
    }),
  ],
  module: {
    rules: [...] 
  },
}

module.exports = async ({ config, mode }) => {
  // console.log(config)
  const c = {
    ...config,
    plugins: [...config.plugins, ...options.plugins],
    externals: options.externals,
    resolve: options.resolve,
    module: { ...config.module, ...options.module }
  }
  return c
}

Also I launch Storybook like this:

start-storybook -p 9001 -c storybook --ci

And I launch electron separately:

electron stories/electron_index.js

electron_index.js

const { default: installExtension, REACT_DEVELOPER_TOOLS } = require('electron-devtools-installer')
const app = require('electron').app
const BrowserWindow = require('electron').BrowserWindow
app.commandLine.appendSwitch('disable-web-security')
app.commandLine.appendSwitch('remote-debugging-port', '5858')

app.on('ready', function() {

  mainWindow = new BrowserWindow({
    width: 1100,
    height: 800,
    allowRunningInsecureContent: true,
    webPreferences: {
      nodeIntegration: true,
      webviewTag: false,
      sandbox: false,
      allowRunningInsecureContent: true,
      offscreen: false,
      webSecurity: false,
      contextIsolation: false,
    },
  })

  mainWindow.loadURL('http://localhost:9001')

  installExtension(REACT_DEVELOPER_TOOLS)
    .then((name) => console.log(`Added Extension:  ${name}`))
    .catch((err) => console.log('An error occurred: ', err));
})

So it's totally possible to tweak the BrowserWindow.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

ZigGreen picture ZigGreen  Â·  3Comments

tomitrescak picture tomitrescak  Â·  3Comments

shilman picture shilman  Â·  3Comments

purplecones picture purplecones  Â·  3Comments

Jonovono picture Jonovono  Â·  3Comments