Skip to content

Commit c741a56

Browse files
authored
docs for running locally (#488)
1 parent 780bbc9 commit c741a56

File tree

3 files changed

+194
-3
lines changed

3 files changed

+194
-3
lines changed

docs/pages/contribute.mdx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
11
To run `OpenNext` locally:
22

33
1. Clone [this repository](https://github.com/sst/open-next).
4-
1. Build `open-next`:
4+
2. Build `open-next`:
55
```bash
66
cd open-next
77
pnpm build
88
```
9-
1. Run `open-next` in watch mode:
9+
3. Run `open-next` in watch mode:
1010
```bash
1111
pnpm dev
1212
```
13-
1. Now, you can make changes in `open-next` and build your Next.js app to test the changes.
13+
4. Now, you can make changes in `open-next` and build your Next.js app to test the changes.
1414
```bash
1515
cd path/to/my/nextjs/app
1616
path/to/open-next/packages/open-next/dist/index.js build
1717
```
18+
19+
It can be a bit cumbersome to need to deploy every time you want to test changes. If your change is not dependent on the wrapper or the converter, then you can create a custom `open-next.config.ts` file, you can take a look [here](/contribute/local_run) for more information.

docs/pages/contribute/_meta.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"local_run": "Run locally",
3+
"plugin": "Internal plugin system"
4+
}
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
When making some changes to OpenNext, it can be a bit cumbersome to need to deploy every time you want to test changes. If your change is not dependent on the wrapper or the converter, then you can create a custom `open-next.config.ts` file (you can use another name so that it doesn't conflict with your existing `open-next.config.ts`). Here is an example with a bunch of custom overrides:
2+
3+
To run `OpenNext` locally:
4+
```bash
5+
# This is to build (the config-path is needed if you use a different name than the default one)
6+
node /path/to/open-next/packages/open-next/dist/index.js build --config-path open-next.local.config.ts
7+
# Then to run the server
8+
node .open-next/server-functions/default/index.mjs
9+
```
10+
11+
```typescript
12+
// open-next.local.config.ts -
13+
// A good practice would be to use a different name so that it doesn't conflict
14+
// with your existing open-next.config.ts i.e. open-next.local.config.ts
15+
import type {OpenNextConfig} from 'open-next/types/open-next'
16+
17+
const config = {
18+
default: {
19+
override:{
20+
// We use a custom wrapper so that we can use static assets and image optimization locally
21+
wrapper: () => import('./dev/wrapper').then(m => m.default),
22+
// ISR and SSG won't work properly locally without this - Remove if you only need SSR
23+
incrementalCache: () => import('./dev/incrementalCache').then(m => m.default),
24+
// ISR requires a queue to work properly - Remove if you only need SSR or SSG
25+
queue: () => import('./dev/queue').then(m => m.default),
26+
converter: 'node',
27+
}
28+
},
29+
// You don't need this part if you don't use image optimization or don't need it in your test
30+
imageOptimization: {
31+
// Image optimization only work on linux, and you have to use the correct architecture for your system
32+
arch: 'x64',
33+
override: {
34+
wrapper: 'node',
35+
converter: 'node',
36+
}
37+
// If you need to test with local assets, you'll have to override the imageLoader as well
38+
},
39+
40+
dangerous: {
41+
// We disable the cache tags as it will usually not be needed locally for testing
42+
// It's only used for next/cache revalidateTag and revalidatePath
43+
// If you need it you'll have to override the tagCache as well
44+
disableTagCache: true,
45+
46+
47+
// You can uncomment this line if you only need to test SSR
48+
//disableIncrementalCache: true,
49+
},
50+
// You can override the build command so that you don't have to rebuild the app every time
51+
// You need to have run the default build command at least once
52+
buildCommand: 'echo "no build command"',
53+
edgeExternals: ['./dev/wrapper', './dev/incrementalCache', './dev/queue'],
54+
} satisfies OpenNextConfig
55+
56+
export default config
57+
```
58+
59+
```typescript
60+
// dev/wrapper.ts
61+
// You'll need to install express
62+
import express from 'express'
63+
// The proxy is used to proxy the image optimization server
64+
// you don't have to use it, but image request will return 500 error
65+
import proxy from 'express-http-proxy'
66+
import { fork } from 'child_process'
67+
68+
import type { StreamCreator } from "open-next/http/openNextResponse";
69+
import type { WrapperHandler } from "open-next/types/open-next";
70+
71+
const wrapper: WrapperHandler = async (handler, converter) => {
72+
const app = express();
73+
// To serve static assets
74+
app.use(express.static('../../assets'))
75+
76+
//Launch image server fork
77+
fork('../../image-optimization-function/index.mjs', [], {
78+
env: {
79+
NODE_ENV: 'development',
80+
PORT: '3001',
81+
}
82+
})
83+
app.use('/_next/image', proxy('localhost:3001'))
84+
85+
app.all('*', async (req, res) => {
86+
const internalEvent = await converter.convertFrom(req);
87+
const _res : StreamCreator = {
88+
writeHeaders: (prelude) => {
89+
res.writeHead(prelude.statusCode, prelude.headers);
90+
res.uncork();
91+
return res;
92+
},
93+
onFinish: () => {
94+
// Is it necessary to do something here?
95+
},
96+
};
97+
await handler(internalEvent, _res);
98+
});
99+
100+
const server = app.listen(parseInt(process.env.PORT ?? "3000", 10), ()=> {
101+
console.log(`Server running on port ${process.env.PORT ?? 3000}`);
102+
})
103+
104+
105+
app.on('error', (err) => {
106+
console.error('error', err);
107+
});
108+
109+
return () => {
110+
server.close();
111+
};
112+
};
113+
114+
export default {
115+
wrapper,
116+
name: "dev-node",
117+
supportStreaming: true,
118+
};
119+
```
120+
121+
```typescript
122+
// dev/incrementalCache.ts
123+
import type {IncrementalCache} from 'open-next/cache/incremental/types'
124+
125+
import fs from 'node:fs/promises'
126+
import path from 'node:path'
127+
128+
const buildId = process.env.NEXT_BUILD_ID
129+
const basePath= path.resolve(process.cwd(), `../../cache/${buildId}`)
130+
131+
const getCacheKey = (key: string) => {
132+
return path.join(basePath, `${key}.cache`)
133+
}
134+
135+
const cache: IncrementalCache = {
136+
name: 'dev-fs',
137+
get: async (key: string) => {
138+
const fileData = await fs.readFile(getCacheKey(key), 'utf-8')
139+
const data = JSON.parse(fileData)
140+
const {mtime} = await fs.stat(getCacheKey(key))
141+
return {
142+
value: data,
143+
lastModified: mtime.getTime(),
144+
}
145+
},
146+
set: async (key, value, isFetch) => {
147+
const data = JSON.stringify(value)
148+
await fs.writeFile(getCacheKey(key), data)
149+
},
150+
delete: async (key) => {
151+
await fs.rm(getCacheKey(key))
152+
}
153+
}
154+
155+
export default cache
156+
```
157+
158+
```typescript
159+
// dev/queue.ts
160+
import type {Queue} from 'open-next/queue/types'
161+
162+
declare global {
163+
// This is declared in the global scope so that we can use it in the queue
164+
// We need to use this one as next overrides the global fetch
165+
var internalFetch: typeof fetch
166+
}
167+
168+
const queue: Queue = {
169+
name: 'dev-queue',
170+
send: async (message) => {
171+
const prerenderManifest = (await import('open-next/adapters/config')).PrerenderManifest as any
172+
const revalidateId : string = prerenderManifest.preview.previewModeId
173+
await globalThis.internalFetch(`http://localhost:3000${message.MessageBody.url}`, {
174+
method: "HEAD",
175+
headers: {
176+
"x-prerender-revalidate": revalidateId,
177+
"x-isr": "1",
178+
},
179+
},)
180+
console.log('sending message', message)
181+
},
182+
}
183+
184+
export default queue
185+
```

0 commit comments

Comments
 (0)