🫥
Next.js + Storybook(Webpack5) + TypeScriptでsvgファイルを表示する
環境
技術 | バージョン |
---|---|
React.js | ^18 |
Next.js | 14.0.3 |
Storybook | ^7.6.6 |
svgr | ^8.1.0 |
Webpack @ Storybook | 5 |
問題
Storybookを起動すると、ビルドは成功するが、Failed to execute 'createElement' on 'Document': The tag name provided ('static/media/public/images/icons/check.svg') is not a valid name.
のエラーが出てしまう。
前提
アイコンを表示するコンポーネントを作成しています。
// Iconコンポーネント
import Check from '/public/images/icons/check.svg';
const ICONS = { Check };
type IconName = keyof typeof ICONS;
type Size = 16 | 24 | 32 | 64;
type Props = {
name: IconName;
size: Size;
};
export default function Icon({ name, size }: Props) {
const Icon = ICONS[name];
return <Icon height={size} width={size}></Icon>;
}
アイコン画像は、Next.jsプロジェクトのルートディレクトリのpublic
ディレクトリ内に作成しています。
Next.js側の設定では、Webpackにsvgrの設定を行っています。
また、コンポーネント内で画像をimport
したときの型定義設定を無効にしています。
(そのため、svgファイルをimportした場合の独自の型定義を作成していますが、ここでは割愛させていただきます)
/** @type {import('next').NextConfig} */
const nextConfig = {
webpack: (config) => {
config.module.rules.push({
test: /\.svg$/,
use: [
{
loader: '@svgr/webpack',
options: {},
},
],
});
return config;
},
images: {
disableStaticImages: true,
},
};
export default nextConfig;
Storybookの設定は以下になります。
// .storybook/main.ts
import type { StorybookConfig } from '@storybook/nextjs';
import path from 'path';
const config: StorybookConfig = {
stories: ['../src/**/*.stories.(tsx)'],
addons: [
'@storybook/addon-links',
'@storybook/addon-essentials',
'@storybook/addon-onboarding',
'@storybook/addon-interactions',
],
framework: {
name: '@storybook/nextjs',
options: {},
},
staticDirs: ['../public'],
docs: {
autodocs: 'tag',
},
webpackFinal: async (config) => {
if (config.resolve) {
config.resolve.alias = {
...config.resolve.alias,
'~': [path.resolve(__dirname, '../src/')],
};
}
if (config.module) {
config.module.rules = [
...(config.module.rules || []),
{
test: /\.svg$/i,
issuer: /\.tsx?$/,
use: [
{
loader: '@svgr/webpack',
options: {},
},
],
},
];
}
return config;
},
};
export default config;
原因
公式において、上記のコードでsvg用のloader
を設定しています。
// code/builders/builder-webpack5/src/preview/base-webpack.config.ts
// (省略...)
{
test: /\.svg|ico|jpg|jpeg|png|apng|gif|eot|otf|webp|ttf|woff|woff2|cur|ani|pdf)(\?.*)?$/,
type: 'asset/resource',
generator: {
filename: isProd
? 'static/media/[name].[contenthash:8][ext]'
: 'static/media/[path][name][ext]',
},
},
// (省略...)
これによると、Webpack5のAsset Modulesである asset/resource
を使っています。
こちらの読み込みを優先してしまい、パスが正しい読み込みをできておらず、エラーが出ているとのことでした。
解決
上記のAsset Modulesを使った読み込みを外せば良さそうです。
ということで、svgファイルを読み込むloader
を持つルールに、svgを除外する設定を行います。
(変更部分だけ記載します)
// .storybook/main.ts
// (省略...)
const config: StorybookConfig = {
// (省略...)
webpackFinal: async (config) => {
// (省略...)
if (config.module) {
const newRule = config.module.rules?.map((rule) => {
if (
rule &&
typeof rule === 'object' &&
rule.test?.toString().includes('svg')
) {
return { ...rule, exclude: /\.svg$/ };
}
return rule;
});
config.module.rules = [
...(newRule || []),
{
test: /\.svg$/i,
issuer: /\.tsx?$/,
use: [
{
loader: '@svgr/webpack',
options: {},
},
],
},
];
}
// (省略...)
},
};
// (省略...)
上記設定を行うことで、無事Storybookでアイコンコンポーネントが表示できました。
参考
こちらの記事を参考にさせていただきました。
転載元
Discussion