In this post, I will introduce how do I setup my development environment for NextJs. My development environments include:
cypress // For component testing
cypress-react-unit-test
storybook // Quickly view react components
tailwindcss // CSS framework
typescript // Provide type safety
In this post, I will assume you have NodeJs and yarn installed in your global environment. Also, I am assuming you know why we need those dependencies and won't explain much about them.
TLDR
Just checkout the repository: https://github.com/cuichenli/nextjs-typescript-cypress-storybook-tailwind
Let's start with NextJs
To start with, let's install create-next-app
via yarn
so we can use it to setup some basic stuff.
yarn global add create-next-app
create-next-app
After you type the above commands in your terminal, create-next-app
will ask you the name of the project, I will use my-app
here.
Once you are done with the questions, you can run cd
to go into the newly created project.
Typescript
There might be some other typescript templates out there, but I chose to do it myself, as I would like to see what are the things we need to do. So, to start with,
yarn add --dev @types/react typescript @types/node
Next we should convert the generated _app.js
and index.js
in pages
directory to tsx
file. For _app.js
, simple rename it, for index.js
, rename it and remove the imported css
(you may need some other configurations to use those import, I will not introduce it here) and simplify the index.js
file like this:
import React from "react";
export default function Home() {
return <p>hello</p>;
}
Once the files are renamed, you can run yarn dev
to start the NextJs
development server. NextJs
will kindly help you to generate one simple tsconfig.json
and one next-env.d.ts
file as it detected you are using TypeScript
.
BTW, in this step, I also moved pages
into src
directory. This is optional of course.
StoryBook
I started with add storybook
as global command here, and then use the recommend sb init
to initialize the files
yarn global add storybook
yarn sb init
Once the commands finished, you will see two new directories
-
.storybook
: This one contains the main configurations for StoryBook, it is created in the root directory. -
stories
: This one stores the stories you would like to use. StoryBook will generate a bunch of example stories for you, you can just delete them. In my case, this directory is generated insrc
directory. And I manually moved it out to root directory.
Configure StoryBook
The generated configurations files .storybook/main.js
contains the main configurations for your storybook. In my case, since I moved the generated stories
directory, I need to change the entry stories
module.exports = {
// Update stories to where you would like to store them.
stories: ["../stories/**/*.stories.mdx", "../stories/**/*.stories.tsx"],
addons: ["@storybook/addon-links", "@storybook/addon-essentials"],
};
Try StoryBook
I added one simple component to try if the StoryBook is configured correctly.
// src/components/text-input.tsx
import React from "react";
export const TextInputComponent: React.FunctionComponent = () => {
return (
<div>
<input placeholder="type"></input>
</div>
);
};
// ./stories/text-input.stories.tsx
import React from "react";
import { TextInputComponent } from "../src/components/text-input";
export default {
title: "story/text-input",
};
export const Primary: React.FunctionComponent = () => <TextInputComponent />;
Once the above files are added, run
yarn run storybook
and you will see the newly created component.
TailwindCSS
You will soon realize the component is a little bit boring - and it is time to add some style on it to make it a little bit better. So the next step is to configure TailwindCSS.
yarn add tailwindcss
yarn tailwindcss init -p
The above two commands will install tailwind and generate the default configurations for you.
You will see two newly added configuration files - postcss.config.js
and tailwind.config.js
.
Following the official guidance of tailwind, I added two plugins under postcss.config.js
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
Note: You must use the interoperable object-based format based on the NextJs
document (you can find this note at the bottom).
Then add tailwind into styles/globals.css
@tailwind base;
@tailwind components;
@tailwind utilities;
Add style to the component
// ./src/components/text-input.tsx
import React from "react";
export const TextInputComponent: React.FunctionComponent = () => {
return (
<div>
<input
placeholder="type"
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
></input>
</div>
);
};
And use it in your main page
// ./src/pages/index.tsx
import { Fragment } from "react";
import { TextInputComponent } from "../components/text-input";
export default function Home() {
return (
<Fragment>
<p>Hello</p>
<TextInputComponent />
</Fragment>
);
}
Then you can start your NextJs
server to view it on your main page.
What about StoryBook
Then you start your StoryBook server, and found - the style is not applied. Why? My understanding is that StoryBook
does not know we are using tailwind
at all - while NextJs
knows it because it is imported in the _app.tsx
file. So our next step is to let StoryBook
be aware of this as well. To achieve this, we should update the .storybook/preview.js
file
import "../styles/globals.css";
export const parameters = {
actions: { argTypesRegex: "^on[A-Z].*" },
};
Then you start your StoryBook
server again, still, you find there is no style (or even worse, you may see error message - I can not recall exactly you will see now.) Basically the reason is StoryBook
's webpack
component is not using postcss-loader
- while NextJs
has builtin support for it. To change it, we should update the .storybook/main.js
file
module.exports = {
stories: ["../stories/**/*.stories.mdx", "../stories/**/*.stories.tsx"],
addons: ["@storybook/addon-links", "@storybook/addon-essentials"],
webpackFinal: (config) => {
return {
...config,
module: {
...config.module,
rules: [
// Filter out the default css loader
...config.module.rules.filter((rule) => /\.css$/ !== rule.test),
{
test: /\.css/,
use: [
{
loader: "postcss-loader",
options: {
plugins: [require("tailwindcss"), require("autoprefixer")],
postcssOptions: {
ident: "postcss",
sourceMap: true,
},
},
},
],
},
],
},
};
},
};
Note1 We are filtering out one built in css
rule here, as the builtin one is not applicable for postcss
.
Note2 When speicfy plugins
, you can not simply put a string here, it has to be required object.
Once the configurations are placed, you can start your storybook server again and see your styled component.
Cypress and Cypress-React-Unit-Test
To test the newly added component, we can use cypress
.
yarn add --dev cypress cypress-react-unit-test
Although in the document of cypress-react-unit-test
it says we can initialize cypress
with cypress-react-unit-test init
command, but I failed to achive it. So I will do it manually. To start, start cypress
server once.
yarn cypress open
The above command will initialize your basic cypress configurations - it creates one cypress.json
file and one cypress
directory.
Then we can follow the document in cypress-react-unit-test
to setup it
// cypress.json
{
"experimentalComponentTesting": true
}
// cypress/support/index.js
import "./commands";
require("cypress-react-unit-test/support");
// cypress/tsconfig.json
{
"extends": "../tsconfig.json",
"include": ["./**/*.ts*"],
"compilerOptions": {
"baseUrl": ".",
"jsx": "react",
"types": ["cypress"]
}
}
// ./tsconfig.json
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": false,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"baseUrl": "src" // <- Add this line
},
"include": ["next-env.d.ts", "src"],
"exclude": ["node_modules", ".storybook"]
}
// ./cypress/global.d.ts
/// <reference types="cypress" />
// ./cypress/plugins/index.js
/// <reference types="cypress" />
module.exports = (on, config) => {
return config;
};
Note We need to add the baseUrl
line to satisfy cypress, otherwise it will complain (sorry I can not recall what it was complaining about exactly).
After that, you can start to write a simple cypress test
// cypress/component/text-input.spec.tsx
import React from "react";
import { mount } from "cypress-react-unit-test";
import { TextInputComponent } from "../../src/components/text-input";
import "../../styles/globals.css";
describe("HelloWorld component", () => {
it("works", () => {
mount(<TextInputComponent />);
});
});
Then you run yarn cypress open
and click the test you just added - however, it wont make it. You will see it is complaining that there is no loader for this file. Guess cypress
or cypress-react-unit-test
does not have builtin support for Typescript - so we need to add the webpack loader manually. In addition, you also want to set the loader for css
, as cypress does not support it by default either.
/// <reference types="cypress" />
const webpackPreprocessor = require("@cypress/webpack-preprocessor");
const commonOptions = require("../../webpack.config");
commonOptions.resolve = {
extensions: [".tsx", ".ts", ".js", ".css"],
};
commonOptions.module.rules.push({
test: /tsx$/,
use: [
{
loader: "babel-loader",
options: {
presets: ["@babel/preset-react", "@babel/preset-typescript"],
},
},
],
});
commonOptions.module.rules.push({
test: /css$/,
use: [
"style-loader",
"css-loader",
{
loader: "postcss-loader",
options: {
plugins: [require("tailwindcss"), require("autoprefixer")],
},
},
],
});
module.exports = (on, config) => {
// `on` is used to hook into various events Cypress emits
// `config` is the resolved Cypress config
const options = {
// send in the options from your webpack.config.js, so it works the same
// as your app's code
webpackOptions: commonOptions,
watchOptions: {},
};
on("file:preprocessor", webpackPreprocessor(options));
return config;
};
Forgive the bad naming, I am really lazy to change it atm.
You may notice that the loader used here is different to the ones we used in StoryBook
, because if we use the same configuration as the ones in StoryBook
, it wont work. I am still unsure why this is happening.
OK, with all the configuration above, you can now use cypress without any issue.
Main Take Away
WebPack
is really important.
Top comments (0)