|
| 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 | +``` |
0 commit comments