Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: resolve next-server from next app directory and not from plugin #2059

Merged
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
8d8d8ae
fix: resolve next-server from next app directory and not from plugin
pieh Apr 21, 2023
e7ae048
fix: update api handler
pieh Apr 21, 2023
15c1bdd
chore: export type from server module
pieh Apr 21, 2023
4b64c17
fix: server.spec.ts
pieh Apr 21, 2023
1933824
chore: move next server throwing to runtime
pieh Apr 21, 2023
e06157d
fix: restore trying to resolve from plugin and not appdir
pieh Apr 21, 2023
ffae6a2
fix: use relative import paths not absolute
pieh Apr 21, 2023
d50c047
fix: server.spec.ts (again)
pieh Apr 21, 2023
c5a4afd
fix: index.spec.ts
pieh Apr 21, 2023
48468bc
chore: diff cleanup
pieh Apr 21, 2023
33c28ae
fix: handle function config parsing as well
pieh Apr 25, 2023
be8bc6e
fix: adjust import
pieh Apr 25, 2023
caef767
fix: server.spec.ts (again vol 2)
pieh Apr 25, 2023
56aa4f7
test: add integration test
pieh Apr 26, 2023
1e48952
Merge remote-tracking branch 'origin/main' into fix/use-next-director…
pieh Apr 26, 2023
22e2fa2
test: log npm version
pieh Apr 26, 2023
6e229c9
test: install newer npm/node
pieh Apr 26, 2023
74ab70e
test: bump timeout for setup
pieh Apr 26, 2023
e4fadf4
test: ensure test page is ssr
pieh Apr 26, 2023
1774f47
refactor: get rid of unneded new helper
pieh Apr 26, 2023
bbff3a9
chore: cleanup some debugging logs
pieh Apr 27, 2023
ade1a2e
fix: add fallback in case findModuleBase will be false
pieh Apr 27, 2023
610d73f
refactor: use one-parameter object for makeHandler functions
pieh Apr 27, 2023
917d3ce
Merge remote-tracking branch 'origin/main' into fix/use-next-director…
pieh Apr 27, 2023
1b01485
Merge remote-tracking branch 'origin/main' into fix/use-next-director…
pieh Apr 28, 2023
04f3164
refactor: don't rely on MODULE_NOT_FOUND for lack of advanced API rou…
pieh Apr 28, 2023
92dcf3a
Update packages/runtime/src/templates/server.ts
pieh May 2, 2023
66a9670
fix: streamline no-shadow handling
pieh May 2, 2023
6e4d78a
Merge branch 'main' into fix/use-next-directory-when-resolving-server…
LekoArts May 3, 2023
8f71783
chore: automatic linting
LekoArts May 3, 2023
9efa6cb
chore: fix linting
LekoArts May 3, 2023
a92dbac
chore: post-commit linting :rolleyes:
LekoArts May 3, 2023
767dca9
Merge branch 'main' into fix/use-next-directory-when-resolving-server…
LekoArts May 3, 2023
40c6469
Merge branch 'main' into fix/use-next-directory-when-resolving-server…
LekoArts May 4, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/runtime/src/helpers/files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,7 @@ const patchFile = async ({
* The file we need has moved around a bit over the past few versions,
* so we iterate through the options until we find it
*/
const getServerFile = (root: string, includeBase = true) => {
export const getServerFile = (root: string, includeBase = true) => {
const candidates = ['next/dist/server/next-server', 'next/dist/next-server/server/next-server']

if (includeBase) {
Expand Down
7 changes: 6 additions & 1 deletion packages/runtime/src/helpers/functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,12 @@ export const generateFunctions = async (
}

const writeHandler = async (functionName: string, functionTitle: string, isODB: boolean) => {
const handlerSource = await getHandler({ isODB, publishDir, appDir: relative(functionDir, appDir) })
const handlerSource = await getHandler({
isODB,
publishDir,
appDir: relative(functionDir, appDir),
appDirAbsolute: appDir,
})
await ensureDir(join(functionsDir, functionName))

// write main handler file (standard or ODB)
Expand Down
46 changes: 37 additions & 9 deletions packages/runtime/src/templates/getHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ import type { Bridge as NodeBridge } from '@vercel/node-bridge/bridge'
import { outdent as javascript } from 'outdent'

import type { NextConfig } from '../helpers/config'
import { getServerFile } from '../helpers/files'

import { NextServerType } from './handlerUtils'
import type { getNetlifyNextServer as getNetlifyNextServerType } from './server'

type NetlifyNextServerType = ReturnType<typeof getNetlifyNextServerType>

/* eslint-disable @typescript-eslint/no-var-requires */
const { promises } = require('fs')
Expand All @@ -21,16 +27,24 @@ const {
getPrefetchResponse,
normalizePath,
} = require('./handlerUtils')
const { NetlifyNextServer } = require('./server')
const { getNetlifyNextServer } = require('./server')
/* eslint-enable @typescript-eslint/no-var-requires */

type Mutable<T> = {
-readonly [K in keyof T]: T[K]
}

// We return a function and then call `toString()` on it to serialise it as the launcher function
// eslint-disable-next-line max-params, max-lines-per-function
const makeHandler = (conf: NextConfig, app, pageRoot, staticManifest: Array<[string, string]> = [], mode = 'ssr') => {
// eslint-disable-next-line max-lines-per-function
const makeHandler = (
conf: NextConfig,
app: string,
pageRoot,
NextServer: NextServerType,
staticManifest: Array<[string, string]> = [],
mode = 'ssr',
// eslint-disable-next-line max-params
) => {
// Change working directory into the site root, unless using Nx, which moves the
// dist directory and handles this itself
const dir = path.resolve(__dirname, app)
Expand All @@ -44,6 +58,8 @@ const makeHandler = (conf: NextConfig, app, pageRoot, staticManifest: Array<[str
require.resolve('./pages.js')
} catch {}

const NetlifyNextServer: NetlifyNextServerType = getNetlifyNextServer(NextServer)

const ONE_YEAR_IN_SECONDS = 31536000

// React assumes you want development mode if NODE_ENV is unset.
Expand Down Expand Up @@ -86,7 +102,7 @@ const makeHandler = (conf: NextConfig, app, pageRoot, staticManifest: Array<[str
port,
},
{
revalidateToken: customContext.odb_refresh_hooks,
revalidateToken: customContext?.odb_refresh_hooks,
pieh marked this conversation as resolved.
Show resolved Hide resolved
},
)
const requestHandler = nextServer.getRequestHandler()
Expand Down Expand Up @@ -177,15 +193,26 @@ const makeHandler = (conf: NextConfig, app, pageRoot, staticManifest: Array<[str
}
}

export const getHandler = ({ isODB = false, publishDir = '../../../.next', appDir = '../../..' }): string =>
export const getHandler = ({
isODB = false,
publishDir = '../../../.next',
appDir = '../../..',
appDirAbsolute = process.cwd(),
}): string => {
const nextServerModuleLocation = getServerFile(appDirAbsolute, false)
if (!nextServerModuleLocation) {
throw new Error('Could not find next-server.js')
}

// This is a string, but if you have the right editor plugin it should format as js
javascript/* javascript */ `
return javascript/* javascript */ `
const { Server } = require("http");
const { promises } = require("fs");
// We copy the file here rather than requiring from the node module
const { Bridge } = require("./bridge");
const { augmentFsModule, getMaxAge, getMultiValueHeaders, getPrefetchResponse, getNextServer, normalizePath } = require('./handlerUtils')
const { NetlifyNextServer } = require('./server')
const { getNetlifyNextServer } = require('./server')
const NextServer = require(${JSON.stringify(nextServerModuleLocation)}).default

${isODB ? `const { builder } = require("@netlify/functions")` : ''}
const { config } = require("${publishDir}/required-server-files.json")
Expand All @@ -197,7 +224,8 @@ export const getHandler = ({ isODB = false, publishDir = '../../../.next', appDi
const pageRoot = path.resolve(path.join(__dirname, "${publishDir}", "server"));
exports.handler = ${
isODB
? `builder((${makeHandler.toString()})(config, "${appDir}", pageRoot, staticManifest, 'odb'));`
: `(${makeHandler.toString()})(config, "${appDir}", pageRoot, staticManifest, 'ssr');`
? `builder((${makeHandler.toString()})(config, "${appDir}", pageRoot, NextServer, staticManifest, 'odb'));`
: `(${makeHandler.toString()})(config, "${appDir}", pageRoot, NextServer, staticManifest, 'ssr');`
}
`
}
31 changes: 0 additions & 31 deletions packages/runtime/src/templates/handlerUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,37 +160,6 @@ export const augmentFsModule = ({
}) as typeof promises.stat
}

/**
* Next.js has an annoying habit of needing deep imports, but then moving those in patch releases. This is our abstraction.
*/
export const getNextServer = (): NextServerType => {
let NextServer: NextServerType
try {
// next >= 11.0.1. Yay breaking changes in patch releases!
// eslint-disable-next-line @typescript-eslint/no-var-requires
NextServer = require('next/dist/server/next-server').default
} catch (error) {
if (!error.message.includes("Cannot find module 'next/dist/server/next-server'")) {
// A different error, so rethrow it
throw error
}
// Probably an old version of next, so fall through and find it elsewhere.
}

if (!NextServer) {
try {
// next < 11.0.1
// eslint-disable-next-line n/no-missing-require, import/no-unresolved, @typescript-eslint/no-var-requires
NextServer = require('next/dist/next-server/server/next-server').default
} catch (error) {
if (!error.message.includes("Cannot find module 'next/dist/next-server/server/next-server'")) {
throw error
}
throw new Error('Could not find Next.js server')
}
}
return NextServer
}
/**
* Prefetch requests are used to check for middleware redirects, and shouldn't trigger SSR.
*/
Expand Down
145 changes: 73 additions & 72 deletions packages/runtime/src/templates/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,102 +3,103 @@ import { NodeRequestHandler, Options } from 'next/dist/server/next-server'

import {
netlifyApiFetch,
getNextServer,
NextServerType,
normalizeRoute,
localizeRoute,
localizeDataRoute,
unlocalizeRoute,
} from './handlerUtils'

const NextServer: NextServerType = getNextServer()

interface NetlifyConfig {
revalidateToken?: string
}

class NetlifyNextServer extends NextServer {
private netlifyConfig: NetlifyConfig
private netlifyPrerenderManifest: PrerenderManifest
const getNetlifyNextServer = (NextServer: NextServerType) => {
pieh marked this conversation as resolved.
Show resolved Hide resolved
class NetlifyNextServer extends NextServer {
private netlifyConfig: NetlifyConfig
private netlifyPrerenderManifest: PrerenderManifest

public constructor(options: Options, netlifyConfig: NetlifyConfig) {
super(options)
this.netlifyConfig = netlifyConfig
// copy the prerender manifest so it doesn't get mutated by Next.js
const manifest = this.getPrerenderManifest()
this.netlifyPrerenderManifest = {
...manifest,
routes: { ...manifest.routes },
dynamicRoutes: { ...manifest.dynamicRoutes },
public constructor(options: Options, netlifyConfig: NetlifyConfig) {
super(options)
this.netlifyConfig = netlifyConfig
// copy the prerender manifest so it doesn't get mutated by Next.js
const manifest = this.getPrerenderManifest()
this.netlifyPrerenderManifest = {
...manifest,
routes: { ...manifest.routes },
dynamicRoutes: { ...manifest.dynamicRoutes },
}
}
}

public getRequestHandler(): NodeRequestHandler {
const handler = super.getRequestHandler()
return async (req, res, parsedUrl) => {
// preserve the URL before Next.js mutates it for i18n
const { url, headers } = req
// handle the original res.revalidate() request
await handler(req, res, parsedUrl)
// handle on-demand revalidation by purging the ODB cache
if (res.statusCode === 200 && headers['x-prerender-revalidate'] && this.netlifyConfig.revalidateToken) {
await this.netlifyRevalidate(url)
public getRequestHandler(): NodeRequestHandler {
const handler = super.getRequestHandler()
return async (req, res, parsedUrl) => {
// preserve the URL before Next.js mutates it for i18n
const { url, headers } = req
// handle the original res.revalidate() request
await handler(req, res, parsedUrl)
// handle on-demand revalidation by purging the ODB cache
if (res.statusCode === 200 && headers['x-prerender-revalidate'] && this.netlifyConfig.revalidateToken) {
await this.netlifyRevalidate(url)
}
}
}
}

private async netlifyRevalidate(route: string) {
try {
// call netlify API to revalidate the path
const result = await netlifyApiFetch<{ ok: boolean; code: number; message: string }>({
endpoint: `sites/${process.env.SITE_ID}/refresh_on_demand_builders`,
payload: {
paths: this.getNetlifyPathsForRoute(route),
domain: this.hostname,
},
token: this.netlifyConfig.revalidateToken,
method: 'POST',
})
if (!result.ok) {
throw new Error(result.message)
private async netlifyRevalidate(route: string) {
try {
// call netlify API to revalidate the path
const result = await netlifyApiFetch<{ ok: boolean; code: number; message: string }>({
endpoint: `sites/${process.env.SITE_ID}/refresh_on_demand_builders`,
payload: {
paths: this.getNetlifyPathsForRoute(route),
domain: this.hostname,
},
token: this.netlifyConfig.revalidateToken,
method: 'POST',
})
if (!result.ok) {
throw new Error(result.message)
}
} catch (error) {
console.log(`Error revalidating ${route}:`, error.message)
throw error
}
} catch (error) {
console.log(`Error revalidating ${route}:`, error.message)
throw error
}
}

private getNetlifyPathsForRoute(route: string): string[] {
const { i18n } = this.nextConfig
const { routes, dynamicRoutes } = this.netlifyPrerenderManifest

// matches static routes
const normalizedRoute = normalizeRoute(i18n ? localizeRoute(route, i18n) : route)
if (normalizedRoute in routes) {
const { dataRoute } = routes[normalizedRoute]
const normalizedDataRoute = i18n ? localizeDataRoute(dataRoute, normalizedRoute) : dataRoute
return [route, normalizedDataRoute]
}
private getNetlifyPathsForRoute(route: string): string[] {
const { i18n } = this.nextConfig
const { routes, dynamicRoutes } = this.netlifyPrerenderManifest

// matches dynamic routes
const unlocalizedRoute = i18n ? unlocalizeRoute(normalizedRoute, i18n) : normalizedRoute
for (const dynamicRoute in dynamicRoutes) {
const { dataRoute, routeRegex } = dynamicRoutes[dynamicRoute]
const matches = unlocalizedRoute.match(routeRegex)
if (matches && matches.length !== 0) {
// remove the first match, which is the full route
matches.shift()
// replace the dynamic segments with the actual values
const interpolatedDataRoute = dataRoute.replace(/\[(.*?)]/g, () => matches.shift())
const normalizedDataRoute = i18n
? localizeDataRoute(interpolatedDataRoute, normalizedRoute)
: interpolatedDataRoute
// matches static routes
const normalizedRoute = normalizeRoute(i18n ? localizeRoute(route, i18n) : route)
if (normalizedRoute in routes) {
const { dataRoute } = routes[normalizedRoute]
const normalizedDataRoute = i18n ? localizeDataRoute(dataRoute, normalizedRoute) : dataRoute
return [route, normalizedDataRoute]
}
}

throw new Error(`not an ISR route`)
// matches dynamic routes
const unlocalizedRoute = i18n ? unlocalizeRoute(normalizedRoute, i18n) : normalizedRoute
for (const dynamicRoute in dynamicRoutes) {
const { dataRoute, routeRegex } = dynamicRoutes[dynamicRoute]
const matches = unlocalizedRoute.match(routeRegex)
if (matches && matches.length !== 0) {
pieh marked this conversation as resolved.
Show resolved Hide resolved
// remove the first match, which is the full route
matches.shift()
// replace the dynamic segments with the actual values
const interpolatedDataRoute = dataRoute.replace(/\[(.*?)]/g, () => matches.shift())
const normalizedDataRoute = i18n
? localizeDataRoute(interpolatedDataRoute, normalizedRoute)
: interpolatedDataRoute
return [route, normalizedDataRoute]
}
}

throw new Error(`not an ISR route`)
}
}

return NetlifyNextServer
}

export { NetlifyNextServer, NetlifyConfig }
export { getNetlifyNextServer, NetlifyConfig }