投稿日:2023/03/02 最終更新日:2023/03/03
個人的なNext.js×ESLint×Prettier×Storybook×Styled-componentsの環境構築
環境構築
プロジェクトの環境作成
npx create-next-app@latest . --ts
styled-componentをインストール
npm install styled-components
npm install --save-dev @types/styled-components
_app.tsxを編集
import { AppProps } from "next/app";
import Head from "next/head";
import styled, { createGlobalStyle } from "styled-components";
const GlobalStyle = createGlobalStyle`
html {box-sizing:border-box;-webkit-text-size-adjust:100%;word-break:normal;-moz-tab-size:4;tab-size:4;font-size:62.5%;}*,:after,:before{background-repeat:no-repeat;box-sizing:inherit}:after,:before{text-decoration:inherit;vertical-align:inherit}*{padding:0;margin:0}hr{overflow:visible;height:0;color:inherit}details,main{display:block}summary{display:list-item}small{font-size:80%}[hidden]{display:none}abbr[title]{border-bottom:0;text-decoration:underline;text-decoration:underline dotted}a{background-color:transparent;color:inherit;text-decoration:none;}a:active,a:hover{outline-width:0}code,kbd,pre,samp{font-family:monospace}pre{font-size:1em}b,strong{font-weight:bolder}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{border-color:inherit;text-indent:0}iframe{border-style:none}input{border-radius:0}[type='number']::-webkit-inner-spin-button,[type='number']::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}[type='search']::-webkit-search-decoration{-webkit-appearance:none}textarea{overflow:auto;resize:vertical}button,input,optgroup,select,textarea{font:inherit}optgroup{font-weight:700}button{overflow:visible}button,select{text-transform:none}[role=button],[type=button],[type=reset],[type=submit],button{cursor:pointer}[type='button']::-moz-focus-inner,[type='reset']::-moz-focus-inner,[type='submit']::-moz-focus-inner,button::-moz-focus-inner{border-style:none;padding:0}[type='button']::-moz-focus-inner,[type='reset']::-moz-focus-inner,[type='submit']::-moz-focus-inner,button:-moz-focusring{outline:1px dotted ButtonText}[type=reset],[type=submit],button,html [type='button']{-webkit-appearance:button}button,input,select,textarea{background-color:transparent;border-style:none}a:focus,button:focus,input:focus,select:focus,textarea:focus{outline-width:0}select{-moz-appearance:none;-webkit-appearance:none}select::-ms-expand{display:none}select::-ms-value{color:currentColor}legend{border:0;color:inherit;display:table;white-space:normal;max-width:100%}::-webkit-file-upload-button{-webkit-appearance:button;color:inherit;font:inherit}[disabled]{cursor:default}img{border-style:none}progress{vertical-align:baseline}[aria-busy=true]{cursor:progress}[aria-controls]{cursor:pointer}[aria-disabled=true]{cursor:default}li{list-style:none;}img{width:100%;vertical-align:bottom;}
`;
const MyApp = ({ Component, pageProps }: AppProps) => {
return (
<>
<Head>
<meta key="charset" name="charset" content="utf-8" />
<meta key="viewport" name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, maximum-scale=5" />
<meta property="og:locale" content="ja_JP" />
<meta property="og:type" content="website" />
</Head>
<GlobalStyle />
<Component {...pageProps} />
</>
);
};
export default MyApp;
_documents.tsxを編集
import Document, { DocumentContext, DocumentInitialProps } from "next/document";
import { ServerStyleSheet } from "styled-components";
export default class MyDocument extends Document {
static async getInitialProps(getProps: DocumentContext): Promise {
const serverStyleSheet = new ServerStyleSheet();
const originalRenderPage = getProps.renderPage;
try {
getProps.renderPage = () =>
originalRenderPage({
enhanceApp: (App) => (props) => serverStyleSheet.collectStyles( ),
});
const initialProps = await Document.getInitialProps(getProps);
return {
...initialProps,
styles: [
<>
{initialProps.styles}
{serverStyleSheet.getStyleElement()}
</>,
],
};
} finally {
serverStyleSheet.seal();
}
}
}
ESLint/Prettierのインストールと設定
npm install --save-dev prettier eslint typescript-eslint @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint-config-prettier eslint-plugin-prettier eslint-plugin-react eslint-plugin-react-hooks eslint-plugin-import
npm i --save-dev eslint-config-next
.eslintrc.jsonを作成
{
"extends": [
"next",
"next/core-web-vitals",
"eslint:recommended",
"plugin:prettier/recommended",
"plugin:react/recommended",
"plugin:react-hooks/recommended",
"plugin:@typescript-eslint/recommended",
"plugin:import/recommended",
"plugin:import/typescript"
],
"rules": {
"react/react-in-jsx-scope": "off",
"import/order": [2, { "alphabetize": { "order": "asc" } }],
"prettier/prettier": [
"error",
{
"trailingComma": "all",
"endOfLine": "lf",
"semi": true,
"singleQuote": false,
"printWidth": 140,
"tabWidth": 2
}
]
}
}
package.jsonの編集
"scripts": {
"dev": "next dev",
"build": "next build && next export",
"start": "next start",
"lint": "next lint --dir src",
"format": "next lint --fix --dir src"
},
Storybookの導入
npx sb init
Storybookの設定
npm install --save-dev @storybook/addon-postcss tsconfig-paths-webpack-plugin @babel/plugin-proposal-class-properties @babel/plugin-proposal-class-properties @babel/plugin-proposal-private-methods @babel/plugin-proposal-private-property-in-object tsconfig-paths-webpack-plugin @mdx-js/react
package.jsonの設定修正
"storybook": "NODE_OPTIONS='--openssl-legacy-provider' start-storybook -p 6006",
.storybookにpublicフォルダーを作成
mkdir .storybook/public
.storybookのmain.jsを修正
const TsconfigPathsPlugin = require("tsconfig-paths-webpack-plugin");
const path = require("path");
module.exports = {
stories: [
"../src/**/*.stories.mdx",
"../src/**/*.stories.@(js|jsx|ts|tsx)"
],
addons: [
"@storybook/addon-links",
"@storybook/addon-essentials",
"@storybook/addon-postcss",
],
staticDirs: ["public"],
babel: async options => ({
...options,
plugins: [
"@babel/plugin-proposal-class-properties",
"@babel/plugin-proposal-private-methods",
"@babel/plugin-proposal-private-property-in-object",
],
}),
webpackFinal: async (config) => {
config.resolve.plugins = [
new TsconfigPathsPlugin({
configFile: path.resolve(__dirname, "../tsconfig.json")
}),
];
return config
},
}
.storybookのpreview.jsを修正
import { addDecorator } from "@storybook/react";
import { createGlobalStyle } from "styled-components";
import * as NextImage from "next/image";
export const parameters = {
actions: { argTypesRegex: '^on[A-Z].*' },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
};
export const GlobalStyle = createGlobalStyle`
html,
body,
textarea {
padding: 0;
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
}
* {
box-sizing: border-box;
}
a {
text-decoration: none;
transition: .25s;
color: #000000;
}
`;
addDecorator((story) => (
<>
{story()}
</>
));
const OriginalNextImage = NextImage.default;
Object.defineProperty(NextImage, 'default', {
configurable: true,
value: (props) => typeof props.src === 'string' ? (
) : (
),
});
next.config.jsの編集
const urlPrefix = process.env.URL_PREFIX ? '/' + process.env.URL_PREFIX : ''
module.exports = {
assetPrefix: urlPrefix,
basePath: urlPrefix,
trailingSlash: true,
publicRuntimeConfig: { urlPrefix },
}
/src/utils/config.jsの作成
import getConfig from "next/config";
export function url(filename: string): string {
const { publicRuntimeConfig } = getConfig() as {
publicRuntimeConfig: { urlPrefix: string };
};
return publicRuntimeConfig.urlPrefix + "." + filename;
}