Skip to content

Commit e920cc6

Browse files
feat: monorepo handling (#434)
* feat: monorepo handling * chore: move demo to use monorepo features * feat: add Nx config checks * chore: fix tests * Remove log Co-authored-by: lindsaylevine <[email protected]> * chore: changes from review * fix: handle next imports * fix: sort out require handling * chore: add todo for documenting monorepo in error message * chore: add docs * chore: add links to error messages * chore: set targetPort in nx doc Co-authored-by: lindsaylevine <[email protected]>
1 parent a458378 commit e920cc6

21 files changed

+11347
-727
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,3 +150,5 @@ Temporary Items
150150

151151
# End of https://www.toptal.com/developers/gitignore/api/osx,node
152152
demo/package-lock.json
153+
154+
.netlify

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ Read more about [file-based plugin installation](https://docs.netlify.com/config
7777
- [CLI Usage](https://github.com/netlify/netlify-plugin-nextjs/tree/main/docs/cli-usage.md)
7878
- [Custom Netlify Functions](https://github.com/netlify/netlify-plugin-nextjs/tree/main/docs/custom-functions.md)
7979
- [Image Handling](https://github.com/netlify/netlify-plugin-nextjs/tree/main/docs/image-handling.md)
80+
- [Monorepos and Nx](https://github.com/netlify/netlify-plugin-nextjs/tree/main/docs/monorepos.md)
8081
- [Custom Netlify Redirects](https://github.com/netlify/netlify-plugin-nextjs/tree/main/docs/custom-redirects.md)
8182
- [Local Files in Runtime](https://github.com/netlify/netlify-plugin-nextjs/tree/main/docs/local-files-in-runtime.md)
8283
- [FAQ](https://github.com/netlify/netlify-plugin-nextjs/tree/main/docs/faq.md)

demo/netlify.toml

Lines changed: 0 additions & 11 deletions
This file was deleted.

demo/package.json

Lines changed: 0 additions & 15 deletions
This file was deleted.

docs/monorepos.md

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
## Using in a monorepo or subdirectory
2+
3+
The Essential Next.js plugin works in most monorepos, but may need some configuration changes. This depends on the type of monorepo and the tooling that you use.
4+
5+
### Self-contained subdirectory
6+
7+
If your Next.js site is in a subdirectory of the repo, but doesn't rely on installing or compiling anything outside of that directory, then the simplest arrangement is to set the `base` of the site to that directory. This can be done either in the Netlify dashboard or in the `netlify.toml`. If your site is in `/frontend`, you should set up your site with the following in the root of the repo:
8+
9+
```toml
10+
# ./netlify.toml
11+
[build]
12+
base="frontend"
13+
```
14+
You can then place another `netlify.toml` file in `/frontend/` that configures the actual build:
15+
16+
```toml
17+
# ./frontend/netlify.toml
18+
19+
[build]
20+
command = "npm run build"
21+
publish = "out"
22+
23+
[[plugins]]
24+
package = "@netlify/plugin-nextjs"
25+
```
26+
27+
### Yarn workspace
28+
29+
If your site is a yarn workspace - including one that uses lerna - you should keep the base as the root of the repo, but change the configuration as follows. Assuming the site is in `/packages/frontend/`:
30+
31+
```toml
32+
# ./netlify.toml
33+
34+
[build]
35+
command = "next build packages/frontend"
36+
publish = "packages/frontend/out"
37+
38+
[dev]
39+
command = "next dev packages/frontend"
40+
41+
[[plugins]]
42+
package = "@netlify/plugin-nextjs"
43+
```
44+
45+
Ensure that the `next.config.js` is in the site directory, i.e. `/packages/frontend/next.config.js`. You must ensure that there is either a `yarn.lock` in the root of the site, or the environment variable `NETLIFY_USE_YARN` is set to true.
46+
47+
### Lerna monorepo using npm
48+
49+
If your monorepo uses Yarn workspaces, then set it up as shown above in the Yarn workspace section. If it uses npm then it is a little more complicated. First, you need to ensure that the `next` package is installed as a top-level dependency, i.e. it is in `./package.json` rather than `packages/frontend/package.json`. This is because it needs to be installed before lerna is bootstrapped as the build plugin needs to use it. Generally, hoisting as many packages to the top level as possible is best, so that they are more efficiently cached. You then should change the build command, and make it similar to this:
50+
51+
```toml
52+
# ./netlify.toml
53+
54+
[build]
55+
command = "lerna bootstrap && next build packages/frontend"
56+
publish = "packages/frontend/out"
57+
58+
[dev]
59+
command = "next dev packages/frontend"
60+
61+
[[plugins]]
62+
package = "@netlify/plugin-nextjs"
63+
```
64+
65+
### Nx
66+
67+
[Nx](https://nx.dev/) is a build framework that handles scaffolding, building and deploying projects. It has support for Next.js via the `@nrwl/next` package. When building a Next.js site, it changes a lot of the configuraiton on the fly, and has quite a different directory structure to a normal Next.js site. The Essential Next.js plugin has full support for sites that use Nx, but there are a few required changes that you must make to the configuration.
68+
69+
First, you need to make the `publish` directory point at a dirctory called `out` inside the app directory, rather than the build directory. If your app is called `myapp`, your `netlify.toml` should look something like:
70+
71+
```toml
72+
# ./netlify.toml
73+
74+
[build]
75+
command = "npm run build"
76+
publish = "apps/myapp/out"
77+
78+
[dev]
79+
command = "npm run start"
80+
targetPort = 4200
81+
82+
[[plugins]]
83+
package = "@netlify/plugin-nextjs"
84+
```
85+
86+
You also need to make a change to the `next.config.js` inside the app directory. By default, Nx changes the Next.js `distDir` on the fly, changing it to a directory in the root of the repo. The Essential Next.js plugin can't read this value, so has no way of determining where the build files can be found. However, if you change the `distDir` in the config to anything except `.next`, then `Nx` will leave it unchanged, and the Essential Next.js plugin can read the value from there. e.g.
87+
88+
```js
89+
// ./apps/myapp/next.config.js
90+
91+
const withNx = require('@nrwl/next/plugins/with-nx');
92+
93+
module.exports = withNx({
94+
distDir: '.dist',
95+
target: 'serverless'
96+
});
97+
98+
```
99+
100+
### Other monorepos
101+
102+
Other arrangements may work: for more details, see [the monorepo documentation](https://docs.netlify.com/configure-builds/common-configurations/monorepos/). The important points are:
103+
104+
1. The `next` package must be installed as part of the initial `npm install` or `yarn install`, not from the build command.
105+
2. The `publish` directory must be called `out`, and should be in the same directory as the `next.config.js` file. e.g.
106+
107+
```
108+
backend/
109+
frontend/
110+
|- next.config.js
111+
|- out
112+
netlify.toml
113+
package.json
114+
```
115+
If you have another monorepo tool that you are using, we would welcome PRs to add instructions to this document.

helpers/checkNxConfig.js

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
const { existsSync } = require('fs')
2+
const { EOL } = require('os')
3+
const path = require('path')
4+
5+
const checkNxConfig = ({ netlifyConfig, nextConfig, failBuild, constants: { PUBLISH_DIR } }) => {
6+
const errors = []
7+
if (nextConfig.distDir === '.next') {
8+
errors.push(
9+
"- When using Nx you must set a value for 'distDir' in your next.config.js, and the value cannot be '.next'",
10+
)
11+
}
12+
// The PUBLISH_DIR constant is normalized, so no leading slash is needed
13+
if (!PUBLISH_DIR.startsWith('apps/')) {
14+
errors.push(
15+
"Please set the 'publish' value in your Netlify build config to a folder inside your app directory. e.g. 'apps/myapp/out'",
16+
)
17+
}
18+
// Look for the config file as a sibling of the publish dir
19+
const expectedConfigFile = path.resolve(netlifyConfig.build.publish, '..', 'next.config.js')
20+
21+
if (expectedConfigFile !== nextConfig.configFile) {
22+
const confName = path.relative(process.cwd(), nextConfig.configFile)
23+
errors.push(
24+
`- Using incorrect config file '${confName}'. Expected to use '${path.relative(
25+
process.cwd(),
26+
expectedConfigFile,
27+
)}'`,
28+
)
29+
30+
if (existsSync(expectedConfigFile)) {
31+
errors.push(
32+
`Please move or delete '${confName}'${confName === 'next.config.js' ? ' from the root of your site' : ''}.`,
33+
)
34+
} else {
35+
errors.push(
36+
`Please move or delete '${confName}'${
37+
confName === 'next.config.js' ? ' from the root of your site' : ''
38+
}, and create '${path.relative(process.cwd(), expectedConfigFile)}' instead.`,
39+
)
40+
}
41+
}
42+
43+
if (errors.length !== 0) {
44+
failBuild(
45+
// TODO: Add ntl.fyi link to docs
46+
[
47+
'Invalid configuration',
48+
...errors,
49+
'See the docs on using Nx with Netlify for more information: https://ntl.fyi/nx-next',
50+
].join(EOL),
51+
)
52+
}
53+
}
54+
55+
module.exports = checkNxConfig

helpers/doesNotNeedPlugin.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
const findUp = require('find-up')
2-
31
// Checks all the cases for which the plugin should do nothing
42
const doesSiteUseNextOnNetlify = require('./doesSiteUseNextOnNetlify')
53
const isStaticExportProject = require('./isStaticExportProject')

helpers/getNextConfig.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,17 @@ const { cwd: getCwd } = require('process')
44

55
const moize = require('moize')
66

7+
const resolveNextModule = require('./resolveNextModule')
8+
79
// We used to cache nextConfig for any cwd. Now we pass process.cwd() to cache
810
// (or memoize) nextConfig per cwd.
911
const getNextConfig = async function (failBuild = defaultFailBuild, cwd = getCwd()) {
1012
// We cannot load `next` at the top-level because we validate whether the
1113
// site is using `next` inside `onPreBuild`.
12-
const { PHASE_PRODUCTION_BUILD } = require('next/constants')
13-
const loadConfig = require('next/dist/next-server/server/config').default
14+
/* eslint-disable import/no-dynamic-require */
15+
const { PHASE_PRODUCTION_BUILD } = require(resolveNextModule('next/constants', cwd))
16+
const loadConfig = require(resolveNextModule('next/dist/next-server/server/config', cwd)).default
17+
/* eslint-enable import/no-dynamic-require */
1418

1519
try {
1620
return await loadConfig(PHASE_PRODUCTION_BUILD, cwd)

helpers/getNextRoot.js

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
const { existsSync } = require('fs')
2+
const path = require('path')
3+
4+
/**
5+
* If we're in a monorepo then the Next root may not be the same as the base directory
6+
* If there's no next.config.js in the root, we instead look for it as a sibling of the publish dir
7+
*/
8+
const getNextRoot = ({ netlifyConfig }) => {
9+
let nextRoot = process.cwd()
10+
if (!existsSync(path.join(nextRoot, 'next.config.js')) && netlifyConfig.build.publish) {
11+
nextRoot = path.dirname(netlifyConfig.build.publish)
12+
}
13+
return nextRoot
14+
}
15+
16+
module.exports = getNextRoot

helpers/resolveNextModule.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/**
2+
* We can't require() these normally, because the "next" package might not be resolvable from the root of a monorepo
3+
*/
4+
const resolveNextModule = (module, nextRoot) => {
5+
// Get the default list of require paths...
6+
const paths = require.resolve.paths(module)
7+
// ...add the root of the Next site to the beginning of that list so we try it first...
8+
paths.unshift(nextRoot)
9+
// ...then resolve the module using that list of paths.
10+
return require.resolve(module, { paths })
11+
}
12+
13+
module.exports = resolveNextModule

0 commit comments

Comments
 (0)