Debugging Storybook Angular Customisation

Posted on October 26, 2019

Out of the box the support for Angular under Storybook is great. However, depending on how customised your setup is, you’ll probably need to harmonise your Angular setup with your Storybook setup.

Typescript Paths

Because we have customised tsconfig.json roots in order to clean up our import paths, we first need to make sure that Storybook’s Webpack uses the the TsconfigPathsPlugin.

const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');

module.exports = async ({ config, mode }) => {
    config.resolve.plugins = [new TsconfigPathsPlugin({})];
    return config
};

MapBox and Node core libraries

Because we use MapBox we get the following error

ERROR in ./node_modules/jsonlint-lines/lib/jsonlint.js
Module not found: Error: Can't resolve 'fs' in '/home/nigel/flyfreely-workspace/flyfreely-portal-ui/node_modules/jsonlint-lines/lib'
 @ ./node_modules/jsonlint-lines/lib/jsonlint.js 692:17-30
 @ ./node_modules/@mapbox/geojsonhint/lib/index.js
 @ ./node_modules/@mapbox/mapbox-gl-draw/src/api.js
 @ ./node_modules/@mapbox/mapbox-gl-draw/index.js
 @ ./src/flyfreely/modules/map/flyfreely-map/flyfreely-map.component.ts
 @ ./src/flyfreely/modules/map/map.module.ts
 @ ./src/flyfreely/modules/map/map.stories.ts
 @ ./src/flyfreely sync \.stories\.ts$
 @ ./.storybook/config.js

To resolve this we can get Webpack to provide an empty object to fulfil the requirement.

const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');

module.exports = async ({ config, mode }) => {
    config.resolve.plugins = [new TsconfigPathsPlugin({})];
    config.node = { fs: 'empty' };
    return config
};

Stylesheets

Next, we need to pull in the site-wide stylesheets, which are expected to be available in the window where the component is being drawn. To achieve this we import the stylesheet into the .storybook/config.js file:

import { configure, addParameters } from '@storybook/angular';

import '../src/stylesheets/styles.scss';

addParameters({
    options: {
      hierarchyRootSeparator: /\|/,
    },
  });

function loadStories() {
    // require('../stories/index.ts');
    const req = require.context('../src', true, /\.stories\.ts$/);
    req.keys().forEach(filename => req(filename));
}

configure(loadStories, module);

We started by trying the example provided in the Storybook Customer Webpack Config docs.

const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
const path = require('path');

module.exports = async ({ config, mode }) => {
    config.resolve.plugins = [new TsconfigPathsPlugin({})];
    config.node = { fs: 'empty' };

    config.module.rules.push({
        test: /\.scss$/,
        use: ['style-loader', 'css-loader', 'sass-loader'],
        include: path.resolve(__dirname, '../src'),
    });

    return config;
};

However, this results in the following error at startup.

ERROR in ./src/stylesheets/styles.scss
Module build failed (from ./node_modules/sass-loader/dist/cjs.js):
SassError: Invalid CSS after "v": expected 1 selector or at-rule, was "var content = requi"
        on line 1 of /home/nigel/flyfreely-workspace/flyfreely-portal-ui/src/stylesheets/styles.scss
>> var content = require("!!../../node_modules/css-loader/dist/cjs.js!../../nod
   ^

The solution was to use the MiniCssExtractPlugin. This has two parts: a module rule, and a plugin.

const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = async ({ config, mode }) => {
    config.resolve.plugins = [new TsconfigPathsPlugin({})];
    config.node = { fs: 'empty' };

    config.module.rules.push({
        test: /\.scss$/,
        use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader'],
        include: path.resolve(__dirname, '../src'),
    });
    config.plugins.push(new MiniCssExtractPlugin({ filename: '[name].css' }))

    return config;
};

Conclusion

You need to think about Storybook as a separate application environment, and while out of the box it might behave a lot like it is the same as your Angular application, there are subtleties at the edges that can catch you out. All completely solvable, but it requires a deeper understanding of the frameworks that bring the whole show together (i.e., webpack, babel, etc).