From 8d8d8ae7be8c0f383b79289c1bef9d23dcb8d83b Mon Sep 17 00:00:00 2001 From: Michal Piechowiak Date: Fri, 21 Apr 2023 12:49:47 +0200 Subject: [PATCH 01/28] fix: resolve next-server from next app directory and not from plugin --- packages/runtime/src/helpers/files.ts | 2 +- packages/runtime/src/helpers/functions.ts | 7 +- packages/runtime/src/templates/getHandler.ts | 46 ++++-- .../runtime/src/templates/handlerUtils.ts | 31 ---- packages/runtime/src/templates/server.ts | 145 +++++++++--------- 5 files changed, 117 insertions(+), 114 deletions(-) diff --git a/packages/runtime/src/helpers/files.ts b/packages/runtime/src/helpers/files.ts index 3d386d0c67..6919b50db5 100644 --- a/packages/runtime/src/helpers/files.ts +++ b/packages/runtime/src/helpers/files.ts @@ -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) { diff --git a/packages/runtime/src/helpers/functions.ts b/packages/runtime/src/helpers/functions.ts index 5d0f8032b7..c9922427f2 100644 --- a/packages/runtime/src/helpers/functions.ts +++ b/packages/runtime/src/helpers/functions.ts @@ -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) diff --git a/packages/runtime/src/templates/getHandler.ts b/packages/runtime/src/templates/getHandler.ts index cde440b4f8..dbd623a1c7 100644 --- a/packages/runtime/src/templates/getHandler.ts +++ b/packages/runtime/src/templates/getHandler.ts @@ -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 /* eslint-disable @typescript-eslint/no-var-requires */ const { promises } = require('fs') @@ -21,7 +27,7 @@ const { getPrefetchResponse, normalizePath, } = require('./handlerUtils') -const { NetlifyNextServer } = require('./server') +const { getNetlifyNextServer } = require('./server') /* eslint-enable @typescript-eslint/no-var-requires */ type Mutable = { @@ -29,8 +35,16 @@ type Mutable = { } // 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) @@ -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. @@ -86,7 +102,7 @@ const makeHandler = (conf: NextConfig, app, pageRoot, staticManifest: Array<[str port, }, { - revalidateToken: customContext.odb_refresh_hooks, + revalidateToken: customContext?.odb_refresh_hooks, }, ) const requestHandler = nextServer.getRequestHandler() @@ -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") @@ -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');` } ` +} diff --git a/packages/runtime/src/templates/handlerUtils.ts b/packages/runtime/src/templates/handlerUtils.ts index cbb26e9d58..0d985c21a3 100644 --- a/packages/runtime/src/templates/handlerUtils.ts +++ b/packages/runtime/src/templates/handlerUtils.ts @@ -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. */ diff --git a/packages/runtime/src/templates/server.ts b/packages/runtime/src/templates/server.ts index ad0908bc3b..c552c5587f 100644 --- a/packages/runtime/src/templates/server.ts +++ b/packages/runtime/src/templates/server.ts @@ -3,7 +3,6 @@ import { NodeRequestHandler, Options } from 'next/dist/server/next-server' import { netlifyApiFetch, - getNextServer, NextServerType, normalizeRoute, localizeRoute, @@ -11,94 +10,96 @@ import { 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) => { + 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) { + // 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 } From e7ae048b19b59521e87fd9bc4e893df64be57b4e Mon Sep 17 00:00:00 2001 From: Michal Piechowiak Date: Fri, 21 Apr 2023 13:21:15 +0200 Subject: [PATCH 02/28] fix: update api handler --- packages/runtime/src/helpers/functions.ts | 1 + .../runtime/src/templates/getApiHandler.ts | 25 +++++++++++++------ packages/runtime/src/templates/getHandler.ts | 4 +-- 3 files changed, 20 insertions(+), 10 deletions(-) diff --git a/packages/runtime/src/helpers/functions.ts b/packages/runtime/src/helpers/functions.ts index c9922427f2..32c10c9be4 100644 --- a/packages/runtime/src/helpers/functions.ts +++ b/packages/runtime/src/helpers/functions.ts @@ -51,6 +51,7 @@ export const generateFunctions = async ( config, publishDir, appDir: relative(functionDir, appDir), + appDirAbsolute: appDir, }) const functionName = getFunctionNameForPage(route, config.type === ApiRouteType.BACKGROUND) await ensureDir(join(functionsDir, functionName)) diff --git a/packages/runtime/src/templates/getApiHandler.ts b/packages/runtime/src/templates/getApiHandler.ts index 81d40e0674..d92ad56b85 100644 --- a/packages/runtime/src/templates/getApiHandler.ts +++ b/packages/runtime/src/templates/getApiHandler.ts @@ -5,6 +5,7 @@ import { outdent as javascript } from 'outdent' import { ApiConfig, ApiRouteType } from '../helpers/analysis' import type { NextConfig } from '../helpers/config' +import { getServerFile } from '../helpers/files' import type { NextServerType } from './handlerUtils' @@ -17,7 +18,7 @@ const { URLSearchParams, URL } = require('url') const { Bridge } = require('@vercel/node-bridge/bridge') -const { getMultiValueHeaders, getNextServer } = require('./handlerUtils') +const { getMultiValueHeaders } = require('./handlerUtils') /* eslint-enable @typescript-eslint/no-var-requires */ type Mutable = { @@ -25,8 +26,8 @@ type Mutable = { } // We return a function and then call `toString()` on it to serialise it as the launcher function - -const makeHandler = (conf: NextConfig, app, pageRoot, page) => { +// eslint-disable-next-line max-params +const makeHandler = (conf: NextConfig, app, pageRoot, page, NextServer: NextServerType) => { // 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) @@ -64,7 +65,6 @@ const makeHandler = (conf: NextConfig, app, pageRoot, page) => { const url = event.rawUrl ? new URL(event.rawUrl) : new URL(path, process.env.URL || 'http://n') const port = Number.parseInt(url.port) || 80 - const NextServer: NextServerType = getNextServer() const nextServer = new NextServer({ conf, dir, @@ -118,18 +118,26 @@ export const getApiHandler = ({ config, publishDir = '../../../.next', appDir = '../../..', + appDirAbsolute = process.cwd(), }: { page: string config: ApiConfig publishDir?: string appDir?: string -}): string => + appDirAbsolute: string +}): string => { + const nextServerModuleLocation = getServerFile(appDirAbsolute, false) + if (!nextServerModuleLocation) { + throw new Error('Could not find Next.js server') + } + // 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"); // We copy the file here rather than requiring from the node module const { Bridge } = require("./bridge"); - const { getMultiValueHeaders, getNextServer } = require('./handlerUtils') + const { getMultiValueHeaders } = require('./handlerUtils') + const NextServer = require(${JSON.stringify(nextServerModuleLocation)}).default ${config.type === ApiRouteType.SCHEDULED ? `const { schedule } = require("@netlify/functions")` : ''} @@ -138,8 +146,9 @@ export const getApiHandler = ({ let staticManifest const path = require("path"); const pageRoot = path.resolve(path.join(__dirname, "${publishDir}", "server")); - const handler = (${makeHandler.toString()})(config, "${appDir}", pageRoot, ${JSON.stringify(page)}) + const handler = (${makeHandler.toString()})(config, "${appDir}", pageRoot, ${JSON.stringify(page)}, NextServer) exports.handler = ${ config.type === ApiRouteType.SCHEDULED ? `schedule(${JSON.stringify(config.schedule)}, handler);` : 'handler' } ` +} diff --git a/packages/runtime/src/templates/getHandler.ts b/packages/runtime/src/templates/getHandler.ts index dbd623a1c7..bb680661bb 100644 --- a/packages/runtime/src/templates/getHandler.ts +++ b/packages/runtime/src/templates/getHandler.ts @@ -201,7 +201,7 @@ export const getHandler = ({ }): string => { const nextServerModuleLocation = getServerFile(appDirAbsolute, false) if (!nextServerModuleLocation) { - throw new Error('Could not find next-server.js') + throw new Error('Could not find Next.js server') } // This is a string, but if you have the right editor plugin it should format as js @@ -210,7 +210,7 @@ export const getHandler = ({ 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 { augmentFsModule, getMaxAge, getMultiValueHeaders, getPrefetchResponse, normalizePath } = require('./handlerUtils') const { getNetlifyNextServer } = require('./server') const NextServer = require(${JSON.stringify(nextServerModuleLocation)}).default From 15c1bdda222e39b68ad06124b431c377d49ceeff Mon Sep 17 00:00:00 2001 From: Michal Piechowiak Date: Fri, 21 Apr 2023 13:53:38 +0200 Subject: [PATCH 03/28] chore: export type from server module --- packages/runtime/src/templates/getHandler.ts | 4 +--- packages/runtime/src/templates/server.ts | 2 ++ 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/runtime/src/templates/getHandler.ts b/packages/runtime/src/templates/getHandler.ts index bb680661bb..ce9e9d7af8 100644 --- a/packages/runtime/src/templates/getHandler.ts +++ b/packages/runtime/src/templates/getHandler.ts @@ -7,9 +7,7 @@ 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 +import type { NetlifyNextServerType } from './server' /* eslint-disable @typescript-eslint/no-var-requires */ const { promises } = require('fs') diff --git a/packages/runtime/src/templates/server.ts b/packages/runtime/src/templates/server.ts index c552c5587f..d241b02fc6 100644 --- a/packages/runtime/src/templates/server.ts +++ b/packages/runtime/src/templates/server.ts @@ -102,4 +102,6 @@ const getNetlifyNextServer = (NextServer: NextServerType) => { return NetlifyNextServer } +export type NetlifyNextServerType = ReturnType + export { getNetlifyNextServer, NetlifyConfig } From 4b64c17c8511a4cbf235124689880171361a7683 Mon Sep 17 00:00:00 2001 From: Michal Piechowiak Date: Fri, 21 Apr 2023 13:54:00 +0200 Subject: [PATCH 04/28] fix: server.spec.ts --- test/templates/server.spec.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/test/templates/server.spec.ts b/test/templates/server.spec.ts index 23f9337e83..e23c872071 100644 --- a/test/templates/server.spec.ts +++ b/test/templates/server.spec.ts @@ -1,8 +1,9 @@ import { mockRequest } from 'next/dist/server/lib/mock-request' import { Options } from 'next/dist/server/next-server' -import { getNextServer, NextServerType, netlifyApiFetch } from '../../packages/runtime/src/templates/handlerUtils' -import { NetlifyNextServer, NetlifyConfig } from '../../packages/runtime/src/templates/server' +import { NextServerType, netlifyApiFetch } from '../../packages/runtime/src/templates/handlerUtils' +import { getServerFile } from '../../packages/runtime/src/helpers/files' +import { getNetlifyNextServer, NetlifyNextServerType, NetlifyConfig } from '../../packages/runtime/src/templates/server' jest.mock('../../packages/runtime/src/templates/handlerUtils', () => { const originalModule = jest.requireActual('../../packages/runtime/src/templates/handlerUtils') @@ -53,9 +54,11 @@ jest.mock( { virtual: true }, ) +let NetlifyNextServer: NetlifyNextServerType beforeAll(() => { - const NextServer: NextServerType = getNextServer() + const NextServer: NextServerType = require(getServerFile(__dirname, false)).default jest.spyOn(NextServer.prototype, 'getRequestHandler').mockImplementation(() => () => Promise.resolve()) + NetlifyNextServer = getNetlifyNextServer(NextServer) const MockNetlifyNextServerConstructor = function (nextOptions: Options, netlifyConfig: NetlifyConfig) { this.distDir = '.' @@ -63,6 +66,7 @@ beforeAll(() => { this.nextConfig = nextOptions.conf this.netlifyConfig = netlifyConfig } + Object.setPrototypeOf(NetlifyNextServer, MockNetlifyNextServerConstructor) }) From 193382464c0f4a174f7b42fc0dbb83393ec5f0ff Mon Sep 17 00:00:00 2001 From: Michal Piechowiak Date: Fri, 21 Apr 2023 16:22:16 +0200 Subject: [PATCH 05/28] chore: move next server throwing to runtime --- packages/runtime/src/templates/getApiHandler.ts | 7 ++++--- packages/runtime/src/templates/getHandler.ts | 7 ++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/runtime/src/templates/getApiHandler.ts b/packages/runtime/src/templates/getApiHandler.ts index d92ad56b85..27663ac776 100644 --- a/packages/runtime/src/templates/getApiHandler.ts +++ b/packages/runtime/src/templates/getApiHandler.ts @@ -127,12 +127,13 @@ export const getApiHandler = ({ appDirAbsolute: string }): string => { const nextServerModuleLocation = getServerFile(appDirAbsolute, false) - if (!nextServerModuleLocation) { - throw new Error('Could not find Next.js server') - } // This is a string, but if you have the right editor plugin it should format as js return javascript/* javascript */ ` + if (!${JSON.stringify(nextServerModuleLocation)}) { + throw new Error('Could not find Next.js server') + } + const { Server } = require("http"); // We copy the file here rather than requiring from the node module const { Bridge } = require("./bridge"); diff --git a/packages/runtime/src/templates/getHandler.ts b/packages/runtime/src/templates/getHandler.ts index ce9e9d7af8..171e74e0bd 100644 --- a/packages/runtime/src/templates/getHandler.ts +++ b/packages/runtime/src/templates/getHandler.ts @@ -198,12 +198,13 @@ export const getHandler = ({ appDirAbsolute = process.cwd(), }): string => { const nextServerModuleLocation = getServerFile(appDirAbsolute, false) - if (!nextServerModuleLocation) { - throw new Error('Could not find Next.js server') - } // This is a string, but if you have the right editor plugin it should format as js return javascript/* javascript */ ` + if (!${JSON.stringify(nextServerModuleLocation)}) { + throw new Error('Could not find Next.js server') + } + const { Server } = require("http"); const { promises } = require("fs"); // We copy the file here rather than requiring from the node module From e06157d41ae179fd9e687e4590b9e30757260d21 Mon Sep 17 00:00:00 2001 From: Michal Piechowiak Date: Fri, 21 Apr 2023 16:34:32 +0200 Subject: [PATCH 06/28] fix: restore trying to resolve from plugin and not appdir --- packages/runtime/src/helpers/files.ts | 50 ++++++++++++++++++- .../runtime/src/templates/getApiHandler.ts | 4 +- packages/runtime/src/templates/getHandler.ts | 4 +- 3 files changed, 53 insertions(+), 5 deletions(-) diff --git a/packages/runtime/src/helpers/files.ts b/packages/runtime/src/helpers/files.ts index 6919b50db5..4022e7802d 100644 --- a/packages/runtime/src/helpers/files.ts +++ b/packages/runtime/src/helpers/files.ts @@ -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 */ -export const getServerFile = (root: string, includeBase = true) => { +const getServerFile = (root: string, includeBase = true) => { const candidates = ['next/dist/server/next-server', 'next/dist/next-server/server/next-server'] if (includeBase) { @@ -340,6 +340,54 @@ export const getServerFile = (root: string, includeBase = true) => { return findModuleFromBase({ candidates, paths: [root] }) } +/** + * Try to find next-server module in few locations (to support different next versions) and in few context (try to resolve from app location and from this module) + */ +export const getNextServerModulePath = (root: string): string | null => { + // first let's try to use app location directory to find next-server + try { + const nextServerModuleLocation = getServerFile(root, false) + if (nextServerModuleLocation) { + return nextServerModuleLocation + } + } catch (error) { + if (!error.message.includes('Cannot find module')) { + // A different error, so rethrow it + throw error + } + } + + // if we didn't find it, let's try to resolve "next" package from this module + try { + // next >= 11.0.1. Yay breaking changes in patch releases! + const nextServerModuleLocation = require.resolve('next/dist/server/next-server') + if (nextServerModuleLocation) { + return nextServerModuleLocation + } + } 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. + } + + try { + // next < 11.0.1 + // eslint-disable-next-line n/no-missing-require + const nextServerModuleLocation = require.resolve('next/dist/next-server/server/next-server') + if (nextServerModuleLocation) { + return nextServerModuleLocation + } + } catch (error) { + if (!error.message.includes("Cannot find module 'next/dist/next-server/server/next-server'")) { + throw error + } + } + + return null +} + /** * Find the source file for a given page route */ diff --git a/packages/runtime/src/templates/getApiHandler.ts b/packages/runtime/src/templates/getApiHandler.ts index 27663ac776..373756d393 100644 --- a/packages/runtime/src/templates/getApiHandler.ts +++ b/packages/runtime/src/templates/getApiHandler.ts @@ -5,7 +5,7 @@ import { outdent as javascript } from 'outdent' import { ApiConfig, ApiRouteType } from '../helpers/analysis' import type { NextConfig } from '../helpers/config' -import { getServerFile } from '../helpers/files' +import { getNextServerModulePath } from '../helpers/files' import type { NextServerType } from './handlerUtils' @@ -126,7 +126,7 @@ export const getApiHandler = ({ appDir?: string appDirAbsolute: string }): string => { - const nextServerModuleLocation = getServerFile(appDirAbsolute, false) + const nextServerModuleLocation = getNextServerModulePath(appDirAbsolute) // This is a string, but if you have the right editor plugin it should format as js return javascript/* javascript */ ` diff --git a/packages/runtime/src/templates/getHandler.ts b/packages/runtime/src/templates/getHandler.ts index 171e74e0bd..556cf7e5cd 100644 --- a/packages/runtime/src/templates/getHandler.ts +++ b/packages/runtime/src/templates/getHandler.ts @@ -4,7 +4,7 @@ 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 { getNextServerModulePath } from '../helpers/files' import { NextServerType } from './handlerUtils' import type { NetlifyNextServerType } from './server' @@ -197,7 +197,7 @@ export const getHandler = ({ appDir = '../../..', appDirAbsolute = process.cwd(), }): string => { - const nextServerModuleLocation = getServerFile(appDirAbsolute, false) + const nextServerModuleLocation = getNextServerModulePath(appDirAbsolute) // This is a string, but if you have the right editor plugin it should format as js return javascript/* javascript */ ` From ffae6a21bbb4efe7f347b8a0ac76d2e192299270 Mon Sep 17 00:00:00 2001 From: Michal Piechowiak Date: Fri, 21 Apr 2023 17:16:28 +0200 Subject: [PATCH 07/28] fix: use relative import paths not absolute --- packages/runtime/src/helpers/functions.ts | 11 ++++++++--- packages/runtime/src/templates/getApiHandler.ts | 16 +++++----------- packages/runtime/src/templates/getHandler.ts | 14 ++++---------- 3 files changed, 17 insertions(+), 24 deletions(-) diff --git a/packages/runtime/src/helpers/functions.ts b/packages/runtime/src/helpers/functions.ts index 32c10c9be4..62a0a3c95d 100644 --- a/packages/runtime/src/helpers/functions.ts +++ b/packages/runtime/src/helpers/functions.ts @@ -21,7 +21,7 @@ import { getHandler } from '../templates/getHandler' import { getResolverForPages, getResolverForSourceFiles } from '../templates/getPageResolver' import { ApiConfig, ApiRouteType, extractConfigFromFile, isEdgeConfig } from './analysis' -import { getSourceFileForPage } from './files' +import { getNextServerModulePath, getSourceFileForPage } from './files' import { writeFunctionConfiguration } from './functionsMetaData' import { getFunctionNameForPage } from './utils' @@ -41,6 +41,11 @@ export const generateFunctions = async ( const functionDir = join(functionsDir, HANDLER_FUNCTION_NAME) const publishDir = relative(functionDir, publish) + const nextServerModuleAbsoluteLocation = getNextServerModulePath(appDir) + const nextServerModuleRelativeLocation = nextServerModuleAbsoluteLocation + ? relative(functionDir, nextServerModuleAbsoluteLocation) + : undefined + for (const { route, config, compiled } of apiRoutes) { // Don't write a lambda if the runtime is edge if (isEdgeConfig(config.runtime)) { @@ -51,7 +56,7 @@ export const generateFunctions = async ( config, publishDir, appDir: relative(functionDir, appDir), - appDirAbsolute: appDir, + nextServerModuleRelativeLocation, }) const functionName = getFunctionNameForPage(route, config.type === ApiRouteType.BACKGROUND) await ensureDir(join(functionsDir, functionName)) @@ -85,7 +90,7 @@ export const generateFunctions = async ( isODB, publishDir, appDir: relative(functionDir, appDir), - appDirAbsolute: appDir, + nextServerModuleRelativeLocation, }) await ensureDir(join(functionsDir, functionName)) diff --git a/packages/runtime/src/templates/getApiHandler.ts b/packages/runtime/src/templates/getApiHandler.ts index 373756d393..9ed49f8378 100644 --- a/packages/runtime/src/templates/getApiHandler.ts +++ b/packages/runtime/src/templates/getApiHandler.ts @@ -5,7 +5,6 @@ import { outdent as javascript } from 'outdent' import { ApiConfig, ApiRouteType } from '../helpers/analysis' import type { NextConfig } from '../helpers/config' -import { getNextServerModulePath } from '../helpers/files' import type { NextServerType } from './handlerUtils' @@ -118,19 +117,15 @@ export const getApiHandler = ({ config, publishDir = '../../../.next', appDir = '../../..', - appDirAbsolute = process.cwd(), + nextServerModuleRelativeLocation, }: { page: string config: ApiConfig publishDir?: string appDir?: string - appDirAbsolute: string -}): string => { - const nextServerModuleLocation = getNextServerModulePath(appDirAbsolute) - - // This is a string, but if you have the right editor plugin it should format as js - return javascript/* javascript */ ` - if (!${JSON.stringify(nextServerModuleLocation)}) { + nextServerModuleRelativeLocation: string | undefined +}): string => javascript/* javascript */ ` + if (!${JSON.stringify(nextServerModuleRelativeLocation)}) { throw new Error('Could not find Next.js server') } @@ -138,7 +133,7 @@ export const getApiHandler = ({ // We copy the file here rather than requiring from the node module const { Bridge } = require("./bridge"); const { getMultiValueHeaders } = require('./handlerUtils') - const NextServer = require(${JSON.stringify(nextServerModuleLocation)}).default + const NextServer = require(${JSON.stringify(nextServerModuleRelativeLocation)}).default ${config.type === ApiRouteType.SCHEDULED ? `const { schedule } = require("@netlify/functions")` : ''} @@ -152,4 +147,3 @@ export const getApiHandler = ({ config.type === ApiRouteType.SCHEDULED ? `schedule(${JSON.stringify(config.schedule)}, handler);` : 'handler' } ` -} diff --git a/packages/runtime/src/templates/getHandler.ts b/packages/runtime/src/templates/getHandler.ts index 556cf7e5cd..dfc2d08a2a 100644 --- a/packages/runtime/src/templates/getHandler.ts +++ b/packages/runtime/src/templates/getHandler.ts @@ -4,7 +4,6 @@ import type { Bridge as NodeBridge } from '@vercel/node-bridge/bridge' import { outdent as javascript } from 'outdent' import type { NextConfig } from '../helpers/config' -import { getNextServerModulePath } from '../helpers/files' import { NextServerType } from './handlerUtils' import type { NetlifyNextServerType } from './server' @@ -195,13 +194,9 @@ export const getHandler = ({ isODB = false, publishDir = '../../../.next', appDir = '../../..', - appDirAbsolute = process.cwd(), -}): string => { - const nextServerModuleLocation = getNextServerModulePath(appDirAbsolute) - - // This is a string, but if you have the right editor plugin it should format as js - return javascript/* javascript */ ` - if (!${JSON.stringify(nextServerModuleLocation)}) { + nextServerModuleRelativeLocation, +}): string => javascript/* javascript */ ` + if (!${JSON.stringify(nextServerModuleRelativeLocation)}) { throw new Error('Could not find Next.js server') } @@ -211,7 +206,7 @@ export const getHandler = ({ const { Bridge } = require("./bridge"); const { augmentFsModule, getMaxAge, getMultiValueHeaders, getPrefetchResponse, normalizePath } = require('./handlerUtils') const { getNetlifyNextServer } = require('./server') - const NextServer = require(${JSON.stringify(nextServerModuleLocation)}).default + const NextServer = require(${JSON.stringify(nextServerModuleRelativeLocation)}).default ${isODB ? `const { builder } = require("@netlify/functions")` : ''} const { config } = require("${publishDir}/required-server-files.json") @@ -227,4 +222,3 @@ export const getHandler = ({ : `(${makeHandler.toString()})(config, "${appDir}", pageRoot, NextServer, staticManifest, 'ssr');` } ` -} From d50c047da250c71b6eafb0eb9de7eb7ff6fac4bf Mon Sep 17 00:00:00 2001 From: Michal Piechowiak Date: Fri, 21 Apr 2023 17:34:21 +0200 Subject: [PATCH 08/28] fix: server.spec.ts (again) --- test/templates/server.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/templates/server.spec.ts b/test/templates/server.spec.ts index e23c872071..53ca0d2fd9 100644 --- a/test/templates/server.spec.ts +++ b/test/templates/server.spec.ts @@ -2,7 +2,7 @@ import { mockRequest } from 'next/dist/server/lib/mock-request' import { Options } from 'next/dist/server/next-server' import { NextServerType, netlifyApiFetch } from '../../packages/runtime/src/templates/handlerUtils' -import { getServerFile } from '../../packages/runtime/src/helpers/files' +import { getNextServerModulePath } from '../../packages/runtime/src/helpers/files' import { getNetlifyNextServer, NetlifyNextServerType, NetlifyConfig } from '../../packages/runtime/src/templates/server' jest.mock('../../packages/runtime/src/templates/handlerUtils', () => { @@ -56,7 +56,7 @@ jest.mock( let NetlifyNextServer: NetlifyNextServerType beforeAll(() => { - const NextServer: NextServerType = require(getServerFile(__dirname, false)).default + const NextServer: NextServerType = require(getNextServerModulePath(__dirname)).default jest.spyOn(NextServer.prototype, 'getRequestHandler').mockImplementation(() => () => Promise.resolve()) NetlifyNextServer = getNetlifyNextServer(NextServer) From c5a4afddcf91044d27429787bdbee2c3f684c02c Mon Sep 17 00:00:00 2001 From: Michal Piechowiak Date: Fri, 21 Apr 2023 17:36:40 +0200 Subject: [PATCH 09/28] fix: index.spec.ts --- test/index.spec.ts | 75 ++++++++++++++++++++++------------------------ 1 file changed, 35 insertions(+), 40 deletions(-) diff --git a/test/index.spec.ts b/test/index.spec.ts index cc3310c3bc..bd9f8c525e 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -16,42 +16,29 @@ jest.mock('../packages/runtime/src/helpers/functionsMetaData', () => { } }) -import Chance from "chance" -import { - writeJSON, - unlink, - existsSync, - readFileSync, - ensureDir, - readJson, - pathExists, - writeFile, - move, -} from "fs-extra" -import path from "path" -import process from "process" -import os from "os" -import { dir as getTmpDir } from "tmp-promise" +import Chance from 'chance' +import { writeJSON, unlink, existsSync, readFileSync, ensureDir, readJson, pathExists, writeFile, move } from 'fs-extra' +import path from 'path' +import process from 'process' +import os from 'os' +import { dir as getTmpDir } from 'tmp-promise' // @ts-expect-error - TODO: Convert runtime export to ES6 -import nextRuntimeFactory from "../packages/runtime/src" +import nextRuntimeFactory from '../packages/runtime/src' const nextRuntime = nextRuntimeFactory({}) -import { watchForMiddlewareChanges } from "../packages/runtime/src/helpers/compiler" -import { HANDLER_FUNCTION_NAME, ODB_FUNCTION_NAME, IMAGE_FUNCTION_NAME } from "../packages/runtime/src/constants" -import { join } from "pathe" -import { - getRequiredServerFiles, - updateRequiredServerFiles, -} from "../packages/runtime/src/helpers/config" -import { resolve } from "path" +import { watchForMiddlewareChanges } from '../packages/runtime/src/helpers/compiler' +import { HANDLER_FUNCTION_NAME, ODB_FUNCTION_NAME, IMAGE_FUNCTION_NAME } from '../packages/runtime/src/constants' +import { join } from 'pathe' +import { getRequiredServerFiles, updateRequiredServerFiles } from '../packages/runtime/src/helpers/config' +import { resolve } from 'path' import type { NetlifyPluginOptions } from '@netlify/build' -import { changeCwd, useFixture, moveNextDist } from "./test-utils" +import { changeCwd, useFixture, moveNextDist } from './test-utils' const chance = new Chance() const constants = { INTERNAL_FUNCTIONS_SRC: '.netlify/functions-internal', PUBLISH_DIR: '.next', FUNCTIONS_DIST: '.netlify/functions', -} as unknown as NetlifyPluginOptions["constants"] +} as unknown as NetlifyPluginOptions['constants'] const utils = { build: { failBuild(message) { @@ -63,14 +50,19 @@ const utils = { save: jest.fn(), restore: jest.fn(), }, -} as unknown as NetlifyPluginOptions["utils"] +} as unknown as NetlifyPluginOptions['utils'] const normalizeChunkNames = (source) => source.replaceAll(/\/chunks\/\d+\.js/g, '/chunks/CHUNK_ID.js') const onBuildHasRun = (netlifyConfig) => Boolean(netlifyConfig.functions[HANDLER_FUNCTION_NAME]?.included_files?.some((file) => file.includes('BUILD_ID'))) -const netlifyConfig = { build: { command: 'npm run build' }, functions: {}, redirects: [], headers: [] } as NetlifyPluginOptions["netlifyConfig"] +const netlifyConfig = { + build: { command: 'npm run build' }, + functions: {}, + redirects: [], + headers: [], +} as NetlifyPluginOptions['netlifyConfig'] const defaultArgs = { netlifyConfig, utils, @@ -560,8 +552,12 @@ describe('onBuild()', () => { expect(existsSync(handlerFile)).toBeTruthy() expect(existsSync(odbHandlerFile)).toBeTruthy() - expect(readFileSync(handlerFile, 'utf8')).toMatch(`(config, "../../..", pageRoot, staticManifest, 'ssr')`) - expect(readFileSync(odbHandlerFile, 'utf8')).toMatch(`(config, "../../..", pageRoot, staticManifest, 'odb')`) + expect(readFileSync(handlerFile, 'utf8')).toMatch( + `(config, "../../..", pageRoot, NextServer, staticManifest, 'ssr')`, + ) + expect(readFileSync(odbHandlerFile, 'utf8')).toMatch( + `(config, "../../..", pageRoot, NextServer, staticManifest, 'odb')`, + ) expect(readFileSync(handlerFile, 'utf8')).toMatch(`require("../../../.next/required-server-files.json")`) expect(readFileSync(odbHandlerFile, 'utf8')).toMatch(`require("../../../.next/required-server-files.json")`) }) @@ -637,30 +633,29 @@ describe('onBuild()', () => { await nextRuntime.onBuild(defaultArgs) const manifestPath = await readJson(path.resolve('.netlify/edge-functions/manifest.json')) const manifest = manifestPath.functions - + expect(manifest).toEqual( expect.arrayContaining([ expect.objectContaining({ - generator: '@netlify/next-runtime@1.0.0' - }) - ]) + generator: '@netlify/next-runtime@1.0.0', + }), + ]), ) }) - test('generates generator field within the edge-functions manifest includes IPX', async () => { process.env.NEXT_FORCE_EDGE_IMAGES = '1' await moveNextDist() await nextRuntime.onBuild(defaultArgs) const manifestPath = await readJson(path.resolve('.netlify/edge-functions/manifest.json')) const manifest = manifestPath.functions - + expect(manifest).toEqual( expect.arrayContaining([ expect.objectContaining({ - generator: '@netlify/next-runtime@1.0.0' - }) - ]) + generator: '@netlify/next-runtime@1.0.0', + }), + ]), ) }) From 48468bc39523b5f88b1c594f18ae6e920a6d58bb Mon Sep 17 00:00:00 2001 From: Michal Piechowiak Date: Fri, 21 Apr 2023 17:59:50 +0200 Subject: [PATCH 10/28] chore: diff cleanup --- .../runtime/src/templates/getApiHandler.ts | 4 +- packages/runtime/src/templates/getHandler.ts | 6 +- test/index.spec.ts | 75 ++++++++++--------- test/templates/server.spec.ts | 1 - 4 files changed, 47 insertions(+), 39 deletions(-) diff --git a/packages/runtime/src/templates/getApiHandler.ts b/packages/runtime/src/templates/getApiHandler.ts index 9ed49f8378..b11ed866c4 100644 --- a/packages/runtime/src/templates/getApiHandler.ts +++ b/packages/runtime/src/templates/getApiHandler.ts @@ -124,7 +124,9 @@ export const getApiHandler = ({ publishDir?: string appDir?: string nextServerModuleRelativeLocation: string | undefined -}): string => javascript/* javascript */ ` +}): string => + // This is a string, but if you have the right editor plugin it should format as js + javascript/* javascript */ ` if (!${JSON.stringify(nextServerModuleRelativeLocation)}) { throw new Error('Could not find Next.js server') } diff --git a/packages/runtime/src/templates/getHandler.ts b/packages/runtime/src/templates/getHandler.ts index dfc2d08a2a..6cf633918d 100644 --- a/packages/runtime/src/templates/getHandler.ts +++ b/packages/runtime/src/templates/getHandler.ts @@ -5,7 +5,7 @@ import { outdent as javascript } from 'outdent' import type { NextConfig } from '../helpers/config' -import { NextServerType } from './handlerUtils' +import type { NextServerType } from './handlerUtils' import type { NetlifyNextServerType } from './server' /* eslint-disable @typescript-eslint/no-var-requires */ @@ -195,7 +195,9 @@ export const getHandler = ({ publishDir = '../../../.next', appDir = '../../..', nextServerModuleRelativeLocation, -}): string => javascript/* javascript */ ` +}): string => + // This is a string, but if you have the right editor plugin it should format as js + javascript/* javascript */ ` if (!${JSON.stringify(nextServerModuleRelativeLocation)}) { throw new Error('Could not find Next.js server') } diff --git a/test/index.spec.ts b/test/index.spec.ts index bd9f8c525e..7f24001871 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -16,29 +16,42 @@ jest.mock('../packages/runtime/src/helpers/functionsMetaData', () => { } }) -import Chance from 'chance' -import { writeJSON, unlink, existsSync, readFileSync, ensureDir, readJson, pathExists, writeFile, move } from 'fs-extra' -import path from 'path' -import process from 'process' -import os from 'os' -import { dir as getTmpDir } from 'tmp-promise' +import Chance from "chance" +import { + writeJSON, + unlink, + existsSync, + readFileSync, + ensureDir, + readJson, + pathExists, + writeFile, + move, +} from "fs-extra" +import path from "path" +import process from "process" +import os from "os" +import { dir as getTmpDir } from "tmp-promise" // @ts-expect-error - TODO: Convert runtime export to ES6 -import nextRuntimeFactory from '../packages/runtime/src' +import nextRuntimeFactory from "../packages/runtime/src" const nextRuntime = nextRuntimeFactory({}) -import { watchForMiddlewareChanges } from '../packages/runtime/src/helpers/compiler' -import { HANDLER_FUNCTION_NAME, ODB_FUNCTION_NAME, IMAGE_FUNCTION_NAME } from '../packages/runtime/src/constants' -import { join } from 'pathe' -import { getRequiredServerFiles, updateRequiredServerFiles } from '../packages/runtime/src/helpers/config' -import { resolve } from 'path' +import { watchForMiddlewareChanges } from "../packages/runtime/src/helpers/compiler" +import { HANDLER_FUNCTION_NAME, ODB_FUNCTION_NAME, IMAGE_FUNCTION_NAME } from "../packages/runtime/src/constants" +import { join } from "pathe" +import { + getRequiredServerFiles, + updateRequiredServerFiles, +} from "../packages/runtime/src/helpers/config" +import { resolve } from "path" import type { NetlifyPluginOptions } from '@netlify/build' -import { changeCwd, useFixture, moveNextDist } from './test-utils' +import { changeCwd, useFixture, moveNextDist } from "./test-utils" const chance = new Chance() const constants = { INTERNAL_FUNCTIONS_SRC: '.netlify/functions-internal', PUBLISH_DIR: '.next', FUNCTIONS_DIST: '.netlify/functions', -} as unknown as NetlifyPluginOptions['constants'] +} as unknown as NetlifyPluginOptions["constants"] const utils = { build: { failBuild(message) { @@ -50,19 +63,14 @@ const utils = { save: jest.fn(), restore: jest.fn(), }, -} as unknown as NetlifyPluginOptions['utils'] +} as unknown as NetlifyPluginOptions["utils"] const normalizeChunkNames = (source) => source.replaceAll(/\/chunks\/\d+\.js/g, '/chunks/CHUNK_ID.js') const onBuildHasRun = (netlifyConfig) => Boolean(netlifyConfig.functions[HANDLER_FUNCTION_NAME]?.included_files?.some((file) => file.includes('BUILD_ID'))) -const netlifyConfig = { - build: { command: 'npm run build' }, - functions: {}, - redirects: [], - headers: [], -} as NetlifyPluginOptions['netlifyConfig'] +const netlifyConfig = { build: { command: 'npm run build' }, functions: {}, redirects: [], headers: [] } as NetlifyPluginOptions["netlifyConfig"] const defaultArgs = { netlifyConfig, utils, @@ -552,12 +560,8 @@ describe('onBuild()', () => { expect(existsSync(handlerFile)).toBeTruthy() expect(existsSync(odbHandlerFile)).toBeTruthy() - expect(readFileSync(handlerFile, 'utf8')).toMatch( - `(config, "../../..", pageRoot, NextServer, staticManifest, 'ssr')`, - ) - expect(readFileSync(odbHandlerFile, 'utf8')).toMatch( - `(config, "../../..", pageRoot, NextServer, staticManifest, 'odb')`, - ) + expect(readFileSync(handlerFile, 'utf8')).toMatch(`(config, "../../..", pageRoot, NextServer, staticManifest, 'ssr')`) + expect(readFileSync(odbHandlerFile, 'utf8')).toMatch(`(config, "../../..", pageRoot, NextServer, staticManifest, 'odb')`) expect(readFileSync(handlerFile, 'utf8')).toMatch(`require("../../../.next/required-server-files.json")`) expect(readFileSync(odbHandlerFile, 'utf8')).toMatch(`require("../../../.next/required-server-files.json")`) }) @@ -633,29 +637,30 @@ describe('onBuild()', () => { await nextRuntime.onBuild(defaultArgs) const manifestPath = await readJson(path.resolve('.netlify/edge-functions/manifest.json')) const manifest = manifestPath.functions - + expect(manifest).toEqual( expect.arrayContaining([ expect.objectContaining({ - generator: '@netlify/next-runtime@1.0.0', - }), - ]), + generator: '@netlify/next-runtime@1.0.0' + }) + ]) ) }) + test('generates generator field within the edge-functions manifest includes IPX', async () => { process.env.NEXT_FORCE_EDGE_IMAGES = '1' await moveNextDist() await nextRuntime.onBuild(defaultArgs) const manifestPath = await readJson(path.resolve('.netlify/edge-functions/manifest.json')) const manifest = manifestPath.functions - + expect(manifest).toEqual( expect.arrayContaining([ expect.objectContaining({ - generator: '@netlify/next-runtime@1.0.0', - }), - ]), + generator: '@netlify/next-runtime@1.0.0' + }) + ]) ) }) diff --git a/test/templates/server.spec.ts b/test/templates/server.spec.ts index 53ca0d2fd9..0f9b5052ff 100644 --- a/test/templates/server.spec.ts +++ b/test/templates/server.spec.ts @@ -66,7 +66,6 @@ beforeAll(() => { this.nextConfig = nextOptions.conf this.netlifyConfig = netlifyConfig } - Object.setPrototypeOf(NetlifyNextServer, MockNetlifyNextServerConstructor) }) From 33c28ae5ea82cc0d01bafffc545f3fe5b6ce87e9 Mon Sep 17 00:00:00 2001 From: Michal Piechowiak Date: Tue, 25 Apr 2023 10:43:03 +0200 Subject: [PATCH 11/28] fix: handle function config parsing as well --- packages/runtime/src/helpers/analysis.ts | 18 ++++---- packages/runtime/src/helpers/files.ts | 54 +++++------------------ packages/runtime/src/helpers/functions.ts | 19 ++++---- packages/runtime/src/helpers/types.ts | 7 +++ packages/runtime/src/helpers/utils.ts | 3 +- 5 files changed, 36 insertions(+), 65 deletions(-) diff --git a/packages/runtime/src/helpers/analysis.ts b/packages/runtime/src/helpers/analysis.ts index 2dd1abad18..ccd75d7144 100644 --- a/packages/runtime/src/helpers/analysis.ts +++ b/packages/runtime/src/helpers/analysis.ts @@ -2,12 +2,8 @@ import fs, { existsSync } from 'fs' import { relative } from 'pathe' -// I have no idea what eslint is up to here but it gives an error -// eslint-disable-next-line no-shadow -export const enum ApiRouteType { - SCHEDULED = 'experimental-scheduled', - BACKGROUND = 'experimental-background', -} +import { getNextModulePath } from './files' +import { ApiRouteType } from './types' export interface ApiStandardConfig { type?: never @@ -87,18 +83,20 @@ let hasWarnedAboutNextVersion = false /** * Uses Next's swc static analysis to extract the config values from a file. */ -export const extractConfigFromFile = async (apiFilePath: string): Promise => { +export const extractConfigFromFile = async (apiFilePath: string, appDir: string): Promise => { if (!apiFilePath || !existsSync(apiFilePath)) { return {} } + console.log(`updated extract`) try { if (!extractConstValue) { - extractConstValue = require('next/dist/build/analysis/extract-const-value') + // eslint-disable-next-line import/no-dynamic-require + extractConstValue = require(getNextModulePath(appDir, ['next/dist/build/analysis/extract-const-value'])) } if (!parseModule) { - // eslint-disable-next-line prefer-destructuring, @typescript-eslint/no-var-requires - parseModule = require('next/dist/build/analysis/parse-module').parseModule + // eslint-disable-next-line prefer-destructuring, @typescript-eslint/no-var-requires, import/no-dynamic-require + parseModule = require(getNextModulePath(appDir, ['next/dist/build/analysis/parse-module'])).parseModule } } catch (error) { if (error.code === 'MODULE_NOT_FOUND') { diff --git a/packages/runtime/src/helpers/files.ts b/packages/runtime/src/helpers/files.ts index 4022e7802d..c3612b5223 100644 --- a/packages/runtime/src/helpers/files.ts +++ b/packages/runtime/src/helpers/files.ts @@ -330,61 +330,27 @@ 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) { candidates.unshift('next/dist/server/base-server') } - return findModuleFromBase({ candidates, paths: [root] }) + return getNextModulePath(root, candidates) } -/** - * Try to find next-server module in few locations (to support different next versions) and in few context (try to resolve from app location and from this module) - */ -export const getNextServerModulePath = (root: string): string | null => { - // first let's try to use app location directory to find next-server - try { - const nextServerModuleLocation = getServerFile(root, false) - if (nextServerModuleLocation) { - return nextServerModuleLocation - } - } catch (error) { - if (!error.message.includes('Cannot find module')) { - // A different error, so rethrow it - throw error - } - } - - // if we didn't find it, let's try to resolve "next" package from this module - try { - // next >= 11.0.1. Yay breaking changes in patch releases! - const nextServerModuleLocation = require.resolve('next/dist/server/next-server') - if (nextServerModuleLocation) { - return nextServerModuleLocation - } - } 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. +export const getNextModulePath = (root: string, candidates: Array): string | null => { + const module = findModuleFromBase({ candidates, paths: [root] }) + if (module) { + return module } - try { - // next < 11.0.1 - // eslint-disable-next-line n/no-missing-require - const nextServerModuleLocation = require.resolve('next/dist/next-server/server/next-server') - if (nextServerModuleLocation) { - return nextServerModuleLocation - } - } catch (error) { - if (!error.message.includes("Cannot find module 'next/dist/next-server/server/next-server'")) { - throw error - } + for (const candidate of candidates) { + try { + return require.resolve(candidate) + } catch {} } - return null } diff --git a/packages/runtime/src/helpers/functions.ts b/packages/runtime/src/helpers/functions.ts index 62a0a3c95d..bb11cfed70 100644 --- a/packages/runtime/src/helpers/functions.ts +++ b/packages/runtime/src/helpers/functions.ts @@ -20,9 +20,10 @@ import { getApiHandler } from '../templates/getApiHandler' import { getHandler } from '../templates/getHandler' import { getResolverForPages, getResolverForSourceFiles } from '../templates/getPageResolver' -import { ApiConfig, ApiRouteType, extractConfigFromFile, isEdgeConfig } from './analysis' -import { getNextServerModulePath, getSourceFileForPage } from './files' +import { ApiConfig, extractConfigFromFile, isEdgeConfig } from './analysis' +import { getServerFile, getSourceFileForPage } from './files' import { writeFunctionConfiguration } from './functionsMetaData' +import { ApiRouteType } from './types' import { getFunctionNameForPage } from './utils' export interface ApiRouteConfig { @@ -41,7 +42,7 @@ export const generateFunctions = async ( const functionDir = join(functionsDir, HANDLER_FUNCTION_NAME) const publishDir = relative(functionDir, publish) - const nextServerModuleAbsoluteLocation = getNextServerModulePath(appDir) + const nextServerModuleAbsoluteLocation = getServerFile(appDir, false) const nextServerModuleRelativeLocation = nextServerModuleAbsoluteLocation ? relative(functionDir, nextServerModuleAbsoluteLocation) : undefined @@ -210,18 +211,18 @@ export const setupImageFunction = async ({ /** * Look for API routes, and extract the config from the source file. */ -export const getApiRouteConfigs = async (publish: string, baseDir: string): Promise> => { +export const getApiRouteConfigs = async (publish: string, appDir: string): Promise> => { const pages = await readJSON(join(publish, 'server', 'pages-manifest.json')) const apiRoutes = Object.keys(pages).filter((page) => page.startsWith('/api/')) // two possible places // Ref: https://nextjs.org/docs/advanced-features/src-directory - const pagesDir = join(baseDir, 'pages') - const srcPagesDir = join(baseDir, 'src', 'pages') + const pagesDir = join(appDir, 'pages') + const srcPagesDir = join(appDir, 'src', 'pages') return await Promise.all( apiRoutes.map(async (apiRoute) => { const filePath = getSourceFileForPage(apiRoute, [pagesDir, srcPagesDir]) - return { route: apiRoute, config: await extractConfigFromFile(filePath), compiled: pages[apiRoute] } + return { route: apiRoute, config: await extractConfigFromFile(filePath, appDir), compiled: pages[apiRoute] } }), ) } @@ -229,8 +230,8 @@ export const getApiRouteConfigs = async (publish: string, baseDir: string): Prom /** * Looks for extended API routes (background and scheduled functions) and extract the config from the source file. */ -export const getExtendedApiRouteConfigs = async (publish: string, baseDir: string): Promise> => { - const settledApiRoutes = await getApiRouteConfigs(publish, baseDir) +export const getExtendedApiRouteConfigs = async (publish: string, appDir: string): Promise> => { + const settledApiRoutes = await getApiRouteConfigs(publish, appDir) // We only want to return the API routes that are background or scheduled functions return settledApiRoutes.filter((apiRoute) => apiRoute.config.type !== undefined) diff --git a/packages/runtime/src/helpers/types.ts b/packages/runtime/src/helpers/types.ts index 1819f677b0..833d1c2426 100644 --- a/packages/runtime/src/helpers/types.ts +++ b/packages/runtime/src/helpers/types.ts @@ -51,3 +51,10 @@ export interface RoutesManifest { i18n: I18n rewrites: Rewrites } + +// I have no idea what eslint is up to here but it gives an error +// eslint-disable-next-line no-shadow +export const enum ApiRouteType { + SCHEDULED = 'experimental-scheduled', + BACKGROUND = 'experimental-background', +} diff --git a/packages/runtime/src/helpers/utils.ts b/packages/runtime/src/helpers/utils.ts index 2022fc6b00..4a807164af 100644 --- a/packages/runtime/src/helpers/utils.ts +++ b/packages/runtime/src/helpers/utils.ts @@ -7,9 +7,8 @@ import { join } from 'pathe' import { OPTIONAL_CATCH_ALL_REGEX, CATCH_ALL_REGEX, DYNAMIC_PARAMETER_REGEX, HANDLER_FUNCTION_PATH } from '../constants' -import { ApiRouteType } from './analysis' import type { ApiRouteConfig } from './functions' -import { I18n } from './types' +import { I18n, ApiRouteType } from './types' const RESERVED_FILENAME = /[^\w_-]/g From be8bc6eddf30a25d985399c3abb85029d70f6838 Mon Sep 17 00:00:00 2001 From: Michal Piechowiak Date: Tue, 25 Apr 2023 11:05:08 +0200 Subject: [PATCH 12/28] fix: adjust import --- packages/runtime/src/templates/getApiHandler.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/runtime/src/templates/getApiHandler.ts b/packages/runtime/src/templates/getApiHandler.ts index b11ed866c4..1fd48027e9 100644 --- a/packages/runtime/src/templates/getApiHandler.ts +++ b/packages/runtime/src/templates/getApiHandler.ts @@ -3,8 +3,9 @@ import type { Bridge as NodeBridge } from '@vercel/node-bridge/bridge' // Aliasing like this means the editor may be able to syntax-highlight the string import { outdent as javascript } from 'outdent' -import { ApiConfig, ApiRouteType } from '../helpers/analysis' +import { ApiConfig } from '../helpers/analysis' import type { NextConfig } from '../helpers/config' +import { ApiRouteType } from '../helpers/types' import type { NextServerType } from './handlerUtils' From caef7674c80a1aa77a88764d89ce89d14ac770fc Mon Sep 17 00:00:00 2001 From: Michal Piechowiak Date: Tue, 25 Apr 2023 11:15:56 +0200 Subject: [PATCH 13/28] fix: server.spec.ts (again vol 2) --- test/templates/server.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/templates/server.spec.ts b/test/templates/server.spec.ts index 0f9b5052ff..1524925d96 100644 --- a/test/templates/server.spec.ts +++ b/test/templates/server.spec.ts @@ -2,7 +2,7 @@ import { mockRequest } from 'next/dist/server/lib/mock-request' import { Options } from 'next/dist/server/next-server' import { NextServerType, netlifyApiFetch } from '../../packages/runtime/src/templates/handlerUtils' -import { getNextServerModulePath } from '../../packages/runtime/src/helpers/files' +import { getServerFile } from '../../packages/runtime/src/helpers/files' import { getNetlifyNextServer, NetlifyNextServerType, NetlifyConfig } from '../../packages/runtime/src/templates/server' jest.mock('../../packages/runtime/src/templates/handlerUtils', () => { @@ -56,7 +56,7 @@ jest.mock( let NetlifyNextServer: NetlifyNextServerType beforeAll(() => { - const NextServer: NextServerType = require(getNextServerModulePath(__dirname)).default + const NextServer: NextServerType = require(getServerFile(__dirname, false)).default jest.spyOn(NextServer.prototype, 'getRequestHandler').mockImplementation(() => () => Promise.resolve()) NetlifyNextServer = getNetlifyNextServer(NextServer) From 56aa4f77ec1f6ce19c42519bf6c58b332b6d850d Mon Sep 17 00:00:00 2001 From: Michal Piechowiak Date: Wed, 26 Apr 2023 13:58:41 +0200 Subject: [PATCH 14/28] test: add integration test --- .github/workflows/test-integration.yml | 27 ++ package-lock.json | 286 +++++++++++++++--- package.json | 7 +- .../index.js | 12 + .../manifest.yaml | 1 + .../package.json | 3 + .../netlify.toml | 9 + .../next-app/package.json | 16 + .../next-app/pages/api/hello.js | 3 + .../next-app/pages/index.js | 16 + test/integration/jest.config.js | 8 + ...package-not-resolvable-in-base-dir.spec.ts | 92 ++++++ 12 files changed, 439 insertions(+), 41 deletions(-) create mode 100644 .github/workflows/test-integration.yml create mode 100644 test/integration/fixtures/next-package-not-resolvable-in-base-dir/clear-node-modules-after-functions-bundling-plugin/index.js create mode 100644 test/integration/fixtures/next-package-not-resolvable-in-base-dir/clear-node-modules-after-functions-bundling-plugin/manifest.yaml create mode 100644 test/integration/fixtures/next-package-not-resolvable-in-base-dir/clear-node-modules-after-functions-bundling-plugin/package.json create mode 100644 test/integration/fixtures/next-package-not-resolvable-in-base-dir/netlify.toml create mode 100644 test/integration/fixtures/next-package-not-resolvable-in-base-dir/next-app/package.json create mode 100644 test/integration/fixtures/next-package-not-resolvable-in-base-dir/next-app/pages/api/hello.js create mode 100644 test/integration/fixtures/next-package-not-resolvable-in-base-dir/next-app/pages/index.js create mode 100644 test/integration/jest.config.js create mode 100644 test/integration/next-package-not-resolvable-in-base-dir.spec.ts diff --git a/.github/workflows/test-integration.yml b/.github/workflows/test-integration.yml new file mode 100644 index 0000000000..1803f3115d --- /dev/null +++ b/.github/workflows/test-integration.yml @@ -0,0 +1,27 @@ +name: Next Runtime Integration Tests + +on: + pull_request: + push: + branches: [main] + +concurrency: + group: ${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + build: + name: Integration tests + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Installing with LTS Node.js + uses: actions/setup-node@v2 + with: + node-version: 16 + check-latest: true + - name: NPM Install + run: npm install + - name: Run integration tests + run: npm run test:integration diff --git a/package-lock.json b/package-lock.json index f0254a4b73..aecbb7c2fe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,8 +21,7 @@ "demos/next-with-edge-functions" ], "dependencies": { - "next": "^13.1.6", - "regexp-tree": "^0.1.24" + "next": "^13.1.6" }, "devDependencies": { "@babel/core": "^7.15.8", @@ -47,6 +46,7 @@ "eslint-plugin-promise": "^6.0.0", "eslint-plugin-unicorn": "^43.0.2", "execa": "^5.1.1", + "fs-extra": "^11.1.1", "husky": "^7.0.4", "jest": "^27.0.0", "jest-extended": "^3.2.0", @@ -54,6 +54,7 @@ "jest-junit": "^14.0.1", "mock-fs": "^5.2.0", "netlify-plugin-cypress": "^2.2.1", + "node-fetch": "^2.6.6", "npm-run-all": "^4.1.5", "playwright-chromium": "^1.26.1", "prettier": "^2.1.2", @@ -63,7 +64,8 @@ "sass": "^1.49.0", "sharp": "^0.30.4", "tmp-promise": "^3.0.2", - "typescript": "^4.3.4" + "typescript": "^4.3.4", + "wait-on": "^7.0.1" }, "engines": { "node": ">=16.0.0" @@ -2364,6 +2366,20 @@ "node": ">=v14" } }, + "node_modules/@commitlint/read/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@commitlint/resolve-extends": { "version": "17.1.0", "resolved": "https://registry.npmjs.org/@commitlint/resolve-extends/-/resolve-extends-17.1.0.tgz", @@ -2691,6 +2707,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@hapi/hoek": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", + "dev": true + }, + "node_modules/@hapi/topo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", + "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", + "dev": true, + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.7", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.7.tgz", @@ -4855,19 +4886,6 @@ "unstorage": "^1.0.0" } }, - "node_modules/@netlify/ipx/node_modules/fs-extra": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.1.tgz", - "integrity": "sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, "node_modules/@netlify/ipx/node_modules/ufo": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.1.1.tgz", @@ -5708,6 +5726,27 @@ "node": ">= 8.0.0" } }, + "node_modules/@sideway/address": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.4.tgz", + "integrity": "sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw==", + "dev": true, + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@sideway/formula": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==", + "dev": true + }, + "node_modules/@sideway/pinpoint": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", + "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", + "dev": true + }, "node_modules/@sinclair/typebox": { "version": "0.25.24", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz", @@ -7164,6 +7203,30 @@ "node": ">=4" } }, + "node_modules/axios": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", + "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", + "dev": true, + "dependencies": { + "follow-redirects": "^1.14.9", + "form-data": "^4.0.0" + } + }, + "node_modules/axios/node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/axobject-query": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz", @@ -13128,16 +13191,16 @@ "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" }, "node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.1.tgz", + "integrity": "sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==", "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" }, "engines": { - "node": ">=12" + "node": ">=14.14" } }, "node_modules/fs-memo": { @@ -16177,6 +16240,19 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/joi": { + "version": "17.9.2", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.9.2.tgz", + "integrity": "sha512-Itk/r+V4Dx0V3c7RLFdRh12IOjySm2/WGPMubBT92cQvRfYZhPM2W0hZlctjj72iES8jsRCwp7S/cRmWBnJ4nw==", + "dev": true, + "dependencies": { + "@hapi/hoek": "^9.0.0", + "@hapi/topo": "^5.0.0", + "@sideway/address": "^4.1.3", + "@sideway/formula": "^3.0.1", + "@sideway/pinpoint": "^2.0.0" + } + }, "node_modules/jose": { "version": "4.13.1", "resolved": "https://registry.npmjs.org/jose/-/jose-4.13.1.tgz", @@ -21350,9 +21426,9 @@ } }, "node_modules/rxjs": { - "version": "7.5.7", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.7.tgz", - "integrity": "sha512-z9MzKh/UcOqB3i20H6rtrlaE/CgjLOvheWK/9ILrbhROGTweAi1BaFsTT9FbwZi5Trr1qNRs+MXkhmR06awzQA==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.0.tgz", + "integrity": "sha512-F2+gxDshqmIub1KdvZkaEfGDwLNpPvk9Fs6LD/MyQxNgMds/WH9OdDDXOmxUZpME+iSK3rQCctkL0DYyytUqMg==", "dev": true, "dependencies": { "tslib": "^2.1.0" @@ -23819,6 +23895,25 @@ "node": ">=10" } }, + "node_modules/wait-on": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-7.0.1.tgz", + "integrity": "sha512-9AnJE9qTjRQOlTZIldAaf/da2eW0eSRSgcqq85mXQja/DW3MriHxkpODDSUEg+Gri/rKEcXUZHe+cevvYItaog==", + "dev": true, + "dependencies": { + "axios": "^0.27.2", + "joi": "^17.7.0", + "lodash": "^4.17.21", + "minimist": "^1.2.7", + "rxjs": "^7.8.0" + }, + "bin": { + "wait-on": "bin/wait-on" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/walk-back": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/walk-back/-/walk-back-4.0.0.tgz", @@ -24339,7 +24434,7 @@ }, "packages/runtime": { "name": "@netlify/plugin-nextjs", - "version": "4.34.0", + "version": "4.35.0", "license": "MIT", "dependencies": { "@netlify/esbuild": "0.14.39", @@ -24381,6 +24476,19 @@ "node": ">=12.0.0" } }, + "packages/runtime/node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, "packages/runtime/node_modules/semver": { "version": "7.3.8", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", @@ -25908,6 +26016,19 @@ "fs-extra": "^10.0.0", "git-raw-commits": "^2.0.0", "minimist": "^1.2.6" + }, + "dependencies": { + "fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + } } }, "@commitlint/resolve-extends": { @@ -26171,6 +26292,21 @@ } } }, + "@hapi/hoek": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", + "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", + "dev": true + }, + "@hapi/topo": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", + "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", + "dev": true, + "requires": { + "@hapi/hoek": "^9.0.0" + } + }, "@humanwhocodes/config-array": { "version": "0.11.7", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.7.tgz", @@ -27641,16 +27777,6 @@ "unstorage": "^1.0.0" }, "dependencies": { - "fs-extra": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.1.tgz", - "integrity": "sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==", - "requires": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - } - }, "ufo": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.1.1.tgz", @@ -27711,6 +27837,16 @@ "typescript": "^4.6.3" }, "dependencies": { + "fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, "semver": { "version": "7.3.8", "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", @@ -28234,6 +28370,27 @@ "string.prototype.codepointat": "^0.2.1" } }, + "@sideway/address": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.4.tgz", + "integrity": "sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw==", + "dev": true, + "requires": { + "@hapi/hoek": "^9.0.0" + } + }, + "@sideway/formula": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==", + "dev": true + }, + "@sideway/pinpoint": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", + "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", + "dev": true + }, "@sinclair/typebox": { "version": "0.25.24", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz", @@ -29367,6 +29524,29 @@ "integrity": "sha512-u2MVsXfew5HBvjsczCv+xlwdNnB1oQR9HlAcsejZttNjKKSkeDNVwB1vMThIUIFI9GoT57Vtk8iQLwqOfAkboA==", "dev": true }, + "axios": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", + "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", + "dev": true, + "requires": { + "follow-redirects": "^1.14.9", + "form-data": "^4.0.0" + }, + "dependencies": { + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + } + } + }, "axobject-query": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz", @@ -33918,9 +34098,9 @@ "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" }, "fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.1.tgz", + "integrity": "sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==", "requires": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -36184,6 +36364,19 @@ } } }, + "joi": { + "version": "17.9.2", + "resolved": "https://registry.npmjs.org/joi/-/joi-17.9.2.tgz", + "integrity": "sha512-Itk/r+V4Dx0V3c7RLFdRh12IOjySm2/WGPMubBT92cQvRfYZhPM2W0hZlctjj72iES8jsRCwp7S/cRmWBnJ4nw==", + "dev": true, + "requires": { + "@hapi/hoek": "^9.0.0", + "@hapi/topo": "^5.0.0", + "@sideway/address": "^4.1.3", + "@sideway/formula": "^3.0.1", + "@sideway/pinpoint": "^2.0.0" + } + }, "jose": { "version": "4.13.1", "resolved": "https://registry.npmjs.org/jose/-/jose-4.13.1.tgz", @@ -40126,9 +40319,9 @@ } }, "rxjs": { - "version": "7.5.7", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.7.tgz", - "integrity": "sha512-z9MzKh/UcOqB3i20H6rtrlaE/CgjLOvheWK/9ILrbhROGTweAi1BaFsTT9FbwZi5Trr1qNRs+MXkhmR06awzQA==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.0.tgz", + "integrity": "sha512-F2+gxDshqmIub1KdvZkaEfGDwLNpPvk9Fs6LD/MyQxNgMds/WH9OdDDXOmxUZpME+iSK3rQCctkL0DYyytUqMg==", "dev": true, "requires": { "tslib": "^2.1.0" @@ -42055,6 +42248,19 @@ "xml-name-validator": "^3.0.0" } }, + "wait-on": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-7.0.1.tgz", + "integrity": "sha512-9AnJE9qTjRQOlTZIldAaf/da2eW0eSRSgcqq85mXQja/DW3MriHxkpODDSUEg+Gri/rKEcXUZHe+cevvYItaog==", + "dev": true, + "requires": { + "axios": "^0.27.2", + "joi": "^17.7.0", + "lodash": "^4.17.21", + "minimist": "^1.2.7", + "rxjs": "^7.8.0" + } + }, "walk-back": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/walk-back/-/walk-back-4.0.0.tgz", diff --git a/package.json b/package.json index 8a8c964c2b..3a56ef8170 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "test:next:all": "RUN_SKIPPED_TESTS=1 jest -c test/e2e/jest.config.all.js", "test:next:appdir": "jest -c test/e2e/jest.config.appdir.js", "test:jest": "jest", + "test:integration": "jest -c test/integration/jest.config.js", "playwright:install": "playwright install --with-deps chromium", "test:jest:update": "jest --updateSnapshot", "test:update": "run-s build build:demo test:jest:update" @@ -70,6 +71,7 @@ "eslint-plugin-promise": "^6.0.0", "eslint-plugin-unicorn": "^43.0.2", "execa": "^5.1.1", + "fs-extra": "^11.1.1", "husky": "^7.0.4", "jest": "^27.0.0", "jest-extended": "^3.2.0", @@ -77,6 +79,7 @@ "jest-junit": "^14.0.1", "mock-fs": "^5.2.0", "netlify-plugin-cypress": "^2.2.1", + "node-fetch": "^2.6.6", "npm-run-all": "^4.1.5", "playwright-chromium": "^1.26.1", "prettier": "^2.1.2", @@ -86,7 +89,8 @@ "sass": "^1.49.0", "sharp": "^0.30.4", "tmp-promise": "^3.0.2", - "typescript": "^4.3.4" + "typescript": "^4.3.4", + "wait-on": "^7.0.1" }, "dependencies": { "next": "^13.1.6" @@ -102,6 +106,7 @@ "**/test/**/*.spec.js", "**/test/**/*.spec.ts", "!**/test/e2e/**", + "!**/test/integration/**", "!**/test/fixtures/**", "!**/test/sample/**", "!**/test/templates/edge-shared/**" diff --git a/test/integration/fixtures/next-package-not-resolvable-in-base-dir/clear-node-modules-after-functions-bundling-plugin/index.js b/test/integration/fixtures/next-package-not-resolvable-in-base-dir/clear-node-modules-after-functions-bundling-plugin/index.js new file mode 100644 index 0000000000..3ac501d059 --- /dev/null +++ b/test/integration/fixtures/next-package-not-resolvable-in-base-dir/clear-node-modules-after-functions-bundling-plugin/index.js @@ -0,0 +1,12 @@ +const fs = require(`fs`) +const path = require(`path`) + +module.exports = { + onPostBuild: async () => { + const movedDir = path.join(process.cwd(), `next-app`, `node_modules2`) + try { + fs.unlinkSync(movedDir) + } catch {} + fs.renameSync(path.join(process.cwd(), `next-app`, `node_modules`), movedDir) + }, +} diff --git a/test/integration/fixtures/next-package-not-resolvable-in-base-dir/clear-node-modules-after-functions-bundling-plugin/manifest.yaml b/test/integration/fixtures/next-package-not-resolvable-in-base-dir/clear-node-modules-after-functions-bundling-plugin/manifest.yaml new file mode 100644 index 0000000000..1727ced689 --- /dev/null +++ b/test/integration/fixtures/next-package-not-resolvable-in-base-dir/clear-node-modules-after-functions-bundling-plugin/manifest.yaml @@ -0,0 +1 @@ +name: clear-node-modules-after-functions-bundling-plugin diff --git a/test/integration/fixtures/next-package-not-resolvable-in-base-dir/clear-node-modules-after-functions-bundling-plugin/package.json b/test/integration/fixtures/next-package-not-resolvable-in-base-dir/clear-node-modules-after-functions-bundling-plugin/package.json new file mode 100644 index 0000000000..593181fd63 --- /dev/null +++ b/test/integration/fixtures/next-package-not-resolvable-in-base-dir/clear-node-modules-after-functions-bundling-plugin/package.json @@ -0,0 +1,3 @@ +{ + "name": "clear-node-modules-after-functions-bundling-plugin" +} diff --git a/test/integration/fixtures/next-package-not-resolvable-in-base-dir/netlify.toml b/test/integration/fixtures/next-package-not-resolvable-in-base-dir/netlify.toml new file mode 100644 index 0000000000..29f97224b8 --- /dev/null +++ b/test/integration/fixtures/next-package-not-resolvable-in-base-dir/netlify.toml @@ -0,0 +1,9 @@ +[build] +command = "cd next-app; npm install; npm run build" +publish = "next-app/.next" + +[[plugins]] +package = "@netlify/plugin-nextjs" + +[[plugins]] +package = "/clear-node-modules-after-functions-bundling-plugin" diff --git a/test/integration/fixtures/next-package-not-resolvable-in-base-dir/next-app/package.json b/test/integration/fixtures/next-package-not-resolvable-in-base-dir/next-app/package.json new file mode 100644 index 0000000000..a4ca893b9e --- /dev/null +++ b/test/integration/fixtures/next-package-not-resolvable-in-base-dir/next-app/package.json @@ -0,0 +1,16 @@ +{ + "name": "my-app", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "next": "^13.3.0", + "react": "18.2.0", + "react-dom": "18.2.0" + } +} diff --git a/test/integration/fixtures/next-package-not-resolvable-in-base-dir/next-app/pages/api/hello.js b/test/integration/fixtures/next-package-not-resolvable-in-base-dir/next-app/pages/api/hello.js new file mode 100644 index 0000000000..bf4cbe226e --- /dev/null +++ b/test/integration/fixtures/next-package-not-resolvable-in-base-dir/next-app/pages/api/hello.js @@ -0,0 +1,3 @@ +module.exports = function handler(req, res) { + res.status(200).json({ name: 'John Doe' }) +} diff --git a/test/integration/fixtures/next-package-not-resolvable-in-base-dir/next-app/pages/index.js b/test/integration/fixtures/next-package-not-resolvable-in-base-dir/next-app/pages/index.js new file mode 100644 index 0000000000..644044fafd --- /dev/null +++ b/test/integration/fixtures/next-package-not-resolvable-in-base-dir/next-app/pages/index.js @@ -0,0 +1,16 @@ +import Head from 'next/head' + +export default function Home() { + return ( + <> + + Create Next App + + + +
+
Hello world
+
+ + ) +} diff --git a/test/integration/jest.config.js b/test/integration/jest.config.js new file mode 100644 index 0000000000..e5d5909f84 --- /dev/null +++ b/test/integration/jest.config.js @@ -0,0 +1,8 @@ +module.exports = { + transform: { + '\\.[jt]sx?$': 'babel-jest', + }, + verbose: true, + testTimeout: 60000, + maxWorkers: 1, +} diff --git a/test/integration/next-package-not-resolvable-in-base-dir.spec.ts b/test/integration/next-package-not-resolvable-in-base-dir.spec.ts new file mode 100644 index 0000000000..cfc8a4e5bd --- /dev/null +++ b/test/integration/next-package-not-resolvable-in-base-dir.spec.ts @@ -0,0 +1,92 @@ +import execa from 'execa' +import * as fs from 'fs-extra' +import * as path from 'path' +import * as os from 'os' +import waitOn from 'wait-on' +import fetch from 'node-fetch' + +let destroy = () => {} + +beforeAll(async () => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), `isolated-test-`)) + + // copy fixture to isolated tmp directory + const siteSrcDir = path.join(__dirname, 'fixtures', 'next-package-not-resolvable-in-base-dir') + const siteDestDir = path.join(tmpDir, 'next-package-not-resolvable-in-base-dir') + + console.log(`copying fixture site "${siteSrcDir}" to "${siteDestDir}"`) + fs.copySync(siteSrcDir, siteDestDir) + + // bump version so no npm cache tries to use what's in npm registry + const runtimeSrcDir = path.join(__dirname, '..', '..', 'packages', 'runtime') + await execa(`npm`, [`version`, `prerelease`, `--no-git-tag-version`], { cwd: runtimeSrcDir, stdio: `inherit` }) + + // create package tarball + const o = await execa(`npm`, [`pack`, `--json`], { cwd: runtimeSrcDir }) + const tgzName = JSON.parse(o.stdout)[0].filename + const tgzPath = path.join(runtimeSrcDir, tgzName) + + // install runtime from tarball + await execa(`npm`, [`install`, tgzPath], { cwd: siteDestDir, stdio: `inherit` }) + + return new Promise(async (resolve, reject) => { + try { + // run + let isServeRunning = true + const serveProcess = execa('netlify', ['serve', `-p`, `8888`], { cwd: siteDestDir, stdio: `inherit` }) + + let shouldRejectOnNonZeroProcessExit = true + serveProcess.catch((e) => { + isServeRunning = false + if (shouldRejectOnNonZeroProcessExit) { + reject(e) + } + return null + }) + + await waitOn({ + resources: [`http://localhost:8888/`], + timeout: 3 * 60 * 1000, + }) + + if (!isServeRunning) { + return reject(new Error(`serve process exited`)) + } + + destroy = () => { + shouldRejectOnNonZeroProcessExit = false + serveProcess.kill() + } + + // ensure we can't resolve "next" from either base dir or app dir + // this is done to ensure that functions packaging worked correctly and doesn't rely on + // leftover node_modules that wouldn't be available when functions are deployed to lambdas + expect(() => require.resolve(`next`, { paths: [siteDestDir] })).toThrow() + expect(() => require.resolve(`next`, { paths: [path.join(siteDestDir, `next-app`)] })).toThrow() + + return resolve() + } catch (e) { + reject(e) + } + }) +}) + +afterAll(() => destroy()) + +it(`page route executes correctly`, async () => { + const htmlResponse = await fetch(`http://localhost:8888/`) + // ensure we got a 200 + expect(htmlResponse.ok).toBe(true) + // ensure we use ssr handler + expect(htmlResponse.headers.get(`x-nf-render-mode`)).toEqual(`ssr`) + const t = expect(await htmlResponse.text()).toMatch(/Hello world/) +}) + +it(`api route executes correctly`, async () => { + const apiResponse = await fetch(`http://localhost:8888/api/hello`) + // ensure we got a 200 + expect(apiResponse.ok).toBe(true) + // ensure we use ssr handler + expect(apiResponse.headers.get(`x-nf-render-mode`)).toEqual(`ssr`) + expect(await apiResponse.json()).toEqual({ name: 'John Doe' }) +}) From 22e2fa2e7b29c79b0661dafdf69066e6f64d2453 Mon Sep 17 00:00:00 2001 From: Michal Piechowiak Date: Wed, 26 Apr 2023 15:07:56 +0200 Subject: [PATCH 15/28] test: log npm version --- .../integration/next-package-not-resolvable-in-base-dir.spec.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/integration/next-package-not-resolvable-in-base-dir.spec.ts b/test/integration/next-package-not-resolvable-in-base-dir.spec.ts index cfc8a4e5bd..31c2ff9801 100644 --- a/test/integration/next-package-not-resolvable-in-base-dir.spec.ts +++ b/test/integration/next-package-not-resolvable-in-base-dir.spec.ts @@ -17,6 +17,8 @@ beforeAll(async () => { console.log(`copying fixture site "${siteSrcDir}" to "${siteDestDir}"`) fs.copySync(siteSrcDir, siteDestDir) + await execa(`npm`, [`version`], { stdio: `inherit` }) + // bump version so no npm cache tries to use what's in npm registry const runtimeSrcDir = path.join(__dirname, '..', '..', 'packages', 'runtime') await execa(`npm`, [`version`, `prerelease`, `--no-git-tag-version`], { cwd: runtimeSrcDir, stdio: `inherit` }) From 6e229c9fae1ffb0b43282dba25591b01139bd9e4 Mon Sep 17 00:00:00 2001 From: Michal Piechowiak Date: Wed, 26 Apr 2023 15:12:39 +0200 Subject: [PATCH 16/28] test: install newer npm/node --- .github/workflows/test-integration.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test-integration.yml b/.github/workflows/test-integration.yml index 1803f3115d..9e662d6a67 100644 --- a/.github/workflows/test-integration.yml +++ b/.github/workflows/test-integration.yml @@ -19,8 +19,10 @@ jobs: - name: Installing with LTS Node.js uses: actions/setup-node@v2 with: - node-version: 16 + node-version: 18 check-latest: true + - name: Install netlify-cli and npm + run: npm install -g netlify-cli npm - name: NPM Install run: npm install - name: Run integration tests From 74ab70e0e62a6db8472a468067184328820ac600 Mon Sep 17 00:00:00 2001 From: Michal Piechowiak Date: Wed, 26 Apr 2023 15:24:05 +0200 Subject: [PATCH 17/28] test: bump timeout for setup --- .../integration/next-package-not-resolvable-in-base-dir.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/next-package-not-resolvable-in-base-dir.spec.ts b/test/integration/next-package-not-resolvable-in-base-dir.spec.ts index 31c2ff9801..1eb8cbd4cc 100644 --- a/test/integration/next-package-not-resolvable-in-base-dir.spec.ts +++ b/test/integration/next-package-not-resolvable-in-base-dir.spec.ts @@ -71,7 +71,7 @@ beforeAll(async () => { reject(e) } }) -}) +}, 3 * 60 * 1000) afterAll(() => destroy()) From e4fadf4ba0b4dbefd3accbe691198ae305eef5a0 Mon Sep 17 00:00:00 2001 From: Michal Piechowiak Date: Wed, 26 Apr 2023 15:45:40 +0200 Subject: [PATCH 18/28] test: ensure test page is ssr --- .../next-app/package.json | 2 +- .../next-app/pages/index.js | 8 ++++++++ .../next-package-not-resolvable-in-base-dir.spec.ts | 2 -- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/test/integration/fixtures/next-package-not-resolvable-in-base-dir/next-app/package.json b/test/integration/fixtures/next-package-not-resolvable-in-base-dir/next-app/package.json index a4ca893b9e..527ecb61eb 100644 --- a/test/integration/fixtures/next-package-not-resolvable-in-base-dir/next-app/package.json +++ b/test/integration/fixtures/next-package-not-resolvable-in-base-dir/next-app/package.json @@ -9,7 +9,7 @@ "lint": "next lint" }, "dependencies": { - "next": "^13.3.0", + "next": "13.2.4", "react": "18.2.0", "react-dom": "18.2.0" } diff --git a/test/integration/fixtures/next-package-not-resolvable-in-base-dir/next-app/pages/index.js b/test/integration/fixtures/next-package-not-resolvable-in-base-dir/next-app/pages/index.js index 644044fafd..f33154f21e 100644 --- a/test/integration/fixtures/next-package-not-resolvable-in-base-dir/next-app/pages/index.js +++ b/test/integration/fixtures/next-package-not-resolvable-in-base-dir/next-app/pages/index.js @@ -14,3 +14,11 @@ export default function Home() { ) } + +export const getServerSideProps = async ({ params }) => { + return { + props: { + ssr: true, + }, + } +} diff --git a/test/integration/next-package-not-resolvable-in-base-dir.spec.ts b/test/integration/next-package-not-resolvable-in-base-dir.spec.ts index 1eb8cbd4cc..ac48dbc341 100644 --- a/test/integration/next-package-not-resolvable-in-base-dir.spec.ts +++ b/test/integration/next-package-not-resolvable-in-base-dir.spec.ts @@ -17,8 +17,6 @@ beforeAll(async () => { console.log(`copying fixture site "${siteSrcDir}" to "${siteDestDir}"`) fs.copySync(siteSrcDir, siteDestDir) - await execa(`npm`, [`version`], { stdio: `inherit` }) - // bump version so no npm cache tries to use what's in npm registry const runtimeSrcDir = path.join(__dirname, '..', '..', 'packages', 'runtime') await execa(`npm`, [`version`, `prerelease`, `--no-git-tag-version`], { cwd: runtimeSrcDir, stdio: `inherit` }) From 1774f47ee16974cd6143ad41647d8de3af2f878e Mon Sep 17 00:00:00 2001 From: Michal Piechowiak Date: Wed, 26 Apr 2023 16:46:03 +0200 Subject: [PATCH 19/28] refactor: get rid of unneded new helper --- packages/runtime/src/helpers/analysis.ts | 12 +++++++++--- packages/runtime/src/helpers/files.ts | 16 +--------------- packages/runtime/src/helpers/utils.ts | 11 +++++++++++ 3 files changed, 21 insertions(+), 18 deletions(-) diff --git a/packages/runtime/src/helpers/analysis.ts b/packages/runtime/src/helpers/analysis.ts index ccd75d7144..7ee8b549c3 100644 --- a/packages/runtime/src/helpers/analysis.ts +++ b/packages/runtime/src/helpers/analysis.ts @@ -2,8 +2,8 @@ import fs, { existsSync } from 'fs' import { relative } from 'pathe' -import { getNextModulePath } from './files' import { ApiRouteType } from './types' +import { findModuleFromBase } from './utils' export interface ApiStandardConfig { type?: never @@ -92,11 +92,17 @@ export const extractConfigFromFile = async (apiFilePath: string, appDir: string) try { if (!extractConstValue) { // eslint-disable-next-line import/no-dynamic-require - extractConstValue = require(getNextModulePath(appDir, ['next/dist/build/analysis/extract-const-value'])) + extractConstValue = require(findModuleFromBase({ + paths: [appDir], + candidates: ['next/dist/build/analysis/extract-const-value'], + })) } if (!parseModule) { // eslint-disable-next-line prefer-destructuring, @typescript-eslint/no-var-requires, import/no-dynamic-require - parseModule = require(getNextModulePath(appDir, ['next/dist/build/analysis/parse-module'])).parseModule + parseModule = require(findModuleFromBase({ + paths: [appDir], + candidates: ['next/dist/build/analysis/parse-module'], + })).parseModule } } catch (error) { if (error.code === 'MODULE_NOT_FOUND') { diff --git a/packages/runtime/src/helpers/files.ts b/packages/runtime/src/helpers/files.ts index 1fb4630369..534b63d2e5 100644 --- a/packages/runtime/src/helpers/files.ts +++ b/packages/runtime/src/helpers/files.ts @@ -338,21 +338,7 @@ export const getServerFile = (root: string, includeBase = true) => { candidates.unshift('next/dist/server/base-server') } - return getNextModulePath(root, candidates) -} - -export const getNextModulePath = (root: string, candidates: Array): string | null => { - const module = findModuleFromBase({ candidates, paths: [root] }) - if (module) { - return module - } - - for (const candidate of candidates) { - try { - return require.resolve(candidate) - } catch {} - } - return null + return findModuleFromBase({ candidates, paths: [root] }) } /** diff --git a/packages/runtime/src/helpers/utils.ts b/packages/runtime/src/helpers/utils.ts index 4a807164af..4a0ead00f1 100644 --- a/packages/runtime/src/helpers/utils.ts +++ b/packages/runtime/src/helpers/utils.ts @@ -267,6 +267,17 @@ export const findModuleFromBase = ({ paths, candidates }): string | null => { // Ignore the error } } + // if we couldn't find a module from paths, let's try to resolve from here + for (const candidate of candidates) { + try { + const modulePath = require.resolve(candidate) + if (modulePath) { + return modulePath + } + } catch { + // Ignore the error + } + } return null } From bbff3a9c6a4d5dc51049003b9f1fac6cd22282e3 Mon Sep 17 00:00:00 2001 From: Michal Piechowiak Date: Thu, 27 Apr 2023 09:20:24 +0200 Subject: [PATCH 20/28] chore: cleanup some debugging logs --- packages/runtime/src/helpers/analysis.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/runtime/src/helpers/analysis.ts b/packages/runtime/src/helpers/analysis.ts index 7ee8b549c3..03abd17bfd 100644 --- a/packages/runtime/src/helpers/analysis.ts +++ b/packages/runtime/src/helpers/analysis.ts @@ -88,7 +88,6 @@ export const extractConfigFromFile = async (apiFilePath: string, appDir: string) return {} } - console.log(`updated extract`) try { if (!extractConstValue) { // eslint-disable-next-line import/no-dynamic-require From ade1a2ebbc75a52c292d33780da7b92200538491 Mon Sep 17 00:00:00 2001 From: Michal Piechowiak Date: Thu, 27 Apr 2023 09:24:01 +0200 Subject: [PATCH 21/28] fix: add fallback in case findModuleBase will be false --- packages/runtime/src/helpers/analysis.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/runtime/src/helpers/analysis.ts b/packages/runtime/src/helpers/analysis.ts index 03abd17bfd..b1c13eeaa9 100644 --- a/packages/runtime/src/helpers/analysis.ts +++ b/packages/runtime/src/helpers/analysis.ts @@ -94,14 +94,14 @@ export const extractConfigFromFile = async (apiFilePath: string, appDir: string) extractConstValue = require(findModuleFromBase({ paths: [appDir], candidates: ['next/dist/build/analysis/extract-const-value'], - })) + }) ?? 'next/dist/build/analysis/extract-const-value') } if (!parseModule) { // eslint-disable-next-line prefer-destructuring, @typescript-eslint/no-var-requires, import/no-dynamic-require parseModule = require(findModuleFromBase({ paths: [appDir], candidates: ['next/dist/build/analysis/parse-module'], - })).parseModule + }) ?? 'next/dist/build/analysis/parse-module').parseModule } } catch (error) { if (error.code === 'MODULE_NOT_FOUND') { From 610d73f7455cb6c19f639d6736711be26559b6db Mon Sep 17 00:00:00 2001 From: Michal Piechowiak Date: Thu, 27 Apr 2023 10:07:28 +0200 Subject: [PATCH 22/28] refactor: use one-parameter object for makeHandler functions --- .../runtime/src/templates/getApiHandler.ts | 15 ++++++++--- packages/runtime/src/templates/getHandler.ts | 25 ++++++++++--------- test/index.spec.ts | 8 ++++-- 3 files changed, 31 insertions(+), 17 deletions(-) diff --git a/packages/runtime/src/templates/getApiHandler.ts b/packages/runtime/src/templates/getApiHandler.ts index 1fd48027e9..d42bf4a7fe 100644 --- a/packages/runtime/src/templates/getApiHandler.ts +++ b/packages/runtime/src/templates/getApiHandler.ts @@ -25,9 +25,16 @@ type Mutable = { -readonly [K in keyof T]: T[K] } +type MakeApiHandlerParams = { + conf: NextConfig + app: string + pageRoot: string + page: string + NextServer: NextServerType +} + // We return a function and then call `toString()` on it to serialise it as the launcher function -// eslint-disable-next-line max-params -const makeHandler = (conf: NextConfig, app, pageRoot, page, NextServer: NextServerType) => { +const makeApiHandler = ({ conf, app, pageRoot, page, NextServer }: MakeApiHandlerParams) => { // 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) @@ -145,7 +152,9 @@ export const getApiHandler = ({ let staticManifest const path = require("path"); const pageRoot = path.resolve(path.join(__dirname, "${publishDir}", "server")); - const handler = (${makeHandler.toString()})(config, "${appDir}", pageRoot, ${JSON.stringify(page)}, NextServer) + const handler = (${makeApiHandler.toString()})({ conf: config, app: "${appDir}", pageRoot, page:${JSON.stringify( + page, + )}, NextServer}) exports.handler = ${ config.type === ApiRouteType.SCHEDULED ? `schedule(${JSON.stringify(config.schedule)}, handler);` : 'handler' } diff --git a/packages/runtime/src/templates/getHandler.ts b/packages/runtime/src/templates/getHandler.ts index 6cf633918d..c7b7997dea 100644 --- a/packages/runtime/src/templates/getHandler.ts +++ b/packages/runtime/src/templates/getHandler.ts @@ -31,17 +31,18 @@ type Mutable = { -readonly [K in keyof T]: T[K] } +type MakeHandlerParams = { + conf: NextConfig + app: string + pageRoot: string + NextServer: NextServerType + staticManifest: Array<[string, string]> + mode: 'ssr' | 'odb' +} + // We return a function and then call `toString()` on it to serialise it as the launcher function // 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 -) => { +const makeHandler = ({ conf, app, pageRoot, NextServer, staticManifest = [], mode = 'ssr' }: MakeHandlerParams) => { // 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) @@ -117,7 +118,7 @@ const makeHandler = ( } return async function handler(event: HandlerEvent, context: HandlerContext) { - let requestMode = mode + let requestMode: string = mode const prefetchResponse = getPrefetchResponse(event, mode) if (prefetchResponse) { return prefetchResponse @@ -220,7 +221,7 @@ export const getHandler = ({ const pageRoot = path.resolve(path.join(__dirname, "${publishDir}", "server")); exports.handler = ${ isODB - ? `builder((${makeHandler.toString()})(config, "${appDir}", pageRoot, NextServer, staticManifest, 'odb'));` - : `(${makeHandler.toString()})(config, "${appDir}", pageRoot, NextServer, staticManifest, 'ssr');` + ? `builder((${makeHandler.toString()})({ conf: config, app: "${appDir}", pageRoot, NextServer, staticManifest, mode: 'odb' }));` + : `(${makeHandler.toString()})({ conf: config, app: "${appDir}", pageRoot, NextServer, staticManifest, mode: 'ssr' });` } ` diff --git a/test/index.spec.ts b/test/index.spec.ts index 7f24001871..c1320c9e84 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -560,8 +560,12 @@ describe('onBuild()', () => { expect(existsSync(handlerFile)).toBeTruthy() expect(existsSync(odbHandlerFile)).toBeTruthy() - expect(readFileSync(handlerFile, 'utf8')).toMatch(`(config, "../../..", pageRoot, NextServer, staticManifest, 'ssr')`) - expect(readFileSync(odbHandlerFile, 'utf8')).toMatch(`(config, "../../..", pageRoot, NextServer, staticManifest, 'odb')`) + expect(readFileSync(handlerFile, 'utf8')).toMatch( + `({ conf: config, app: \"../../..\", pageRoot, NextServer, staticManifest, mode: 'ssr' })`, + ) + expect(readFileSync(odbHandlerFile, 'utf8')).toMatch( + `({ conf: config, app: \"../../..\", pageRoot, NextServer, staticManifest, mode: 'odb' })`, + ) expect(readFileSync(handlerFile, 'utf8')).toMatch(`require("../../../.next/required-server-files.json")`) expect(readFileSync(odbHandlerFile, 'utf8')).toMatch(`require("../../../.next/required-server-files.json")`) }) From 04f3164c275d50ac12f5958936b9ffe2f4c46d27 Mon Sep 17 00:00:00 2001 From: Michal Piechowiak Date: Fri, 28 Apr 2023 17:50:55 +0200 Subject: [PATCH 23/28] refactor: don't rely on MODULE_NOT_FOUND for lack of advanced API routes warning --- packages/runtime/src/helpers/analysis.ts | 49 ++++++++++++------------ test/helpers/analysis.spec.ts | 40 +++++++++---------- 2 files changed, 45 insertions(+), 44 deletions(-) diff --git a/packages/runtime/src/helpers/analysis.ts b/packages/runtime/src/helpers/analysis.ts index b1c13eeaa9..71cc417caa 100644 --- a/packages/runtime/src/helpers/analysis.ts +++ b/packages/runtime/src/helpers/analysis.ts @@ -88,31 +88,32 @@ export const extractConfigFromFile = async (apiFilePath: string, appDir: string) return {} } - try { - if (!extractConstValue) { - // eslint-disable-next-line import/no-dynamic-require - extractConstValue = require(findModuleFromBase({ - paths: [appDir], - candidates: ['next/dist/build/analysis/extract-const-value'], - }) ?? 'next/dist/build/analysis/extract-const-value') - } - if (!parseModule) { - // eslint-disable-next-line prefer-destructuring, @typescript-eslint/no-var-requires, import/no-dynamic-require - parseModule = require(findModuleFromBase({ - paths: [appDir], - candidates: ['next/dist/build/analysis/parse-module'], - }) ?? 'next/dist/build/analysis/parse-module').parseModule - } - } catch (error) { - if (error.code === 'MODULE_NOT_FOUND') { - if (!hasWarnedAboutNextVersion) { - console.log("This version of Next.js doesn't support advanced API routes. Skipping...") - hasWarnedAboutNextVersion = true - } - // Old Next.js version - return {} + const extractConstValueModulePath = findModuleFromBase({ + paths: [appDir], + candidates: ['next/dist/build/analysis/extract-const-value'], + }) + + const parseModulePath = findModuleFromBase({ + paths: [appDir], + candidates: ['next/dist/build/analysis/parse-module'], + }) + + if (!extractConstValueModulePath || !parseModulePath) { + if (!hasWarnedAboutNextVersion) { + console.log("This version of Next.js doesn't support advanced API routes. Skipping...") + hasWarnedAboutNextVersion = true } - throw error + // Old Next.js version + return {} + } + + if (!extractConstValue && extractConstValueModulePath) { + // eslint-disable-next-line import/no-dynamic-require + extractConstValue = require(extractConstValueModulePath) + } + if (!parseModule && parseModulePath) { + // eslint-disable-next-line prefer-destructuring, @typescript-eslint/no-var-requires, import/no-dynamic-require + parseModule = require(parseModulePath).parseModule } const { extractExportedConstValue, UnsupportedValueError } = extractConstValue diff --git a/test/helpers/analysis.spec.ts b/test/helpers/analysis.spec.ts index a066f56508..a9c4881b6a 100644 --- a/test/helpers/analysis.spec.ts +++ b/test/helpers/analysis.spec.ts @@ -11,70 +11,70 @@ describe('static source analysis', () => { ;(console.error as jest.Mock).mockRestore() }) it('should extract config values from a source file', async () => { - const config = await extractConfigFromFile(resolve(__dirname, '../fixtures/analysis/background.js')) + const config = await extractConfigFromFile(resolve(__dirname, '../fixtures/analysis/background.js'), process.cwd()) expect(config).toEqual({ type: 'experimental-background', }) }) it('should extract config values from a TypeScript source file', async () => { - const config = await extractConfigFromFile(resolve(__dirname, '../fixtures/analysis/background.ts')) + const config = await extractConfigFromFile(resolve(__dirname, '../fixtures/analysis/background.ts'), process.cwd()) expect(config).toEqual({ type: 'experimental-background', }) }) it('should return an empty config if not defined', async () => { - const config = await extractConfigFromFile(resolve(__dirname, '../fixtures/analysis/missing.ts')) + const config = await extractConfigFromFile(resolve(__dirname, '../fixtures/analysis/missing.ts'), process.cwd()) expect(config).toEqual({}) }) it('should return an empty config if config is invalid', async () => { - const config = await extractConfigFromFile(resolve(__dirname, '../fixtures/analysis/invalid.ts')) + const config = await extractConfigFromFile(resolve(__dirname, '../fixtures/analysis/invalid.ts'), process.cwd()) expect(config).toEqual({}) }) it('should extract schedule values from a source file', async () => { - const config = await extractConfigFromFile(resolve(__dirname, '../fixtures/analysis/scheduled.ts')) + const config = await extractConfigFromFile(resolve(__dirname, '../fixtures/analysis/scheduled.ts'), process.cwd()) expect(config).toEqual({ type: 'experimental-scheduled', schedule: '@daily', }) }) it('should throw if schedule is provided when type is background', async () => { - await expect(extractConfigFromFile(resolve(__dirname, '../fixtures/analysis/background-schedule.ts'))).rejects.toThrow( - 'Unsupported config value in test/fixtures/analysis/background-schedule.ts', - ) + await expect( + extractConfigFromFile(resolve(__dirname, '../fixtures/analysis/background-schedule.ts'), process.cwd()), + ).rejects.toThrow('Unsupported config value in test/fixtures/analysis/background-schedule.ts') expect(console.error).toHaveBeenCalledWith( `Invalid config value in test/fixtures/analysis/background-schedule.ts: schedule is not allowed unless type is "experimental-scheduled"`, ) }) it('should throw if schedule is provided when type is default', async () => { - await expect(extractConfigFromFile(resolve(__dirname, '../fixtures/analysis/default-schedule.ts'))).rejects.toThrow( - 'Unsupported config value in test/fixtures/analysis/default-schedule.ts', - ) + await expect( + extractConfigFromFile(resolve(__dirname, '../fixtures/analysis/default-schedule.ts'), process.cwd()), + ).rejects.toThrow('Unsupported config value in test/fixtures/analysis/default-schedule.ts') expect(console.error).toHaveBeenCalledWith( `Invalid config value in test/fixtures/analysis/default-schedule.ts: schedule is not allowed unless type is "experimental-scheduled"`, ) }) it('should throw if schedule is not provided when type is scheduled', async () => { - await expect(extractConfigFromFile(resolve(__dirname, '../fixtures/analysis/missing-schedule.ts'))).rejects.toThrow( - 'Unsupported config value in test/fixtures/analysis/missing-schedule.ts', - ) + await expect( + extractConfigFromFile(resolve(__dirname, '../fixtures/analysis/missing-schedule.ts'), process.cwd()), + ).rejects.toThrow('Unsupported config value in test/fixtures/analysis/missing-schedule.ts') expect(console.error).toHaveBeenCalledWith( `Invalid config value in test/fixtures/analysis/missing-schedule.ts: schedule is required when type is "experimental-scheduled"`, ) }) it('should throw if edge runtime is specified for scheduled functions', async () => { - await expect(extractConfigFromFile(resolve(__dirname, '../fixtures/analysis/scheduled-edge.ts'))).rejects.toThrow( - 'Unsupported config value in test/fixtures/analysis/scheduled-edge.ts', - ) + await expect( + extractConfigFromFile(resolve(__dirname, '../fixtures/analysis/scheduled-edge.ts'), process.cwd()), + ).rejects.toThrow('Unsupported config value in test/fixtures/analysis/scheduled-edge.ts') expect(console.error).toHaveBeenCalledWith( `Invalid config value in test/fixtures/analysis/scheduled-edge.ts: edge runtime is not supported for scheduled functions`, ) }) it('should throw if edge runtime is specified for background functions', async () => { - await expect(extractConfigFromFile(resolve(__dirname, '../fixtures/analysis/background-edge.ts'))).rejects.toThrow( - 'Unsupported config value in test/fixtures/analysis/background-edge.ts', - ) + await expect( + extractConfigFromFile(resolve(__dirname, '../fixtures/analysis/background-edge.ts'), process.cwd()), + ).rejects.toThrow('Unsupported config value in test/fixtures/analysis/background-edge.ts') expect(console.error).toHaveBeenCalledWith( `Invalid config value in test/fixtures/analysis/background-edge.ts: edge runtime is not supported for background functions`, ) From 92dcf3ace1273c5eb97b4d539ae75f92f5faea67 Mon Sep 17 00:00:00 2001 From: Michal Piechowiak Date: Tue, 2 May 2023 08:55:12 +0200 Subject: [PATCH 24/28] Update packages/runtime/src/templates/server.ts Co-authored-by: Nick Taylor --- packages/runtime/src/templates/server.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/runtime/src/templates/server.ts b/packages/runtime/src/templates/server.ts index d241b02fc6..7fb61661eb 100644 --- a/packages/runtime/src/templates/server.ts +++ b/packages/runtime/src/templates/server.ts @@ -83,7 +83,7 @@ const getNetlifyNextServer = (NextServer: NextServerType) => { for (const dynamicRoute in dynamicRoutes) { const { dataRoute, routeRegex } = dynamicRoutes[dynamicRoute] const matches = unlocalizedRoute.match(routeRegex) - if (matches && matches.length !== 0) { + if (matches?.length > 0) { // remove the first match, which is the full route matches.shift() // replace the dynamic segments with the actual values From 66a9670e413aca7959efcdc65901721981a7ee64 Mon Sep 17 00:00:00 2001 From: Michal Piechowiak Date: Tue, 2 May 2023 14:05:34 +0200 Subject: [PATCH 25/28] fix: streamline no-shadow handling --- .eslintrc.js | 3 +++ packages/runtime/src/helpers/types.ts | 2 -- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 69ae1605ad..4c85df8e95 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -48,6 +48,9 @@ module.exports = { 'n/no-unsupported-features/es-syntax': 'off', '@typescript-eslint/no-extra-semi': 'off', 'n/no-missing-import': 'off', + // https://github.com/typescript-eslint/typescript-eslint/issues/2483 + 'no-shadow': 'off', + '@typescript-eslint/no-shadow': 'error', }, }, { diff --git a/packages/runtime/src/helpers/types.ts b/packages/runtime/src/helpers/types.ts index 833d1c2426..92cfadf5cc 100644 --- a/packages/runtime/src/helpers/types.ts +++ b/packages/runtime/src/helpers/types.ts @@ -52,8 +52,6 @@ export interface RoutesManifest { rewrites: Rewrites } -// I have no idea what eslint is up to here but it gives an error -// eslint-disable-next-line no-shadow export const enum ApiRouteType { SCHEDULED = 'experimental-scheduled', BACKGROUND = 'experimental-background', From 8f71783144a4987492727eae9699fc028b53cc5a Mon Sep 17 00:00:00 2001 From: LekoArts Date: Wed, 3 May 2023 08:25:18 +0200 Subject: [PATCH 26/28] chore: automatic linting --- ...package-not-resolvable-in-base-dir.spec.ts | 31 ++++++++++--------- test/templates/server.spec.ts | 2 +- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/test/integration/next-package-not-resolvable-in-base-dir.spec.ts b/test/integration/next-package-not-resolvable-in-base-dir.spec.ts index ac48dbc341..1c84d113ea 100644 --- a/test/integration/next-package-not-resolvable-in-base-dir.spec.ts +++ b/test/integration/next-package-not-resolvable-in-base-dir.spec.ts @@ -1,30 +1,31 @@ +import { tmpdir } from 'os' +import { join } from 'path' + import execa from 'execa' -import * as fs from 'fs-extra' -import * as path from 'path' -import * as os from 'os' -import waitOn from 'wait-on' +import { mkdtempSync, copySync } from 'fs-extra' import fetch from 'node-fetch' +import waitOn from 'wait-on' let destroy = () => {} beforeAll(async () => { - const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), `isolated-test-`)) + const tmpDir = mkdtempSync(join(tmpdir(), `isolated-test-`)) // copy fixture to isolated tmp directory - const siteSrcDir = path.join(__dirname, 'fixtures', 'next-package-not-resolvable-in-base-dir') - const siteDestDir = path.join(tmpDir, 'next-package-not-resolvable-in-base-dir') + const siteSrcDir = join(__dirname, 'fixtures', 'next-package-not-resolvable-in-base-dir') + const siteDestDir = join(tmpDir, 'next-package-not-resolvable-in-base-dir') console.log(`copying fixture site "${siteSrcDir}" to "${siteDestDir}"`) - fs.copySync(siteSrcDir, siteDestDir) + copySync(siteSrcDir, siteDestDir) // bump version so no npm cache tries to use what's in npm registry - const runtimeSrcDir = path.join(__dirname, '..', '..', 'packages', 'runtime') + const runtimeSrcDir = join(__dirname, '..', '..', 'packages', 'runtime') await execa(`npm`, [`version`, `prerelease`, `--no-git-tag-version`], { cwd: runtimeSrcDir, stdio: `inherit` }) // create package tarball const o = await execa(`npm`, [`pack`, `--json`], { cwd: runtimeSrcDir }) const tgzName = JSON.parse(o.stdout)[0].filename - const tgzPath = path.join(runtimeSrcDir, tgzName) + const tgzPath = join(runtimeSrcDir, tgzName) // install runtime from tarball await execa(`npm`, [`install`, tgzPath], { cwd: siteDestDir, stdio: `inherit` }) @@ -36,10 +37,10 @@ beforeAll(async () => { const serveProcess = execa('netlify', ['serve', `-p`, `8888`], { cwd: siteDestDir, stdio: `inherit` }) let shouldRejectOnNonZeroProcessExit = true - serveProcess.catch((e) => { + serveProcess.catch((error) => { isServeRunning = false if (shouldRejectOnNonZeroProcessExit) { - reject(e) + reject(error) } return null }) @@ -62,11 +63,11 @@ beforeAll(async () => { // this is done to ensure that functions packaging worked correctly and doesn't rely on // leftover node_modules that wouldn't be available when functions are deployed to lambdas expect(() => require.resolve(`next`, { paths: [siteDestDir] })).toThrow() - expect(() => require.resolve(`next`, { paths: [path.join(siteDestDir, `next-app`)] })).toThrow() + expect(() => require.resolve(`next`, { paths: [join(siteDestDir, `next-app`)] })).toThrow() return resolve() - } catch (e) { - reject(e) + } catch (error) { + reject(error) } }) }, 3 * 60 * 1000) diff --git a/test/templates/server.spec.ts b/test/templates/server.spec.ts index a42ad13435..2f0155e106 100644 --- a/test/templates/server.spec.ts +++ b/test/templates/server.spec.ts @@ -1,8 +1,8 @@ import { createRequestResponseMocks } from 'next/dist/server/lib/mock-request' import { Options } from 'next/dist/server/next-server' -import { NextServerType, netlifyApiFetch } from '../../packages/runtime/src/templates/handlerUtils' import { getServerFile } from '../../packages/runtime/src/helpers/files' +import { NextServerType, netlifyApiFetch } from '../../packages/runtime/src/templates/handlerUtils' import { getNetlifyNextServer, NetlifyNextServerType, NetlifyConfig } from '../../packages/runtime/src/templates/server' jest.mock('../../packages/runtime/src/templates/handlerUtils', () => { From 9efa6cb4bdac93426d3628911dba359ea53b9b3e Mon Sep 17 00:00:00 2001 From: LekoArts Date: Wed, 3 May 2023 09:14:05 +0200 Subject: [PATCH 27/28] chore: fix linting --- .eslintignore | 3 ++- .eslintrc.js | 9 +++++++++ test/index.spec.ts | 4 ++-- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/.eslintignore b/.eslintignore index cc151edb48..b3d6172497 100644 --- a/.eslintignore +++ b/.eslintignore @@ -8,4 +8,5 @@ packages/runtime/lib packages/runtime/dist-types jestSetup.js test/e2e -test/fixtures/broken_next_config/next.config.js \ No newline at end of file +test/fixtures/broken_next_config/next.config.js +test/integration/fixtures diff --git a/.eslintrc.js b/.eslintrc.js index 3e806ece02..824d4d2e6b 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -77,12 +77,21 @@ module.exports = { 'unicorn/no-await-expression-member': 0, 'import/no-anonymous-default-export': 0, 'no-shadow': 0, + '@typescript-eslint/no-shadow': 0, '@typescript-eslint/no-var-requires': 0, 'require-await': 0, + 'n/no-sync': 0, + 'promise/prefer-await-to-then': 0, + 'no-async-promise-executor': 0, + 'import/no-dynamic-require': 0, // esling-plugin-jest specific rules 'jest/consistent-test-it': ['error', { fn: 'it', withinDescribe: 'it' }], 'jest/no-disabled-tests': 0, 'jest/no-conditional-expect': 0, + "jest/no-standalone-expect": [ + 2, + { "additionalTestBlockFunctions": ["beforeAll"] } + ] }, }, ], diff --git a/test/index.spec.ts b/test/index.spec.ts index 326d39b372..7c10448c7a 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -551,10 +551,10 @@ describe('onBuild()', () => { expect(existsSync(odbHandlerFile)).toBeTruthy() expect(readFileSync(handlerFile, 'utf8')).toMatch( - `({ conf: config, app: \"../../..\", pageRoot, NextServer, staticManifest, mode: 'ssr' })`, + `({ conf: config, app: "../../..", pageRoot, NextServer, staticManifest, mode: 'ssr' })`, ) expect(readFileSync(odbHandlerFile, 'utf8')).toMatch( - `({ conf: config, app: \"../../..\", pageRoot, NextServer, staticManifest, mode: 'odb' })`, + `({ conf: config, app: "../../..", pageRoot, NextServer, staticManifest, mode: 'odb' })`, ) expect(readFileSync(handlerFile, 'utf8')).toMatch(`require("../../../.next/required-server-files.json")`) expect(readFileSync(odbHandlerFile, 'utf8')).toMatch(`require("../../../.next/required-server-files.json")`) From a92dbac0bd8cadaf5c6c1aaa82d683681185af2f Mon Sep 17 00:00:00 2001 From: LekoArts Date: Wed, 3 May 2023 09:14:36 +0200 Subject: [PATCH 28/28] chore: post-commit linting :rolleyes: --- .eslintrc.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 824d4d2e6b..94cbdcf921 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -88,10 +88,7 @@ module.exports = { 'jest/consistent-test-it': ['error', { fn: 'it', withinDescribe: 'it' }], 'jest/no-disabled-tests': 0, 'jest/no-conditional-expect': 0, - "jest/no-standalone-expect": [ - 2, - { "additionalTestBlockFunctions": ["beforeAll"] } - ] + 'jest/no-standalone-expect': [2, { additionalTestBlockFunctions: ['beforeAll'] }], }, }, ],