This is an alpha, sneak peek of Monorepo Maestros. For this iteration, I'm getting all of my thoughts down. In the future, we'll have better information architecture, graphics, and other awesomeness. Your feedback is welcome!
Next.js
Using Next.js in a monorepo is a great way to give your Next.js applications even more superpowers.
If you prefer, you can clone the monorepo for Maestros (remember to star the repo while you're there!) and use the application found in apps/starter-nextjs
. Remove any apps you don't want and you're good to go!
In your apps
directory, use create-next-app
to create a new Next.js app:
cd apps
npx create-next-app@latest
cd apps
npx create-next-app@latest
cd apps
npx create-next-app@latest
cd apps
npx create-next-app@latest
Add a turbo.json
to your new app's directory, configured for Next.js:
{
"extends": ["//"],
"pipeline": {
"build": {
"outputs": ["./next/**", "!./next/cache/**"],
"dotEnv": [
".env.production.local",
".env.local",
".env.production",
".env"
],
"env": [
// Add your environment variables!
]
},
"dev": {
"persistent": true,
"dotEnv": [
".env.development.local",
".env.local",
".env.development",
".env"
]
},
"lint": {
"outputs": ["node_modules/.cache/.eslintcache"]
},
"lint:fix": {
"outputs": ["node_modules/.cache/.eslintcache"],
"cache": false
},
"format": {
"outputs": ["node_modules/.cache/.prettiercache"]
},
"format:fix": {
"outputs": ["node_modules/.cache/.prettiercache"],
"cache": false
},
"typecheck": {
"outputs": ["node_modules/.cache/tsbuildinfo.json"],
"outputMode": "errors-only"
}
}
}
{
"extends": ["//"],
"pipeline": {
"build": {
"outputs": ["./next/**", "!./next/cache/**"],
"dotEnv": [
".env.production.local",
".env.local",
".env.production",
".env"
],
"env": [
// Add your environment variables!
]
},
"dev": {
"persistent": true,
"dotEnv": [
".env.development.local",
".env.local",
".env.development",
".env"
]
},
"lint": {
"outputs": ["node_modules/.cache/.eslintcache"]
},
"lint:fix": {
"outputs": ["node_modules/.cache/.eslintcache"],
"cache": false
},
"format": {
"outputs": ["node_modules/.cache/.prettiercache"]
},
"format:fix": {
"outputs": ["node_modules/.cache/.prettiercache"],
"cache": false
},
"typecheck": {
"outputs": ["node_modules/.cache/tsbuildinfo.json"],
"outputMode": "errors-only"
}
}
}
Note: Please refer to the docs at turbo.build/repo/docs/reference/configuration for details on the keys seen here.
{
"extends": ["//"],
"pipeline": {
"build": {
"outputs": ["./next/**", "!./next/cache/**"],
"dotEnv": [
".env.production.local",
".env.local",
".env.production",
".env"
],
"env": [
// Add your environment variables!
]
},
"dev": {
"persistent": true,
"dotEnv": [
".env.development.local",
".env.local",
".env.development",
".env"
]
},
"lint": {
"outputs": ["node_modules/.cache/.eslintcache"]
},
"lint:fix": {
"outputs": ["node_modules/.cache/.eslintcache"],
"cache": false
},
"format": {
"outputs": ["node_modules/.cache/.prettiercache"]
},
"format:fix": {
"outputs": ["node_modules/.cache/.prettiercache"],
"cache": false
},
"typecheck": {
"outputs": ["node_modules/.cache/tsbuildinfo.json"],
"outputMode": "errors-only"
}
}
}
{
"extends": ["//"],
"pipeline": {
"build": {
"outputs": ["./next/**", "!./next/cache/**"],
"dotEnv": [
".env.production.local",
".env.local",
".env.production",
".env"
],
"env": [
// Add your environment variables!
]
},
"dev": {
"persistent": true,
"dotEnv": [
".env.development.local",
".env.local",
".env.development",
".env"
]
},
"lint": {
"outputs": ["node_modules/.cache/.eslintcache"]
},
"lint:fix": {
"outputs": ["node_modules/.cache/.eslintcache"],
"cache": false
},
"format": {
"outputs": ["node_modules/.cache/.prettiercache"]
},
"format:fix": {
"outputs": ["node_modules/.cache/.prettiercache"],
"cache": false
},
"typecheck": {
"outputs": ["node_modules/.cache/tsbuildinfo.json"],
"outputMode": "errors-only"
}
}
}
Note: Please refer to the docs at turbo.build/repo/docs/reference/configuration for details on the keys seen here.
Then, use the install command for your package manager. Your new app is now a part of your graph!
If you've followed the Prettier, ESLint, and TypeScript pages here in Maestros, you'll want to integrate the configuration from these packages into your Next.js app. Let's get those incorporated in one by one.
First, add the scripts to package.json
:
{
"scripts": {
"format": "prettier . --check --cache --cache-location='node_modules/.cache/prettiercache'",
"format:fix": "prettier . --write --cache --cache-location='node_modules/.cache/prettiercache' --log-level=warn"
},
"devDependencies": {
// Replace latest with most recent version
"prettier": "latest"
}
}
{
"scripts": {
"format": "prettier . --check --cache --cache-location='node_modules/.cache/prettiercache'",
"format:fix": "prettier . --write --cache --cache-location='node_modules/.cache/prettiercache' --log-level=warn"
},
"devDependencies": {
// Replace latest with most recent version
"prettier": "latest"
}
}
{
"scripts": {
"format": "prettier . --check --cache --cache-location='node_modules/.cache/prettiercache'",
"format:fix": "prettier . --write --cache --cache-location='node_modules/.cache/prettiercache' --log-level=warn"
},
"devDependencies": {
// Replace latest with most recent version
"prettier": "latest"
}
}
{
"scripts": {
"format": "prettier . --check --cache --cache-location='node_modules/.cache/prettiercache'",
"format:fix": "prettier . --write --cache --cache-location='node_modules/.cache/prettiercache' --log-level=warn"
},
"devDependencies": {
// Replace latest with most recent version
"prettier": "latest"
}
}
Prettier will use the configuration found in the root of your monorepo - but it won't respect the .prettierignore
found there. That's okay, though, because we can add one within our application's directory so we can make sure we don't spend time formatting files we don't need to.
apps/my-app/.prettierignore
.env*
node_modules
.next
apps/my-app/.prettierignore
.env*
node_modules
.next
apps/my-app/.prettierignore
.env*
node_modules
.next
apps/my-app/.prettierignore
.env*
node_modules
.next
First, install your TSConfig presets to your application and making a type checking script:
{
"scripts": {
"typecheck": "tsc --noEmit"
},
"devDependencies": {
"@repo/tsconfig": "workspace:*", // or "*" for npm and yarn
// Replace latest with most recent version
"typescript": "latest"
}
}
{
"scripts": {
"typecheck": "tsc --noEmit"
},
"devDependencies": {
"@repo/tsconfig": "workspace:*", // or "*" for npm and yarn
// Replace latest with most recent version
"typescript": "latest"
}
}
{
"scripts": {
"typecheck": "tsc --noEmit"
},
"devDependencies": {
"@repo/tsconfig": "workspace:*", // or "*" for npm and yarn
// Replace latest with most recent version
"typescript": "latest"
}
}
{
"scripts": {
"typecheck": "tsc --noEmit"
},
"devDependencies": {
"@repo/tsconfig": "workspace:*", // or "*" for npm and yarn
// Replace latest with most recent version
"typescript": "latest"
}
}
create-next-app
will generate a tsconfig.json
for you (as long as you picked TypeScript) so we will extend off of our root configurations there:
apps/my-app/tsconfig.json
"extends": "@repo/tsconfig/nextjs.json",
"compilerOptions": {
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json",
"plugins": [{ "name": "next" }],
"baseUrl": ".",
"paths": {
"#/*": ["./*"]
}
},
"include": [
"next-env.d.ts",
"**/*.js",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
"next.config.js"
],
"exclude": ["node_modules", ".next"]
}
apps/my-app/tsconfig.json
"extends": "@repo/tsconfig/nextjs.json",
"compilerOptions": {
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json",
"plugins": [{ "name": "next" }],
"baseUrl": ".",
"paths": {
"#/*": ["./*"]
}
},
"include": [
"next-env.d.ts",
"**/*.js",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
"next.config.js"
],
"exclude": ["node_modules", ".next"]
}
apps/my-app/tsconfig.json
"extends": "@repo/tsconfig/nextjs.json",
"compilerOptions": {
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json",
"plugins": [{ "name": "next" }],
"baseUrl": ".",
"paths": {
"#/*": ["./*"]
}
},
"include": [
"next-env.d.ts",
"**/*.js",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
"next.config.js"
],
"exclude": ["node_modules", ".next"]
}
apps/my-app/tsconfig.json
"extends": "@repo/tsconfig/nextjs.json",
"compilerOptions": {
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json",
"plugins": [{ "name": "next" }],
"baseUrl": ".",
"paths": {
"#/*": ["./*"]
}
},
"include": [
"next-env.d.ts",
"**/*.js",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
"next.config.js"
],
"exclude": ["node_modules", ".next"]
}
Using our Next.js preset we can quickly have ESLint set up exactly like the rest of our Next.js apps.
Add ESLint and its scripts to your application's package.json
:
{
"scripts": {
"lint": "eslint . --cache --cache-location 'node_modules/.cache/.eslintcache' --max-warnings 0",
"lint:fix": "eslint . --fix --cache --cache-location 'node_modules/.cache/.eslintcache' --max-warnings 0"
},
"devDependencies": {
"@repo/lint": "workspace:*", // or "*" for npm and yarn
// Replace latest with most recent version
"eslint": "latest"
}
}
{
"scripts": {
"lint": "eslint . --cache --cache-location 'node_modules/.cache/.eslintcache' --max-warnings 0",
"lint:fix": "eslint . --fix --cache --cache-location 'node_modules/.cache/.eslintcache' --max-warnings 0"
},
"devDependencies": {
"@repo/lint": "workspace:*", // or "*" for npm and yarn
// Replace latest with most recent version
"eslint": "latest"
}
}
{
"scripts": {
"lint": "eslint . --cache --cache-location 'node_modules/.cache/.eslintcache' --max-warnings 0",
"lint:fix": "eslint . --fix --cache --cache-location 'node_modules/.cache/.eslintcache' --max-warnings 0"
},
"devDependencies": {
"@repo/lint": "workspace:*", // or "*" for npm and yarn
// Replace latest with most recent version
"eslint": "latest"
}
}
{
"scripts": {
"lint": "eslint . --cache --cache-location 'node_modules/.cache/.eslintcache' --max-warnings 0",
"lint:fix": "eslint . --fix --cache --cache-location 'node_modules/.cache/.eslintcache' --max-warnings 0"
},
"devDependencies": {
"@repo/lint": "workspace:*", // or "*" for npm and yarn
// Replace latest with most recent version
"eslint": "latest"
}
}
Then, integrate your configuration into eslintrc.js
:
/** @type {import("eslint").Linter.Config} */
module.exports = {
root: true,
extends: [require.resolve('@repo/lint/next.js')],
parserOptions: {
project: true,
},
plugins: ['@typescript-eslint'],
};
/** @type {import("eslint").Linter.Config} */
module.exports = {
root: true,
extends: [require.resolve('@repo/lint/next.js')],
parserOptions: {
project: true,
},
plugins: ['@typescript-eslint'],
};
Note: If you're not using TypeScript, you can remove parserOptions and plugins.
/** @type {import("eslint").Linter.Config} */
module.exports = {
root: true,
extends: [require.resolve('@repo/lint/next.js')],
parserOptions: {
project: true,
},
plugins: ['@typescript-eslint'],
};
/** @type {import("eslint").Linter.Config} */
module.exports = {
root: true,
extends: [require.resolve('@repo/lint/next.js')],
parserOptions: {
project: true,
},
plugins: ['@typescript-eslint'],
};
Note: If you're not using TypeScript, you can remove parserOptions and plugins.
If you're following the Just-in-Time Package Pattern, you'll want to make sure that Next.js knows to compile your UI components on the fly. To do this, we'll use the transpilePackages property in our Next.js configuration.
/** @type {import('next').NextConfig} */
const nextConfig = {
transpilePackages: ['@repo/ui'],
};
module.exports = nextConfig;
/** @type {import('next').NextConfig} */
const nextConfig = {
transpilePackages: ['@repo/ui'],
};
module.exports = nextConfig;
/** @type {import('next').NextConfig} */
const nextConfig = {
transpilePackages: ['@repo/ui'],
};
module.exports = nextConfig;
/** @type {import('next').NextConfig} */
const nextConfig = {
transpilePackages: ['@repo/ui'],
};
module.exports = nextConfig;
Now, our Next.js app will hot reload any changes in our UI workspace, seamlessly integrating our components into our Next.js development server.
You may find moments where you'd like to use Next.js-specific components as a part of your UI kit. Rather than installing Next.js into your UI component workspace, it's better to keep your components unaware of Next.js and pass the Next.js components as props to your UI component.
As an example, let's say you wanted to use next/link
in your application using styles you've written out in your UI components. You'd first need to create a UI component that can accept Link
from 'next/link'
as a prop:
packages/ui/src/MyLink.ts
export const MyLink = ({ linkComponent, children, ...props }) => {
const Component = linkComponent;
return <Component {...props}>{children}</Component>;
};
packages/ui/src/MyLink.ts
export const MyLink = ({ linkComponent, children, ...props }) => {
const Component = linkComponent;
return <Component {...props}>{children}</Component>;
};
packages/ui/src/MyLink.ts
export const MyLink = ({ linkComponent, children, ...props }) => {
const Component = linkComponent;
return <Component {...props}>{children}</Component>;
};
packages/ui/src/MyLink.ts
export const MyLink = ({ linkComponent, children, ...props }) => {
const Component = linkComponent;
return <Component {...props}>{children}</Component>;
};
And use it in your app like this:
import { MyLink } from '@repo/ui';
import Link from 'next/link';
export default function Page() {
return (
<MyLink linkComponent={Link} href="/about">
Click me!
</MyLink>
);
}
import { MyLink } from '@repo/ui';
import Link from 'next/link';
export default function Page() {
return (
<MyLink linkComponent={Link} href="/about">
Click me!
</MyLink>
);
}
import { MyLink } from '@repo/ui';
import Link from 'next/link';
export default function Page() {
return (
<MyLink linkComponent={Link} href="/about">
Click me!
</MyLink>
);
}
import { MyLink } from '@repo/ui';
import Link from 'next/link';
export default function Page() {
return (
<MyLink linkComponent={Link} href="/about">
Click me!
</MyLink>
);
}
We like to let the application compile Tailwind classes for us to keep things simple. We've written more about this pattern and why we do it that way on the Tailwind page.