storybook: Unable to set environment variables

Describe the bug

I’m setting up my project to use env-cmd to set environment variables using different .env.{environment} files (ex. .env.dev, .env.staging, etc). Unfortunately storybook isn’t loading my environment variables. I’ve also tried setting environment variables in the command when running start-storybook (ex. REACT_APP_TEST_VAR=testing start-storybook -p 9009 -s public), and storybook isn’t picking up that environment variable either.

To Reproduce

Steps to reproduce the behavior:

Setting env vars in the command:

  1. Add console.log(process.env.REACT_APP_TEST_VAR) to a js story file
  2. Run REACT_APP_TEST_VAR=testing start-storybook -p 9009 -s public
  3. In the console, you should see undefined

Setting env vars with env-cmd:

  1. Install env-cmd (ex. yarn add env-cmd)
  2. Add console.log(process.env.REACT_APP_TEST_VAR) to a js story file
  3. Create a .env.dev file in the same directory as package.json that has the line REACT_APP_TEST_VAR=testing
  4. Run env-cmd -f .env.dev start-storybook -p 9009 -s public
  5. In the console, you should see undefined

Expected behavior

I expect process.env.REACT_APP_TEST_VAR to be set to testing, and for testing to get logged out in the console.

System:

Environment Info:

System: OS: macOS 10.15.6 CPU: (12) x64 Intel® Core™ i7-9750H CPU @ 2.60GHz Binaries: Node: 13.12.0 - ~/.nvm/versions/node/v13.12.0/bin/node Yarn: 1.19.1 - /usr/local/bin/yarn npm: 6.14.4 - ~/.nvm/versions/node/v13.12.0/bin/npm Browsers: Chrome: 84.0.4147.135 Safari: 13.1.2 npmPackages: @storybook/preset-create-react-app: ^3.1.4 => 3.1.4 @storybook/react: ^6.0.6 => 6.0.7

Additional context

A similar issue was brought up in https://github.com/storybookjs/storybook/issues/5166, which is now closed.

About this issue

  • Original URL
  • State: open
  • Created 4 years ago
  • Reactions: 23
  • Comments: 21 (4 by maintainers)

Most upvoted comments

But my application components already expect a given set of environment variables to be present, and not prefixed with STORYBOOK_, I shouldn’t need to change my application code to point to different variables depending on the environment. Is there any workaround for this?

It’s a security issue to make all environment variables available in your frontend code. Since Storybook supports a wide variety of environments, should we support every possible prefix?

Why should Storybook deal with the security of my application? It my responsibility to provide a safe environment and not expose unwanted data, and I could still leak sensitive data in a millon ways, for example by prefixing every environment variable with STORYBOOK_ in a script / webpack config like many people are currently doing to bypass this. You could also turn this off by default but have a configuration flag to forward variables with a warning, but please don’t block my workflow because it may possibly be a security issue

Had the same problem as federicom-scale and FrederickEngelhardt (Wanting to use the env variables my code already uses)

So the “problem” is https://github.com/storybookjs/storybook/blob/188e52c1dbc95bd19600a966a7fdd79c89511bae/lib/core/src/server/config/utils.ts#L53 filters everything that does not start with STORYBOOK_. (I get shilmans point to wipe away all the env variables that are initially present, like $HOME, $PATH and about 100 others – but it would still be nice to have some control over the process / the result).

Anyways, the iframe-webpack.config.ts overwrites process.env, so my dirty solution to add vars afterwards is:

.storybook/main.js

const appConfig = { foo: 'foo', bar: 'bar' };
module.exports = {
  webpackFinal: async config => {
    // find the DefinePlugin
    const plugin = config.plugins.find(plugin => plugin.definitions?.['process.env']);
    // add my env vars
    Object.keys(appConfig).forEach(key => {
      plugin.definitions['process.env'][key] = JSON.stringify(appConfig[key]);
    });
  },
};

Maybe it helps someone.

I think this line:

https://github.com/storybookjs/storybook/blob/188e52c1dbc95bd19600a966a7fdd79c89511bae/lib/core/src/server/config/utils.ts#L53

should not be hardcoded. There should be a config prop that allows us to pass string or array of strings (or even custom regular expressions) that define what variables should be whitelisted. For instance, NextJS already does the same, whitelists all variables that start with NEXT_PUBLIC_. CRA does it for variables that start with REACT_APP_.

Seems a bit redundant to create a separate .env file or duplicate variables with different names inside same .env file just to get over this hump. A workaround can be implemented, but that shouldn’t be considered a long term solution.

This looks tedious to maintain on a project with a lot of env variables:

NEXT_PUBLIC_APIURL=
NEXT_PUBLIC_GOOGLE_MAP_KEY=
....
STORYBOOK_APIURL=
STORYBOOK_GOOGLE_MAP_KEY=
....

It’s a security issue to make all environment variables available in your frontend code. Since Storybook supports a wide variety of environments, should we support every possible prefix?

I’ve repurposed the above into what I feel is a cleaner solution.

In my .storybook directory, I created a .env.storybook file with my desired env vars. To parse this file, I used the dotenv package: npm i -D dotenv or yarn add --dev dotenv.

Then, in my main.js file:

const path = require('path');
const storybookDotenv = require('dotenv').config({
	path: path.resolve(__dirname, '.env.storybook'),
});

module.exports = {
	webpackFinal: async (config) => {
		// ----------------------------------------
		// Manually inject environment variables
		// Note that otherwise, only `STORYBOOK_*` prefix env vars are supported
		// Ref: https://github.com/storybookjs/storybook/issues/12270

		const envVarsToInject = storybookDotenv.parsed;
		const hasEnvVarsToInject =
			envVarsToInject && Object.keys(envVarsToInject).length > 0;

		if (hasEnvVarsToInject) {
			const definePlugin = config.plugins.find(
				(plgn) => plgn.definitions && plgn.definitions['process.env'],
			);

			if (definePlugin) {
				Object.keys(envVarsToInject).forEach((key) => {
					definePlugin.definitions['process.env'][key] = JSON.stringify(
						envVarsToInject[key],
					);
				});
			}
		}

		// ----------------------------------------

		return config;
	},
};

FYI comments and spacing etc is to keep things clear and separate as per my own config file, which has a lot of other stuff going on. Hope this helps someone! 😃

Just spent an hour or so fighting with a combination of this and https://github.com/storybookjs/storybook/issues/17336

This is a combination that appears to work with core.builder = 'webpack5'. As a bonus it fixes the empty env issue from 17336

Note: Set up to inject NEXT_PUBLIC_ currently

const webpack = require('webpack')

// Inject vars prefixed with NEXT_PUBLIC
// Ref: https://github.com/storybookjs/storybook/issues/12270
// Ref: https://github.com/storybookjs/storybook/issues/17336

const injectVars = Object.keys(process.env).reduce((c,key) => {
  if(/^NEXT_PUBLIC_/.test(key)) {
    c[`process.env.${key}`] = JSON.stringify(process.env[key]);
  }
  return c;
}, {})

function injectEnv(definitions) {
  const env = 'process.env';

  if (!definitions[env]) {
    return {
      ...definitions,
      [env]: JSON.stringify(
        Object.fromEntries(
          Object.entries(definitions)
            .filter(([key]) => key.startsWith(env))
            .map(([key, value]) => [key.substring(env.length + 1), JSON.parse(value)]),
        ),
      ),
    };
  }
  return definitions;
}

module.exports = {
  core: {
    builder: "webpack5",
  },
  webpackFinal: (config) => {
    config.plugins = config.plugins.reduce((c, plugin) => {
      if(plugin instanceof webpack.DefinePlugin) {
        return [
          ...c,
          new webpack.DefinePlugin(
            injectEnv({
              ...plugin.definitions,
              ...injectVars,
            })
          ),
        ]
      }

      return [
        ...c,
        plugin,
      ]
    }, []);

    return config;
  },
}

Something here seems fundamentally wrong.

With storybook 6.3.12, all of the environment vars with the REACT_PREFIX in my various .env.* files are available inside my components. In 6.4.13, they are all undefined.

If we have to prefix stuff with REACT_APP so that a CRA build can make them available to the code that we’re trying to test, it really doesn’t help to have storybook filter them all out.

For me I wanted to have an env to know that I’m building for Storybook. I ended up setting in it .storybook/main.js. docs for env vars

module.exports = {
  "stories": ["../src/**/*.stories.js"],
  "addons": ["@storybook/addon-links", "@storybook/addon-essentials", "@storybook/preset-create-react-app"],
  "framework": "@storybook/react",
  core: {
    builder: "webpack5"
  },
  env: (config) => ({
    ...config,
    STORYBOOK_ENV: true,
  }),
};

and accessing it with process.env.STORYBOOK_ENV

Small tweak to @maxbeier’s script above. Important thing is to remember to return the config.

const appConfig = { foo: 'foo', bar: 'bar' };
module.exports = {
  webpackFinal: async (config) => {
    const plugin = config.plugins.find((plugin) => plugin.definitions?.['process.env']);
    plugin.definitions['process.env'] = {
      ...plugin.definitions['process.env'],
      ... appConfig
    };
    return config;
  },
};

Hussshhhh! Finally I’m able to inject environment variables from my .env-cmdrc file (I’m using env-cmdrc, storybook 7.4).

If anyone is still struggling & want to inject the original environment vars (WITHOUT ADDING STORYBOOK_), this post may help you.

Please follow below steps -

  1. Add relevant environment in storybook script in package.json. Example - I’m adding local here - "storybook": "env-cmd -e local storybook dev -p 6006",
  2. Now you will be able to get all the relevant variables from your .env-cmdrc inside env prop in main.ts (document ref). We just need to append them into env the following way - env: config => ({ ...config, ...injectVars }), Where injectVars is the function where I collected my specific environment variables which contain REACT_APP_ prefix - const injectVars = Object.keys(process.env).reduce((c, key) => { if (/^REACT_APP_/.test(key)) { c[key] = process.env[key]; } return c; }, {});

That’s it. Now I get access my environment variables using process.env.<variable_name>.

Thanks @joeswann & everyone for the references

If you want separate development vs production env var files (as I’ve just realised I need), here’s how I did that:

Similar to the above, using the dotenv package - but instead of the one .env.storybook file, I’ve got two separate env files in my .storybook dir now: the standard .env.development and .env.production.

Code:

const path = require('path');
const dotenv = require('dotenv');

// ----------------------------------------
// Storybook custom env var parsing

const devDotenv = dotenv.config({
	path: path.resolve(__dirname, '.env.development'),
});
const prodDotenv = dotenv.config({
	path: path.resolve(__dirname, '.env.production'),
});

// ----------------------------------------

module.exports = {
	webpackFinal: async (config) => {
		// ----------------------------------------
		// Manually inject environment variables
		// Note that otherwise, only `STORYBOOK_*` prefix env vars are supported
		// Ref: https://github.com/storybookjs/storybook/issues/12270

		const definePlugin = config.plugins.find(
			(plgn) => plgn.definitions && plgn.definitions['process.env'],
		);
		const processEnv = definePlugin.definitions['process.env'];
		const nodeEnv = processEnv.NODE_ENV;

		// Note process.env values are stored in double quotation
		const isDevelopment = nodeEnv === '"development"';
		const isProduction = nodeEnv === '"production"';

		/**
		 * For a given dotenv file, validate and inject its
		 * variables into the Storybook `process.env` object.
		 */
		const injectEnvVars = (envObj) => {
			const envVarsToInject = envObj.parsed;
			const hasEnvVarsToInject =
				envVarsToInject && Object.keys(envVarsToInject).length > 0;

			if (hasEnvVarsToInject) {
				Object.keys(envVarsToInject).forEach((key) => {
					processEnv[key] = JSON.stringify(envVarsToInject[key]);
				});
			}
		};

		if (isDevelopment) injectEnvVars(devDotenv);
		else if (isProduction) injectEnvVars(prodDotenv);

		// ----------------------------------------

		return config;
	},
};