storybookjs / storybook

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

[Bug]: ERR! SassError: expected "{". & ERR! Module build failed (from ./node_modules/file-loader/dist/cjs.js): #21592

Closed FK78 closed 1 year ago

FK78 commented 1 year ago

Describe the bug

Currently we are using Node 14, but when upgrading to Node 18 and trying to run npm run build-storybook I am getting these errors:

ERR! Module build failed (from ./node_modules/sass-loader/dist/cjs.js):
ERR! SassError: expected "{".
ERR!   ╷
ERR! 3 │ export default {"root":"style_root__eIUc7","wrapper":"style_wrapper__beJSN","header":"style_header__j-0k7","ticket":"style_ticket__+D5m0","ticketContent":"style_ticketContent__9oCFd","validDates":"style_validDates__XeS7W","howTo":"style_howTo__W21WS","howToSubtext":"style_howToSubtext__RaJs1","steps":"style_steps__fS1LG","stepOne":"style_stepOne__kChvM","stepThree":"style_stepThree__HpJmf","getApp":"style_getApp__SxTMI","getAppPhone":"style_getAppPhone__S60VJ","getAppText":"style_getAppText__o-5tK","getAppDownload":"style_getAppDownload__bjzhC","footer":"style_footer__CWRo3","Logo":"style_Logo__FBond","button":"style_button__HeAO9"};
ERR!   │                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      ^

Reverting "@storybook/preset-create-react-app" to ^3.2.0 fixes the error above, but another appears:

ERR! => Failed to build the preview
ERR! Module not found: Error: You attempted to import /node_modules/@storybook/client-api which falls outside of the project src/ directory. Relative imports outside of src/ are not supported.
ERR! You can either move it inside src/, or add a symlink to it from project's node_modules/.

My devDependencies:

"devDependencies": {
    "@applitools/eyes-webdriverio": "^5.42.2",
    "@babel/core": "^7.21.0",
    "@babel/preset-env": "^7.20.2",
    "@babel/preset-react": "^7.18.6",
    "@cucumber/cucumber": "^7.3.1",
    "@pmmmwh/react-refresh-webpack-plugin": "^0.5.10",
    "@sentry/webpack-plugin": "^1.20.0",
    "@storybook/addon-a11y": "^6.5.16",
    "@storybook/addon-actions": "^6.5.16",
    "@storybook/addon-essentials": "^6.5.16",
    "@storybook/addon-links": "^6.5.16",
    "@storybook/addon-postcss": "^2.0.0",
    "@storybook/builder-webpack5": "^6.5.16",
    "@storybook/manager-webpack5": "^6.5.16",
    "@storybook/preset-create-react-app": "^4.1.2",
    "@storybook/react": "^6.5.16",
    "@testing-library/jest-dom": "^5.16.5",
    "@testing-library/react": "^12.0.0",
    "@testing-library/user-event": "^13.5.0",
    "@wdio/cli": "^7.16.13",
    "@wdio/cucumber-framework": "^7.16.11",
    "@wdio/local-runner": "^7.16.13",
    "@wdio/sauce-service": "^7.16.16",
    "@wdio/selenium-standalone-service": "^7.16.11",
    "@wdio/spec-reporter": "^7.16.11",
    "are-you-es5": "^2.1.2",
    "aws-sdk": "^2.1331.0",
    "babel-jest": "^29.5.0",
    "babel-loader": "^9.1.2",
    "browserslist": "^4.21.5",
    "cdnizer": "^3.3.0",
    "chai": "^4.3.7",
    "chalk": "^4.1.2",
    "chalk-table": "^1.0.2",
    "compression-webpack-plugin": "^10.0.0",
    "concurrently": "^6.4.0",
    "copy-webpack-plugin": "^11.0.0",
    "css-loader": "^6.7.3",
    "css-minimizer-webpack-plugin": "^4.2.2",
    "debug": "^4.3.4",
    "dotenv-cli": "^4.1.1",
    "es-check": "^6.1.0",
    "eslint-loader": "^4.0.2",
    "fetch-mock": "^9.11.0",
    "filesize": "^8.0.6",
    "fs-extra": "^10.0.0",
    "gzip-size": "^6.0.0",
    "history": "^5.3.0",
    "html-webpack-plugin": "^5.5.0",
    "husky": "^8.0.0",
    "identity-obj-proxy": "^3.0.0",
    "image-minimizer-webpack-plugin": "^2.2.0",
    "imagemin-gifsicle": "^7.0.0",
    "imagemin-mozjpeg": "^9.0.0",
    "imagemin-pngquant": "^9.0.2",
    "jest": "^27.5.1",
    "jest-fail-on-console": "^3.0.2",
    "jest-localstorage-mock": "^2.4.26",
    "jsdom": "^16.7.0",
    "jsonwebtoken": "^8.5.1",
    "lint-staged": "^13.1.2",
    "mime": "^3.0.0",
    "mini-css-extract-plugin": "^2.7.3",
    "mockdate": "^3.0.5",
    "nock": "^13.3.0",
    "node-fetch": "^2.6.7",
    "node-sass": "^8.0.0",
    "nodemon": "^2.0.21",
    "npm-run-all": "^4.1.5",
    "pa11y": "^6.2.3",
    "postcss": "^8.4.21",
    "postcss-flexbugs-fixes": "^5.0.2",
    "postcss-import": "^15.1.0",
    "postcss-loader": "^7.0.2",
    "postcss-preset-env": "^8.0.1",
    "progress": "^2.0.3",
    "react-refresh": "^0.14.0",
    "recursive-readdir": "^2.2.3",
    "redux-devtools-extension": "^2.13.9",
    "sass-loader": "^13.2.0",
    "style-loader": "^3.3.1",
    "stylelint": "^14.0.1",
    "stylelint-config-sass-guidelines": "^9.0.1",
    "supertest": "^6.3.3",
    "wait-on": "^7.0.1",
    "webpack": "^5.76.0",
    "webpack-bundle-analyzer": "^4.8.0",
    "webpack-cli": "^5.0.1",
    "webpack-dev-server": "^4.11.1",
    "webpack-manifest-plugin": "^5.0.0",
    "whatwg-fetch": "^3.6.2"
  },

My .storybook/main.js:

require('dotenv').config();

const path = require('path');
const { nodeModulesEs6IncludeRegExp } = require('../config/webpack.helpers');

const { ENVIRONMENT } = process.env;

module.exports = {
  core: {
    builder: "webpack5",
  },
  stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
  addons: [
    '@storybook/preset-create-react-app',
    '@storybook/addon-links',
    '@storybook/addon-essentials',
    '@storybook/addon-postcss',
    '@storybook/addon-a11y',
  ],
  webpackFinal: async config => {
    const newConfig = { ...config };

    newConfig.module.rules[1].include = nodeModulesEs6IncludeRegExp;

    // Sass loader rules
    newConfig.module.rules.unshift(
      {
        test: /\.scss$/,
        include: path.resolve('src'),
        use: [
          require.resolve('style-loader'),
          {
            loader: 'css-loader',
            options: {
              importLoaders: 1,
              modules: {
                auto: true,
                localIdentName: '[name]__[local]--[hash:base64:5]',
              },
            },
          },
          {
            loader: 'sass-loader',
            options: {
              additionalData: '$use-cdn-fonts: false;'
            },
          },
        ],
      },
      {
        test: /\.(sa|sc|c)ss$/,
        include: file =>
          file.includes('node_modules/custom-module),
        sideEffects: true,
        use: ['style-loader', 'css-loader', 'sass-loader'],
      },
      {
        test: /\.(jpg|png|woff|woff2|eot|ttf|svg)$/,
        type: 'asset',
      },
    );

    // Return the altered config
    return newConfig;
  },
};

My .storybook/preview.js:

import '!style-loader!css-loader!sass-loader!../src/stylesheets/index.scss';

// process.env is populated by dotenv at the start of main.js
window.process = { env: process.env }

export const parameters = {
  actions: { argTypesRegex: "^on[A-Z].*" },
  controls: {
    matchers: {
      color: /(background|color)$/i,
      date: /Date$/,
    },
  },
}
ShaunEvening commented 1 year ago

Hey @FK78

errors like this occur when sass files are processed twice. This means that your webpack config already has sass rules in it before you prepend your custom rules in main.js

@storybook/preset-create-react-app uses the webpack config included in react-scripts, so if you are using react-scripts@2.x.x and up, your storybook will already have the scss, css modules, and postcss webpack settings that react-scripts provides.

This means that you have two options.

  1. Remove your custom rules in favour of react-scripts existing rules
  2. If you want to use your custom rules, you should filter out the existing rules with something like the following:
require('dotenv').config();

const path = require('path');
const { nodeModulesEs6IncludeRegExp } = require('../config/webpack.helpers');

const { ENVIRONMENT } = process.env;

module.exports = {
  core: {
    builder: "webpack5",
  },
  stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
  addons: [
    '@storybook/preset-create-react-app',
    '@storybook/addon-links',
    '@storybook/addon-essentials',
    '@storybook/addon-postcss',
    '@storybook/addon-a11y',
  ],
  webpackFinal: async config => {
    const newConfig = { ...config };

    newConfig.module.rules[1].include = nodeModulesEs6IncludeRegExp;

    // Remove existing scss rules
    const newRules = newConfig.module.rules.filter(({ test }) => !test.test('test.scss'));

    // Sass loader rules
    newRules.unshift(
      {
        test: /\.scss$/,
        include: path.resolve('src'),
        use: [
          require.resolve('style-loader'),
          {
            loader: 'css-loader',
            options: {
              importLoaders: 1,
              modules: {
                auto: true,
                localIdentName: '[name]__[local]--[hash:base64:5]',
              },
            },
          },
          {
            loader: 'sass-loader',
            options: {
              additionalData: '$use-cdn-fonts: false;'
            },
          },
        ],
      },
      {
        test: /\.(sa|sc|c)ss$/,
        include: file =>
          file.includes('node_modules/custom-module),
        sideEffects: true,
        use: ['style-loader', 'css-loader', 'sass-loader'],
      },
      {
        test: /\.(jpg|png|woff|woff2|eot|ttf|svg)$/,
        type: 'asset',
      },
    );

    // Set the config rules to our new rule set
    newConfig.module.rules = newRules

    // Return the altered config
    return newConfig;
  },
};

Additionally, you can also remove @storybook/preset-postcss as create react app has this configured for you already.

With all of that configured you can remove the inline loaders to import your scss file in preview.js like so:

-import '!style-loader!css-loader!sass-loader!../src/stylesheets/index.scss';
+import '../src/stylesheets/index.scss';

// process.env is populated by dotenv at the start of main.js
window.process = { env: process.env }

export const parameters = {
  actions: { argTypesRegex: "^on[A-Z].*" },
  controls: {
    matchers: {
      color: /(background|color)$/i,
      date: /Date$/,
    },
  },
}

EDIT: I always mix up .match and .test 😅

FK78 commented 1 year ago

Thanks for the reply @Integrayshaun

Here is the webpack.config.js file:

      // Gather stylesheets from src
      {
        test: /\.(css|scss)$/,
        include: paths.appSrc,
        use: [
          isDev ? 'style-loader' : MiniCssExtractPlugin.loader,
          {
            loader: 'css-loader',
            options: {
              modules: {
                auto: true,
                localIdentName: isDev
                  ? '[dirname]_[name]_[local]-[hash:base64:5]'
                  : '[shortcamelname]_[local]-[hash:base64:5]', // This saves nearly 2kB across gzipped main.js and main.css
              },
            },
          },
          'postcss-loader',
          {
            loader: 'sass-loader',
            options: {
              additionalData: `$use-cdn-fonts: ${
                ['prod', 'production', 'shadow'].includes(ENVIRONMENT)
                  ? 'true'
                  : 'false'
              };`,
            },
          },
        ],
      },
      // Gather scss files from within node_modules (specifically required for the custom package)
      {
        test: /\.(sa|sc|c)ss$/,
        include: sassFile =>
          sassFile.includes('node_modules/custom-package'),
        sideEffects: true,
        use: [
          isDev ? 'style-loader' : MiniCssExtractPlugin.loader,
          'css-loader',
          'sass-loader',
        ],
      },
      // Images are copied or inlined based on the file size
      {
        test: /\.(jpg|jpeg|png|gif)$/,
        type: 'asset',
        parser: {
          dataUrlCondition: {
            maxSize: 10 * 1024, // 10kb
          },
        },
      },
      // Other assets
      {
        test: /\.svg$/,
        type: 'asset/resource',
      },

Removing the custom rules throws some new errors:

Error: => Webpack failed, learn more with --debug-webpack
ERR! Module build failed (from ./node_modules/@svgr/webpack/lib/index.js):
ERR! SyntaxError: unknown file: Namespace tags are not supported by default. React's JSX doesn't support namespace tags. You can set `throwIfNamespace: false` to bypass this warning.
Module not found: Error: Can't resolve './fonts/fonts/HeroicCondensedNumerals-Normal.woff' in '/src/stylesheets'

Fixed the above by removing an extra /fonts/.

I tried option 2 as well, but was hit with:

TypeError: test.match is not a function

It seems that before upgrading to Node 18 the react-scripts module was failing to load (on purpose maybe?) which meant everything worked as intended.

ShaunEvening commented 1 year ago

@FK78 Apologies, I meant test.test('test.scss') not test.match('test.scss')

Okay, so if you have a custom webpack configuration like this you're best to merge it with Storybook's config

FK78 commented 1 year ago

@Integrayshaun

test.test throws the same error:

ERR! TypeError: test is not a function

Maybe I'm missing an import or dependency?

ShaunEvening commented 1 year ago

ah, it's likely that some of the tests are string patterns instead of regex. Try using a function in the filter like this:

const isRuleForSCSS = (rule) =>
  typeof rule !== "string" &&
  rule.test instanceof RegExp &&
  (rule.test.test("test.scss") || rule.test.test("test.sass"));
FK78 commented 1 year ago

ah, it's likely that some of the tests are string patterns instead of regex. Try using a function in the filter like this:

const isRuleForSCSS = (rule) =>
  typeof rule !== "string" &&
  rule.test instanceof RegExp &&
  (rule.test.test("test.scss") || rule.test.test("test.sass"));

Where am I adding this? Could you update this example to show how its going to be used? Thank you.

FK78 commented 1 year ago

Hey @Integrayshaun are you still able to help with the above?

ShaunEvening commented 1 year ago

@FK78 use the isRuleForSCSS function to filter out scss rules

require('dotenv').config();

const path = require('path');
const { nodeModulesEs6IncludeRegExp } = require('../config/webpack.helpers');

const { ENVIRONMENT } = process.env;

const isRuleForSCSS = (rule) =>
  typeof rule !== "string" &&
  rule.test instanceof RegExp &&
  (rule.test.test("test.scss") || rule.test.test("test.sass"));

module.exports = {
  core: {
    builder: "webpack5",
  },
  stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
  addons: [
    '@storybook/preset-create-react-app',
    '@storybook/addon-links',
    '@storybook/addon-essentials',
    '@storybook/addon-postcss',
    '@storybook/addon-a11y',
  ],
  webpackFinal: async config => {
    const newConfig = { ...config };

    newConfig.module.rules[1].include = nodeModulesEs6IncludeRegExp;

    // Remove existing scss rules
    const newRules = newConfig.module.rules.filter(isRuleForSCSS);

    // Sass loader rules
    newRules.unshift(
      {
        test: /\.scss$/,
        include: path.resolve('src'),
        use: [
          require.resolve('style-loader'),
          {
            loader: 'css-loader',
            options: {
              importLoaders: 1,
              modules: {
                auto: true,
                localIdentName: '[name]__[local]--[hash:base64:5]',
              },
            },
          },
          {
            loader: 'sass-loader',
            options: {
              additionalData: '$use-cdn-fonts: false;'
            },
          },
        ],
      },
      {
        test: /\.(sa|sc|c)ss$/,
        include: file =>
          file.includes('node_modules/custom-module),
        sideEffects: true,
        use: ['style-loader', 'css-loader', 'sass-loader'],
      },
      {
        test: /\.(jpg|png|woff|woff2|eot|ttf|svg)$/,
        type: 'asset',
      },
    );

    // Set the config rules to our new rule set
    newConfig.module.rules = newRules

    // Return the altered config
    return newConfig;
  },
};
FK78 commented 1 year ago

@FK78 use the isRuleForSCSS function to filter out scss rules

require('dotenv').config();

const path = require('path');
const { nodeModulesEs6IncludeRegExp } = require('../config/webpack.helpers');

const { ENVIRONMENT } = process.env;

const isRuleForSCSS = (rule) =>
  typeof rule !== "string" &&
  rule.test instanceof RegExp &&
  (rule.test.test("test.scss") || rule.test.test("test.sass"));

module.exports = {
  core: {
    builder: "webpack5",
  },
  stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
  addons: [
    '@storybook/preset-create-react-app',
    '@storybook/addon-links',
    '@storybook/addon-essentials',
    '@storybook/addon-postcss',
    '@storybook/addon-a11y',
  ],
  webpackFinal: async config => {
    const newConfig = { ...config };

    newConfig.module.rules[1].include = nodeModulesEs6IncludeRegExp;

    // Remove existing scss rules
    const newRules = newConfig.module.rules.filter(isRuleForSCSS);

    // Sass loader rules
    newRules.unshift(
      {
        test: /\.scss$/,
        include: path.resolve('src'),
        use: [
          require.resolve('style-loader'),
          {
            loader: 'css-loader',
            options: {
              importLoaders: 1,
              modules: {
                auto: true,
                localIdentName: '[name]__[local]--[hash:base64:5]',
              },
            },
          },
          {
            loader: 'sass-loader',
            options: {
              additionalData: '$use-cdn-fonts: false;'
            },
          },
        ],
      },
      {
        test: /\.(sa|sc|c)ss$/,
        include: file =>
          file.includes('node_modules/custom-module),
        sideEffects: true,
        use: ['style-loader', 'css-loader', 'sass-loader'],
      },
      {
        test: /\.(jpg|png|woff|woff2|eot|ttf|svg)$/,
        type: 'asset',
      },
    );

    // Set the config rules to our new rule set
    newConfig.module.rules = newRules

    // Return the altered config
    return newConfig;
  },
};

Getting back this error now:

ERROR in ./src/stylesheets/index.scss 1:0
Module parse failed: Unexpected character '@' (1:0)
You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders
> @import 'reset-css/reset.css';
| @import 'fonts/font-face';
|
 @ ./src/index.js 14:0-34
FK78 commented 1 year ago

The team has decided it was more work to maintain Storybook than the benefits it provided, therefore has been removed from our repository.

ShaunEvening commented 1 year ago

Hey there, @FK78 👋

I'm sorry to hear this! Storybook is an open-source project and I'm only able to give limited support as part of my work. I hope you can understand.

I do want to fully assure you, though, that Storybook is very much worth the time of maintenance, and that's only going to increase as we continue shipping more features for our devs.

I hope that you're able to revisit this when you have more bandwidth and solve the difficulty you're facing. Alternatively, please consider joining our community discord. There may well be community members who can offer you the support you need.

FK78 commented 1 year ago

Thank you @Integrayshaun, I completely understand, I hope when we have more time we'll be able to reimplement Storybook ❤️

ShaunEvening commented 1 year ago

@FK78 Sounds good, my friend! I'll be here if you need the help 😀

manhhung-fpt commented 7 months ago

Hey @FK78

errors like this occur when sass files are processed twice. This means that your webpack config already has sass rules in it before you prepend your custom rules in main.js

@storybook/preset-create-react-app uses the webpack config included in react-scripts, so if you are using react-scripts@2.x.x and up, your storybook will already have the scss, css modules, and postcss webpack settings that react-scripts provides.

This means that you have two options.

  1. Remove your custom rules in favour of react-scripts existing rules
  2. If you want to use your custom rules, you should filter out the existing rules with something like the following:
require('dotenv').config();

const path = require('path');
const { nodeModulesEs6IncludeRegExp } = require('../config/webpack.helpers');

const { ENVIRONMENT } = process.env;

module.exports = {
  core: {
    builder: "webpack5",
  },
  stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
  addons: [
    '@storybook/preset-create-react-app',
    '@storybook/addon-links',
    '@storybook/addon-essentials',
    '@storybook/addon-postcss',
    '@storybook/addon-a11y',
  ],
  webpackFinal: async config => {
    const newConfig = { ...config };

    newConfig.module.rules[1].include = nodeModulesEs6IncludeRegExp;

    // Remove existing scss rules
    const newRules = newConfig.module.rules.filter(({ test }) => !test.test('test.scss'));

    // Sass loader rules
    newRules.unshift(
      {
        test: /\.scss$/,
        include: path.resolve('src'),
        use: [
          require.resolve('style-loader'),
          {
            loader: 'css-loader',
            options: {
              importLoaders: 1,
              modules: {
                auto: true,
                localIdentName: '[name]__[local]--[hash:base64:5]',
              },
            },
          },
          {
            loader: 'sass-loader',
            options: {
              additionalData: '$use-cdn-fonts: false;'
            },
          },
        ],
      },
      {
        test: /\.(sa|sc|c)ss$/,
        include: file =>
          file.includes('node_modules/custom-module),
        sideEffects: true,
        use: ['style-loader', 'css-loader', 'sass-loader'],
      },
      {
        test: /\.(jpg|png|woff|woff2|eot|ttf|svg)$/,
        type: 'asset',
      },
    );

    // Set the config rules to our new rule set
    newConfig.module.rules = newRules

    // Return the altered config
    return newConfig;
  },
};

Additionally, you can also remove @storybook/preset-postcss as create react app has this configured for you already.

With all of that configured you can remove the inline loaders to import your scss file in preview.js like so:

-import '!style-loader!css-loader!sass-loader!../src/stylesheets/index.scss';
+import '../src/stylesheets/index.scss';

// process.env is populated by dotenv at the start of main.js
window.process = { env: process.env }

export const parameters = {
  actions: { argTypesRegex: "^on[A-Z].*" },
  controls: {
    matchers: {
      color: /(background|color)$/i,
      date: /Date$/,
    },
  },
}

EDIT: I always mix up .match and .test 😅

Thank you i fixed with this one config.module.rules = config.module.rules?.filter( (rule) => rule && rule.test && rule.test.toString() !== '/\\.css$/' )