Skip to content

Commit 1dd2b16

Browse files
fix: Nx monorepo support (#474)
* docs: add docs for using open-next + SST in an Nx monorepo * fix(open-next): output the built open-next config in the right place Solution from #468 all credit to @jarodsim Fixes: #468 * docs: add missing references to eslint/ts config * fix: pass the lTempDir for the edge config * docs: add the reason why we are using the tempDir and lTempDir as we are * Create brave-eggs-end.md --------- Co-authored-by: conico974 <nicodorseuil@yahoo.fr>
1 parent a43fa46 commit 1dd2b16

File tree

4 files changed

+268
-7
lines changed

4 files changed

+268
-7
lines changed

.changeset/brave-eggs-end.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"open-next": patch
3+
---
4+
5+
fix: Nx monorepo support

docs/pages/config/_meta.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"simple_example": "Simple Example",
33
"custom_overrides": "Custom Overrides",
4-
"full_example": "Full Example"
4+
"full_example": "Full Example",
5+
"nx": "Nx Monorepo"
56
}

docs/pages/config/nx.mdx

Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
# Configuring OpenNext for use in an Nx Monorepo.
2+
Here's a detailed exampled of how to add open-next + SST to an existing Nx workspace, with an existing NextJS application sitting at `apps/next-site`
3+
4+
1. install `open-next`: `pnpm add —save-dev open-next`
5+
6+
2. Update your `apps/next-site/next.config.js` add `output: ‘standalone’`, and you want to add `experimental.outputFileTracingRoot`, it should look a little like this:
7+
```javascript
8+
//@ts-check
9+
10+
// eslint-disable-next-line @typescript-eslint/no-var-requires
11+
const { composePlugins, withNx } = require('@nx/next');
12+
const { join } = require('node:path');
13+
14+
/**
15+
* @type {import('@nx/next/plugins/with-nx').WithNxOptions}
16+
**/
17+
const nextConfig = {
18+
nx: {
19+
// Set this to true if you would like to use SVGR
20+
// See: https://github.com/gregberge/svgr
21+
svgr: false,
22+
},
23+
+ output: 'standalone',
24+
+ experimental: {
25+
+ // this should be the path to the root of your repo, in this case it's just two levels down. needed for open-next to detect that it's a monorepo
26+
+ outputFileTracingRoot: join(__dirname, '../../'),
27+
+ },
28+
};
29+
30+
const plugins = [
31+
// Add more Next.js plugins to this list if needed.
32+
withNx,
33+
];
34+
35+
module.exports = composePlugins(...plugins)(nextConfig);
36+
```
37+
38+
3. Create `open-next.config.js` inside your apps root directory, it should look a little something like this:
39+
```javascript
40+
import type { OpenNextConfig } from 'open-next/types/open-next';
41+
42+
const config = {
43+
default: {},
44+
buildCommand: 'exit 0', // in my example we set up Nx task distribution to handle the order of building.
45+
buildOutputPath: '.',
46+
appPath: '.',
47+
packageJsonPath: '../../', // again, path to the root of your repo (where the package.json is)
48+
} satisfies OpenNextConfig;
49+
50+
export default config;
51+
```
52+
53+
4. Set up nx's targets/tasks
54+
55+
Now we have open-next configuration set up, you can try to run `open-next build` and depending on whether you have already built your next app or not
56+
it might even work.
57+
58+
However, we don't want to rely on needing to manually running a build every time we want to deploy a change, so instead we can set up a target.
59+
We do this in your project's `project.json` (in this case, living at `apps/next-site/project`), we want to find the targets object and update it:
60+
```diff
61+
{
62+
"name": "next-site",
63+
"$schema": "../../node_modules/nx/schemas/project-schema.json",
64+
"sourceRoot": "apps/next-site",
65+
"projectType": "application",
66+
"tags": [],
67+
"targets": {
68+
+ "open-next-build": { // name of the target, this is what you will call
69+
+ "executor": "nx:run-commands",
70+
+ "dependsOn": ["build"], // this ensures that Nx will build our next app before running this command.
71+
+ "cache": true, // cache the output, good for if you want to use DTE/Nx cloud
72+
+ "outputs": ["{projectRoot}/.open-next"], // tell nx where the output lives
73+
+ "options": {
74+
+ "cwd": "apps/next-site", // where we run the command
75+
+ "command": "open-next build" // what command we want to run
76+
+ }
77+
+ }
78+
}
79+
}
80+
81+
```
82+
Next we need to add the open-next directory to our eslint's `ignorePatterns` array
83+
```diff
84+
{
85+
"extends": [
86+
"plugin:@nx/react-typescript",
87+
"next",
88+
"next/core-web-vitals",
89+
"../../.eslintrc.json"
90+
],
91+
"ignorePatterns": [
92+
"!**/*",
93+
+ ".next/**/*",
94+
+ ".open-next/**/*"
95+
],
96+
"overrides": [
97+
{
98+
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
99+
"rules": {}
100+
},
101+
{
102+
"files": ["*.ts", "*.tsx"],
103+
"rules": {}
104+
},
105+
{
106+
"files": ["*.js", "*.jsx"],
107+
"rules": {}
108+
},
109+
{
110+
"files": ["*.spec.ts", "*.spec.tsx", "*.spec.js", "*.spec.jsx"],
111+
"env": {
112+
"jest": true
113+
}
114+
}
115+
]
116+
}
117+
```
118+
119+
now, when you run `nx open-next-build next-site`, nx will automatically build the next app, and anything that the next app requires, neat!
120+
121+
5. Deploying with SST
122+
123+
Now, we have a built app, ready to deploy, so how do we get it onto SST/AWS ? Good question!
124+
125+
We are using `sst ion` in this example. i will assume you have already have the cli installed, (if not, check here on how!)[https://ion.sst.dev/],
126+
but we will not use the SST cli to init this project, because it wants to add a package.json to your next app, and it will look like it's working, but you will end up with a big far server error (all because the package.json overrides whatever nx _thinks_ there should be, and it will miss a bunch of dependencies). we will instead manually set this up:
127+
128+
- let's add the sst package with `pnpm add sst@ion`, and the required packages for SST to work with AWS `pnpm add --save-dev aws-cdk-lib constructs @types/aws-lambda`
129+
- then we want to manually create an `sst.config.ts` file in `apps/next-site` that looks like this:
130+
131+
```typescript
132+
/// <reference path="./.sst/platform/config.d.ts" />
133+
134+
export default $config({
135+
app(input) {
136+
return {
137+
name: 'next-site', // use whatever your project is called here
138+
removal: input?.stage === 'production' ? 'retain' : 'remove',
139+
home: 'aws',
140+
};
141+
},
142+
async run() {
143+
new sst.aws.Nextjs('Site', {
144+
buildCommand: 'exit 0;' // again, we want to get Nx to handle building
145+
});
146+
},
147+
});
148+
149+
```
150+
151+
- now, you probably see some type errors, as SST is not initialized yet, but we can resolve this by running
152+
```bash
153+
$ cd apps/next-site && sst install
154+
```
155+
156+
this will resolve the type issues and initialise SST.
157+
158+
- next we need to add `sst.config.ts` to our `tsconfig.json`'s excludes array
159+
160+
- then we want to add both `sst.config.ts` and the `.sst` folder to the eslint ignorePatterns
161+
162+
```diff
163+
{
164+
"extends": [
165+
"plugin:@nx/react-typescript",
166+
"next",
167+
"next/core-web-vitals",
168+
"../../.eslintrc.json"
169+
],
170+
"ignorePatterns": [
171+
"!**/*",
172+
".next/**/*",
173+
+ ".open-next/**/*",
174+
+ ".sst/**/*",
175+
+ "sst.config.ts"
176+
],
177+
"overrides": [
178+
{
179+
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
180+
"rules": {}
181+
},
182+
{
183+
"files": ["*.ts", "*.tsx"],
184+
"rules": {}
185+
},
186+
{
187+
"files": ["*.js", "*.jsx"],
188+
"rules": {}
189+
},
190+
{
191+
"files": ["*.spec.ts", "*.spec.tsx", "*.spec.js", "*.spec.jsx"],
192+
"env": {
193+
"jest": true
194+
}
195+
}
196+
]
197+
}
198+
```
199+
200+
- now, if you want to run `sst dev` you can do so with `sst dev "nx dev next-site"` similarly deploying can be done with `sst deploy`...but you probably want to set up that task chaining, again we can do that by adding a target to your app, and setting it's `dependsOn` to the `open-next-build`, here's what it might look like:
201+
202+
```diff
203+
{
204+
"name": "next-site",
205+
"$schema": "../../node_modules/nx/schemas/project-schema.json",
206+
"sourceRoot": "apps/next-site",
207+
"projectType": "application",
208+
"tags": [],
209+
"targets": {
210+
"open-next-build": {
211+
"executor": "nx:run-commands",
212+
"dependsOn": ["build"],
213+
"cache": true,
214+
"outputs": ["{projectRoot}/.open-next"],
215+
"options": {
216+
"cwd": "apps/next-site",
217+
"command": "open-next build"
218+
}
219+
+ },
220+
+ "deploy": {
221+
+ "executor": "nx:run-commands",
222+
+ "dependsOn": ["open-next-build"],
223+
+ "options": {
224+
+ "cwd": "apps/next-site",
225+
+ "command": "sst deploy --stage {args.stage}", // here we use nx's interpolation to allow --stage to be passed, with some configuration examples below
226+
+ "forwardAllArgs": true
227+
+ },
228+
+ "defaultConfiguration": "dev",
229+
+ "configurations": {
230+
+ "production": {
231+
+ "args": ["--stage=production"]
232+
+ },
233+
+ "staging": {
234+
+ "args": ["--stage=staging"]
235+
+ },
236+
+ "dev": {
237+
+ "args": ["--stage=development"]
238+
+ }
239+
+ }
240+
+ }
241+
+ }
242+
}
243+
244+
245+
```
246+
247+
now we can run (or if you want a custom stage, you can simply do `nx deploy next-site --stage this-is-my-stage` and it will be passed directly to the sst command).
248+
```bash
249+
$ nx deploy next-site --configuration dev # using dev configuration (which sets the stage to development)
250+
# nx deploy next-site -c dev # OR
251+
# nx deploy next-site --stage my-stage # Custom Stage
252+
```

packages/open-next/src/build.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ export async function build(
8888

8989
// Generate deployable bundle
9090
printHeader("Generating bundle");
91-
initOutputDir();
91+
initOutputDir(tempDir);
9292

9393
// Compile cache.ts
9494
compileCache();
@@ -208,8 +208,11 @@ function printOpenNextVersion() {
208208
logger.info(`OpenNext v${openNextVersion}`);
209209
}
210210

211-
function initOutputDir() {
212-
const { outputDir, tempDir } = options;
211+
function initOutputDir(tempDir: string) {
212+
// We need to get the build relative to the cwd to find the compiled config
213+
// This is needed for the case where the app is a single-version monorepo and the package.json is in the root of the monorepo
214+
// where the build is in the app directory, but the compiled config is in the root of the monorepo.
215+
const { outputDir, tempDir: lTempDir } = options;
213216
const openNextConfig = readFileSync(
214217
path.join(tempDir, "open-next.config.mjs"),
215218
"utf8",
@@ -222,11 +225,11 @@ function initOutputDir() {
222225
);
223226
}
224227
fs.rmSync(outputDir, { recursive: true, force: true });
225-
fs.mkdirSync(tempDir, { recursive: true });
226-
fs.writeFileSync(path.join(tempDir, "open-next.config.mjs"), openNextConfig);
228+
fs.mkdirSync(lTempDir, { recursive: true });
229+
fs.writeFileSync(path.join(lTempDir, "open-next.config.mjs"), openNextConfig);
227230
if (openNextConfigEdge) {
228231
fs.writeFileSync(
229-
path.join(tempDir, "open-next.config.edge.mjs"),
232+
path.join(lTempDir, "open-next.config.edge.mjs"),
230233
openNextConfigEdge,
231234
);
232235
}

0 commit comments

Comments
 (0)