Skip to content

Commit

Permalink
fix(switchable-runtime): Make it possible to switch between edge and …
Browse files Browse the repository at this point in the history
…server runtime in dev (#39327)

Makes it possible to switch between edge/server runtime in dev without
breaking the server.

Fixes slack:
[1](https://vercel.slack.com/archives/CGU8HUTUH/p1659082535540549)
[2](https://vercel.slack.com/archives/C02CDC2ALJH/p1658978287244359)
[3](https://vercel.slack.com/archives/C03KAR5DCKC/p1656869427468779)

#### middleware-plugin.ts
`middlewareManifest` moved from module scope to local scope. Stale state
from earlier builds ended up in `middleware-manifest.json`. Functions
that changed from edge to server runtime stayed in the manifest as edge
functions.

#### on-demand-entry-handler.ts
When a server or edge entry is added we check if it has switched
runtime. If that's the case the old entry is removed.

#### Reproduce
Create edge API route and visit `/api/hello`
```js
// pages/api/hello.js
export const config = {
  runtime: 'experimental-edge',
}

export default () => new Response('Hello')
```

Change it to a server api route and visit `/api/hello`, it will explode.
```js
// pages/api/hello.js
export default function (req, res) {
  res.send('Hello')
}
```

#### Bug not fixed
One EDGE case is not fixed. It occurs if you switch between edge and
server runtime several times without changing the content of the file:

Edge runtime
```js
export const config = {
  runtime: 'experimental-edge',
}

export default () => new Response('Hello')
```

Change it to a server runtime
```js
export default function (req, res) {
  res.send('Hello')
}
```

Change back to edge runtime, the content of the file is the same as the
first time we compiled the edge runtime version.
```js
export const config = {
  runtime: 'experimental-edge',
}

export default () => new Response('Hello')
```

The reason is that both the edge and server compiler emits to the same
file (/.next/server/pages/api/hello.js) which makes this check fail in
webpack:
https://github.com/webpack/webpack/blob/main/lib/Compiler.js#L849-L861
Possible solution is to use different output folders for edge and server
https://vercel.slack.com/archives/CGU8HUTUH/p1661163106667559

Co-authored-by: JJ Kasper <jj@jjsweb.site>
  • Loading branch information
Hannes Bornö and ijjk committed Sep 7, 2022
1 parent d5e6eb1 commit a9b9e00
Show file tree
Hide file tree
Showing 7 changed files with 218 additions and 6 deletions.
12 changes: 6 additions & 6 deletions packages/next/build/webpack/plugins/middleware-plugin.ts
Expand Up @@ -46,12 +46,6 @@ interface EntryMetadata {
}

const NAME = 'MiddlewarePlugin'
const middlewareManifest: MiddlewareManifest = {
sortedMiddleware: [],
middleware: {},
functions: {},
version: 2,
}

/**
* Checks the value of usingIndirectEval and when it is a set of modules it
Expand Down Expand Up @@ -121,6 +115,12 @@ function getCreateAssets(params: {
}) {
const { compilation, metadataByEntry } = params
return (assets: any) => {
const middlewareManifest: MiddlewareManifest = {
sortedMiddleware: [],
middleware: {},
functions: {},
version: 2,
}
for (const entrypoint of compilation.entrypoints.values()) {
if (!entrypoint.name) {
continue
Expand Down
10 changes: 10 additions & 0 deletions packages/next/server/dev/on-demand-entry-handler.ts
Expand Up @@ -649,12 +649,22 @@ export function onDemandEntryHandler({
},
onServer: () => {
added.set(COMPILER_NAMES.server, addEntry(COMPILER_NAMES.server))
const edgeServerEntry = `${COMPILER_NAMES.edgeServer}${pagePathData.page}`
if (entries[edgeServerEntry]) {
// Runtime switched from edge to server
delete entries[edgeServerEntry]
}
},
onEdgeServer: () => {
added.set(
COMPILER_NAMES.edgeServer,
addEntry(COMPILER_NAMES.edgeServer)
)
const serverEntry = `${COMPILER_NAMES.server}${pagePathData.page}`
if (entries[serverEntry]) {
// Runtime switched from server to edge
delete entries[serverEntry]
}
},
})

Expand Down
184 changes: 184 additions & 0 deletions test/e2e/switchable-runtime/index.test.ts
Expand Up @@ -203,6 +203,190 @@ describe('Switchable runtime', () => {
}
})

it('should be possible to switch between runtimes in API routes', async () => {
await check(
() => renderViaHTTP(next.url, '/api/switch-in-dev'),
'server response'
)

// Edge
await next.patchFile(
'pages/api/switch-in-dev.js',
`
export const config = {
runtime: 'experimental-edge',
}
export default () => new Response('edge response')
`
)
await check(
() => renderViaHTTP(next.url, '/api/switch-in-dev'),
'edge response'
)

// Server
await next.patchFile(
'pages/api/switch-in-dev.js',
`
export default function (req, res) {
res.send('server response again')
}
`
)
await check(
() => renderViaHTTP(next.url, '/api/switch-in-dev'),
'server response again'
)

// Edge
await next.patchFile(
'pages/api/switch-in-dev.js',
`
export const config = {
runtime: 'experimental-edge',
}
export default () => new Response('edge response again')
`
)
await check(
() => renderViaHTTP(next.url, '/api/switch-in-dev'),
'edge response again'
)
})

it('should be possible to switch between runtimes in pages', async () => {
await check(
() => renderViaHTTP(next.url, '/switch-in-dev'),
/Hello from edge page/
)

// Server
await next.patchFile(
'pages/switch-in-dev.js',
`
export default function Page() {
return <p>Hello from server page</p>
}
`
)
await check(
() => renderViaHTTP(next.url, '/switch-in-dev'),
/Hello from server page/
)

// Edge
await next.patchFile(
'pages/switch-in-dev.js',
`
export default function Page() {
return <p>Hello from edge page again</p>
}
export const config = {
runtime: 'experimental-edge',
}
`
)
await check(
() => renderViaHTTP(next.url, '/switch-in-dev'),
/Hello from edge page again/
)

// Server
await next.patchFile(
'pages/switch-in-dev.js',
`
export default function Page() {
return <p>Hello from server page again</p>
}
`
)
await check(
() => renderViaHTTP(next.url, '/switch-in-dev'),
/Hello from server page again/
)
})

// Doesn't work, see https://github.com/vercel/next.js/pull/39327
it.skip('should be possible to switch between runtimes with same content', async () => {
const fileContent = await next.readFile(
'pages/api/switch-in-dev-same-content.js'
)
console.log({ fileContent })
await check(
() => renderViaHTTP(next.url, '/api/switch-in-dev-same-content'),
'server response'
)

// Edge
await next.patchFile(
'pages/api/switch-in-dev-same-content.js',
`
export const config = {
runtime: 'experimental-edge',
}
export default () => new Response('edge response')
`
)
await check(
() => renderViaHTTP(next.url, '/api/switch-in-dev-same-content'),
'edge response'
)

// Server - same content as first compilation of the server runtime version
await next.patchFile(
'pages/api/switch-in-dev-same-content.js',
fileContent
)
await check(
() => renderViaHTTP(next.url, '/api/switch-in-dev-same-content'),
'server response'
)
})

it('should recover from syntax error when using edge runtime', async () => {
await check(
() => renderViaHTTP(next.url, '/api/syntax-error-in-dev'),
'edge response'
)

// Syntax error
await next.patchFile(
'pages/api/syntax-error-in-dev.js',
`
export const config = {
runtime: 'experimental-edge',
}
export default => new Response('edge response')
`
)
await check(
() => renderViaHTTP(next.url, '/api/syntax-error-in-dev'),
/Unexpected token/
)

// Fix syntax error
await next.patchFile(
'pages/api/syntax-error-in-dev.js',
`
export default () => new Response('edge response again')
export const config = {
runtime: 'experimental-edge',
}
`
)
await check(
() => renderViaHTTP(next.url, '/api/syntax-error-in-dev'),
'edge response again'
)
})

it('should not crash the dev server when invalid runtime is configured', async () => {
await check(
() => renderViaHTTP(next.url, '/invalid-runtime'),
Expand Down
@@ -0,0 +1,3 @@
export default (req, res) => {
res.send('server response')
}
3 changes: 3 additions & 0 deletions test/e2e/switchable-runtime/pages/api/switch-in-dev.js
@@ -0,0 +1,3 @@
export default (req, res) => {
res.send('server response')
}
5 changes: 5 additions & 0 deletions test/e2e/switchable-runtime/pages/api/syntax-error-in-dev.js
@@ -0,0 +1,5 @@
export default () => new Response('edge response')

export const config = {
runtime: `experimental-edge`,
}
7 changes: 7 additions & 0 deletions test/e2e/switchable-runtime/pages/switch-in-dev.js
@@ -0,0 +1,7 @@
export default function Page() {
return <p>Hello from edge page</p>
}

export const config = {
runtime: 'experimental-edge',
}

0 comments on commit a9b9e00

Please sign in to comment.