Closed37

Nx×Next.js×Storybook×Emotion環境を用意する

kobokobo

前提

Nxを使っているので、少しベースがある状態でスタート

ベース

main.js
const rootMain = require('../../../.storybook/main');

const path = require('path');

module.exports = {
  ...rootMain,

  core: { ...rootMain.core, builder: 'webpack5' },

  stories: [
    ...rootMain.stories,
    '../components/**/*.stories.mdx',
    '../components/**/*.stories.@(js|jsx|ts|tsx)',
  ],
  addons: [
    ...rootMain.addons,
    '@nrwl/react/plugins/storybook',

    'storybook-addon-swc',
    {
      name: 'storybook-addon-next',
      options: {
        nextConfigPath: path.resolve(__dirname, '../next.config.js'),
      },
    },
  ],
  webpackFinal: async (config, { configType }) => {
    // apply any global webpack configs that might have been specified in .storybook/main.js
    if (rootMain.webpackFinal) {
      config = await rootMain.webpackFinal(config, { configType });
    }

    // add your own webpack tweaks if needed

    return config;
  },
};

まずはこのまま起動
動くには動くけど、emotionは入ってないのでもちろんスタイルは当たらない

kobokobo

よくある要件だと思うので、Nxのプラグイン@nrwl/react/plugins/storybookが何かやってくれないかなーと思ってリポジトリを散策

https://github.com/nrwl/nx/blob/master/packages/react/plugins/storybook/index.ts

kobokobo

// Check whether the project .babelrc uses @emotion/babel-plugin. There's currently
// a Storybook issue (https://github.com/storybookjs/storybook/issues/13277) which apparently
// doesn't work with @emotion/* >= v11
// this is a workaround to fix that

んー
プロジェクトの.babelrcをみてemotionのbabel-pluginを使っているかどうか見ているよう。。

kobokobo

悲しい。Next.jsをswcにあげちゃいました。
.babelrcがプロジェクトルートにあると、Next.jsがbabelコンパイルしちゃうので困る。。

kobokobo

options.babelrcPathがあるのでもしかしたら.storybookにbabelrcおいたらいけるかも

kobokobo
.storybook/.babelrc
{
  "presets": [
    [
      "next/babel",
      {
        "preset-react": {
          "runtime": "automatic",
          "importSource": "@emotion/react"
        }
      }
    ]
  ],
  "plugins": ["@emotion/babel-plugin"]
}

読み込まれたっぽいけど、まだスタイルは当たらない

kobokobo

よくコードを読んでみると、emotionをimportしている部分でaliasを貼っているだけっぽいし、プラグインをstorybookのwebpackConfigで読み込む処理とかは書いてなさそう

kobokobo

こんな感じでemotion/babel-pluginが呼び出されているか見てみる

const rootMain = require('../../../.storybook/main');

const path = require('path');

module.exports = {
 ...
  babel: async (options) => {
    console.log(options);
    return {
      ...options,
    };
  },
...
};

結果

入ってるんですけど、、

{
  cacheDirectory: '...l',
  presets: [ [ 'next/babel', [Object] ] ],
  plugins: [ '@emotion/babel-plugin' ],
  babelrc: false,
  overrides: [
    { test: /\.(story|stories).*$/, plugins: [Array] },
    { test: /\.(mjs|jsx?)$/, plugins: [Array] }
  ]
}
kobokobo

css={}で呼び出している場合は@emotion/babel-preset-css-propが必要そう

babel: async () => {

}

の中で上書きする方法もあると思うけど、なぜか動かなかったので、webpackFinalの中でやった

  webpackFinal: async (config, { configType }) => {
    // apply any global webpack configs that might have been specified in .storybook/main.js
    if (rootMain.webpackFinal) {
      config = await rootMain.webpackFinal(config, { configType });
    }
    config.module.rules.push({
      test: /\.(ts|tsx)$/,
      loader: 'babel-loader',
      options: {
        presets: [
          ['react-app', { flow: false, typescript: true }],
          '@emotion/babel-preset-css-prop',
        ],
      },
    });
...
kobokobo

残念ながら、この辺りをやってくれるpluginはstorybookにはなさそうだったのでこれで妥協

kobokobo

上でEmotionは動くようになったけど、すでにあるStorybookファイルを噛ませてみると

React is not defined

が出てくる

kobokobo

Next.jsはv17?ぐらいのときからReactを暗黙的に挿入してくれている(何か、babelのプラグインがあったはず、、

swcの場合も入れてくれているので、Next.jsをしている分にはいらないのだが、StorybookはただReactを起動させてるだけだと思うのでこれも解消しないと

kobokobo

storybook-addon-swcでswcコンパイルしているみたいだし、Next.jsが使ってるswcコンパイラープラグインを噛ませればいけるのでは?

kobokobo

いや、Emotionをくっつける時に、webpackFinalでtsxのコンパイラーをbabel-loaderにしている時点でダメなのか?わからない。

kobokobo

一旦babel-loaderにreact importのやつを噛ませれないか検討

kobokobo
  1. Adding import React from 'react' to your component files.
  2. Adding a .babelrc that includes babel-plugin-react-require
kobokobo

コードに影響を及ぼしたくないので、2を試してみる

kobokobo

こんな感じでやったらいけた

    config.module.rules.push({
      test: /\.(ts|tsx)$/,
      loader: 'babel-loader',
      options: {
        presets: [
          ['react-app', { flow: false, typescript: true }],
          '@emotion/babel-preset-css-prop',
        ],
        plugins: ['react-require'],
      },
    });
kobokobo

よく見たら、めちゃくちゃデフォルトのローダーを無視したコードだな。

babel: async () => {
}

で動かないのは、デフォルトの挙動と競合したからか?
またどこかのタイミングで検証(swcに全部載せ替えるチャレンジもしてみたい(多分rust書かないといけない。。

kobokobo

いけたいけたと思ってたら、画像が表示されていなかった

storybook-addon-nextがやってくれると思ってたけど、設定が足りてなかったかも

kobokobo

<imgタグには変換してくれてそうなので、srcかな

kobokobo

相対パスで書かれている時は、Next.jsが起動していないと見れないのでは?

Next.jsを起動してみる
→ 表示されない

kobokobo

あー、、

basePathだ、、普段はimage-loaderを噛ませてbasePathがつくようにしているけど、image-loaderはnext.config.jsで設定していて、これが読み込まれてなさそう

kobokobo

なんならこの画像をうまくパースしてくれるものがほしくてこのaddonを使ってるまであるので、こうなってくると使う必要があまりなくなってくる。。

kobokobo

なので、一旦、storybook-addon-nextをなくしてみて実装してみる

kobokobo

前提

basePathがついているが、各Next/Imageを呼び出すコンポーネントでloaderを呼び出すのは保守性が下がりそうなので、customLoaderでくっつけている

Next/Imageをラップしたコンポーネントを作ると言う手もあったし、今となってはそっちの方が良かった気もするが

https://github.com/aiji42/next-image-loader

を使って、webpackのコンパイル中にNext/ImageをラップしたImageにすることで解消している

kobokobo

悲しいけど、一旦storybook-addon-nextをコメントアウト

main.js
const rootMain = require('../../../.storybook/main');

const path = require('path');

module.exports = {
  ...rootMain,

  core: { ...rootMain.core, builder: 'webpack5' },

  stories: [
    ...rootMain.stories,
    '../components/**/*.stories.mdx',
    '../components/**/*.stories.@(js|jsx|ts|tsx)',
  ],
  addons: [
    ...rootMain.addons,
    '@nrwl/react/plugins/storybook',

    'storybook-addon-swc',
    // ここをコメントアウト
    // {
    //   name: 'storybook-addon-next',
    //   options: {
    //     nextConfigPath: path.resolve(__dirname, '../next.config.js'),
    //   },
    // },
  ],
  webpackFinal: async (config, { configType }) => {
    // apply any global webpack configs that might have been specified in .storybook/main.js
    if (rootMain.webpackFinal) {
      config = await rootMain.webpackFinal(config, { configType });
    }

    // add your own webpack tweaks if needed

    return config;
  },
};
kobokobo

webpackFinalで

  • next-image-loaderライブラリのNext/Imageを差し込み(aliasを差し替え)
  • entryにimage-loaderのconfigファイルを追加し、読み込ませる
main.js
const rootMain = require('../../../.storybook/main');

const path = require('path');

module.exports = {
  ...rootMain,

  core: { ...rootMain.core, builder: 'webpack5' },

  stories: [
    ...rootMain.stories,
    '../components/**/*.stories.mdx',
    '../components/**/*.stories.@(js|jsx|ts|tsx)',
  ],
  addons: [
    ...rootMain.addons,
    '@nrwl/react/plugins/storybook',

    'storybook-addon-swc',
    // ここをコメントアウト
    // {
    //   name: 'storybook-addon-next',
    //   options: {
    //     nextConfigPath: path.resolve(__dirname, '../next.config.js'),
    //   },
    // },
  ],
  webpackFinal: async (config, { configType }) => {
    // apply any global webpack configs that might have been specified in .storybook/main.js
    if (rootMain.webpackFinal) {
      config = await rootMain.webpackFinal(config, { configType });
    }

    // add your own webpack tweaks if needed
    config.resolve.alias['next/image'] = 'next-image-loader/build/image';
    config.resolve.alias['next/legacy/image'] =
      'next-image-loader/build/legacy/image';
    config.entry.push(path.resolve(__dirname, './image-loader.config.js'));

    return config;
  },
};
image-loader.config.js
import { imageLoader } from 'next-image-loader/build/image-loader';

// (resolverProps: { src: string; width: number; quality?: number }) => string
imageLoader.loader = ({ src, width, quality }) => {
  return `${
    http://localhost:4200/moving/_next/image?url=/moving${src}&w=${width}&q=${quality || 75}`;
};
kobokobo

あとはローカルだけ見てもらってもCI上とかで困るので、環境変数を流し込む必要がありそう

kobokobo

↑のissueにあるようにNX_の環境変数をzshrcとかに書けばいけるんだろうけど、一人一人の開発環境にそれを強制するのは厳しいので、他の解決策を考える

kobokobo

まぁ、CI上では環境変数はあると思うので、あまり気にせず、

image-loader.config.js
import { imageLoader } from 'next-image-loader/build/image-loader';

const baseUrl = process.env.NEXT_PUBLIC_SERVICE_HOST ?? 'http://localhost:4200';

// write self-define a custom loader
// (resolverProps: { src: string; width: number; quality?: number }) => string
imageLoader.loader = ({ src, width, quality }) => {
  return `${baseUrl}/moving/_next/image?url=/moving${src}&w=${width}&q=${
    quality || 75
  }`;
};

こんな感じで対応

このスクラップは2022/12/16にクローズされました