These days, I use NextJS for my front-end work, and I prefer to do it within a monorepo. This approach allows me to store all the front-ends of the product in the same repository along with back-end services and shared libraries. This setup enables me to reuse code and finish work faster. I will guide you through how to establish such a monorepo using Yarn Workspaces and NextJS. The source code and commands can be found here. Further, you could also use RadzionKit as a template for your project instead of going through the post.
Firstly, verify that you have the latest stable version of Yarn installed. In case you do not have Yarn installed, follow the instructions from their website.
yarn set version stable
Then, create a folder for your project and add a package.json
with the subsequent content.
{
"name": "radzionkit",
"packageManager": "yarn@3.6.1",
"engines": {
"yarn": "3.x"
},
"private": true,
"workspaces": ["./*"],
"devDependencies": {
"@typescript-eslint/eslint-plugin": "latest",
"@typescript-eslint/parser": "latest",
"eslint": "latest",
"husky": "latest",
"lint-staged": "latest",
"prettier": "latest",
"typescript": "latest"
},
"scripts": {
"postinstall": "husky install",
"format": "eslint --fix && prettier --write"
}
}
For this tutorial, we will use the name radzionkit
to maintain consistency with my GitHub Template. The packageManager
and engines
fields are optional; they can be employed to compel your team to utilize the same package manager. Our monorepo will not be published to NPM, so set private
to true
. We will incorporate a flat structure, thus you can consider everything in the root directory a potential Yarn workspace.
The devDependencies
incorporate typescript and libraries for linting and formatting. The "scripts" section includes a postinstall
script that installs Husky hooks, and a format
script that formats and lints the entire project.
Next, add a tsconfig.json
for TypeScript configuration.
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "ESNext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "ESNext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx"
},
"exclude": ["node_modules"],
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "types.d.ts"]
}
To customize Prettier, include a .prettierrc
file:
{
"singleQuote": true,
"semi": false,
"tabWidth": 2
}
It is optional to add a .prettierignore
file, which will utilize the same syntax as .gitignore
to exclude certain files from formatting.
.vscode
.yarn
.next
out
To execute Prettier and eslint upon commit, you can add a .lintstagedrc.json
file with instructions for the lint-staged
library:
{
"*.{js,ts,tsx,json}": ["yarn format"]
}
Below is a generic Eslint config for our monorepo:
{
"env": {
"browser": true,
"es2021": true
},
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"plugins": ["@typescript-eslint"],
"rules": {}
}
Let's include a .gitignore
file to eliminate unnecessary yarn files and node_modules
:
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
.DS_Store
node_modules
Subsequently, generate a new git repository with git init
:
git init
To format files during the pre-commit phase, we will use the husky
library. Initialize it with this command:
npx husky-init
For the pre-commit hook, we want to run lin-staged. So, modify the .husky/pre-commit
file:
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
yarn lint-staged
Before creating our first workspace, you must decide whether to use Yarn's plug'n'play feature or not. Plug'n'play is the default method of managing dependencies in the latest Yarn, but it might not work for certain packages that rely on the traditional method of having all packages in the same node_modules
directory and I found it to be quite troublesome. I believe the ecosystem is not quite ready for it yet, so it might be better to try it in a year. For now, we'll proceed with node_modules by adding the following line to the .yarnrc.yml
file:
nodeLinker: node-modules
Now, you can install dependencies with:
yarn
Our NextJS app requires a UI package with a components system. So, let's copy the ui folder from RadzionKit to our monorepo.
To establish a new NextJS project, we will use the create-next-app
command:
npx create-next-app@latest app
We won't use server components in this tutorial, so there is no need for the app router
.
Update the next.config.js
file to add support for styled-components, static export, and to transpile our shared UI package:
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
compiler: {
styledComponents: true,
},
output: "export",
transpilePackages: ["@radzionkit/ui"],
}
// eslint-disable-next-line no-undef
module.exports = nextConfig
Also, you should install styled-component
and next-sitemap
packages.
yarn add styled-components@^5.3.5 next-sitemap
yarn add --dev @types/styled-components@^5.1.25
To generate a sitemap, add a next-sitemap.config.js
file. I prefer to provide siteUrl from the environment variable - NEXT_PUBLIC_BASE_URL
/** @type {import('next-sitemap').IConfig} */
module.exports = {
siteUrl: process.env.NEXT_PUBLIC_BASE_URL,
generateRobotsTxt: true,
generateIndexSitemap: false,
outDir: "./out",
}
Also, update the build
command for the app
:
{
"scripts": {
"build": "next build && next-sitemap"
}
}
To use absolute imports within the app project, update the tsconfig.json
file with the compilerOptions
field:
{
"compilerOptions": {
"baseUrl": "."
}
}
For efficient use of local storage, we'll copy the state folder from RadzionKit. To understand more about the implementation, you can check this post.
We'll do the same thing with the ui folder that gives support for the dark and light mode along with the theme for styled-components. You can watch a video on this topic here.
We also need to update the _document.tsx
file to support styled-components and add basic meta tags. To learn more about generating icons for PWA, you can check this post.
import Document, {
Html,
Main,
NextScript,
DocumentContext,
Head,
} from "next/document"
import { ServerStyleSheet } from "styled-components"
import { MetaTags } from "@radzionkit/ui/metadata/MetaTags"
import { AppIconMetaTags } from "@radzionkit/ui/metadata/AppIconMetaTags"
class MyDocument extends Document {
static async getInitialProps(ctx: DocumentContext) {
const sheet = new ServerStyleSheet()
const originalRenderPage = ctx.renderPage
try {
ctx.renderPage = () =>
originalRenderPage({
enhanceApp: (App) => (props) =>
sheet.collectStyles(<App {...props} />), //gets the styles from all the components inside <App>
})
const initialProps = await Document.getInitialProps(ctx)
return {
...initialProps,
styles: (
<>
{initialProps.styles}
{sheet.getStyleElement()}
</>
),
}
} finally {
sheet.seal()
}
}
render() {
return (
<Html lang="en">
<Head>
<MetaTags
title="RadzionKit"
description="A React components system for faster development"
url={process.env.NEXT_PUBLIC_BASE_URL}
twitterId="@radzionc"
/>
<AppIconMetaTags />
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
)
}
}
export default MyDocument
We will be using the styled-component, so let's remove the styles
folder and modify the _app.tsx
file:
import type { AppProps } from "next/app"
import { GlobalStyle } from "@radzionkit/ui/ui/GlobalStyle"
import { ThemeProvider } from "ui/ThemeProvider"
import { Inter } from "next/font/google"
const inter = Inter({
subsets: ["latin"],
weight: ["400", "500", "600", "800"],
})
function MyApp({ Component, pageProps }: AppProps) {
return (
<ThemeProvider>
<GlobalStyle fontFamily={inter.style.fontFamily} />
<Component {...pageProps} />
</ThemeProvider>
)
}
export default MyApp
Finally, we can try to import one of the reusable components from the ui package to the index.tsx
page:
import { Center } from "@radzionkit/ui/ui/Center"
import { Button } from "@radzionkit/ui/ui/buttons/Button"
export default function Home() {
return (
<Center>
<Button size="xl">RadzionKit is Awesome!</Button>
</Center>
)
}
To deploy a static NextJS app to AWS S3 and CloudFront, you can follow this post.