From 95386c2baf60b761cdd7964cecd72545e230920d Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Mon, 15 May 2023 13:51:03 +0200 Subject: [PATCH 1/8] fix custom server with app dir --- packages/next/src/build/webpack-config.ts | 9 ++ .../server/lib/render-server-standalone.ts | 4 +- packages/next/src/server/next.ts | 106 ++++++++++++++++-- packages/next/src/server/require-hook.ts | 7 ++ test/production/custom-server/app/1/page.js | 5 + test/production/custom-server/app/layout.js | 12 ++ .../custom-server/custom-server.test.ts | 13 +++ test/production/custom-server/pages/2.js | 5 + 8 files changed, 148 insertions(+), 13 deletions(-) create mode 100644 test/production/custom-server/app/1/page.js create mode 100644 test/production/custom-server/app/layout.js create mode 100644 test/production/custom-server/pages/2.js diff --git a/packages/next/src/build/webpack-config.ts b/packages/next/src/build/webpack-config.ts index cdd5c43279903..63530ed60df57 100644 --- a/packages/next/src/build/webpack-config.ts +++ b/packages/next/src/build/webpack-config.ts @@ -1937,6 +1937,15 @@ export default async function getBaseWebpackConfig( }, }, { + issuerLayer: { + or: [ + WEBPACK_LAYERS.server, + WEBPACK_LAYERS.action, + WEBPACK_LAYERS.client, + WEBPACK_LAYERS.appClient, + WEBPACK_LAYERS.shared, + ], + }, test: codeCondition.test, resolve: { alias: { diff --git a/packages/next/src/server/lib/render-server-standalone.ts b/packages/next/src/server/lib/render-server-standalone.ts index 83be239a0079c..cb4778b742103 100644 --- a/packages/next/src/server/lib/render-server-standalone.ts +++ b/packages/next/src/server/lib/render-server-standalone.ts @@ -11,11 +11,13 @@ export const createServerHandler = async ({ port, hostname, dir, + dev = false, minimalMode, }: { port: number hostname: string dir: string + dev?: boolean minimalMode: boolean }) => { const routerWorker = new Worker(renderServerPath, { @@ -60,7 +62,7 @@ export const createServerHandler = async ({ const { port: routerPort } = await routerWorker.initialize({ dir, port, - dev: false, + dev, hostname, minimalMode, workerType: 'router', diff --git a/packages/next/src/server/next.ts b/packages/next/src/server/next.ts index 81fbb5f1e1dc1..3dffcc7be8255 100644 --- a/packages/next/src/server/next.ts +++ b/packages/next/src/server/next.ts @@ -2,6 +2,8 @@ import type { Options as DevServerOptions } from './dev/next-dev-server' import type { NodeRequestHandler } from './next-server' import type { UrlWithParsedQuery } from 'url' import type { NextConfigComplete } from './config-shared' +import type { IncomingMessage, ServerResponse } from 'http' +import type { NextParsedUrlQuery, NextUrlWithParsedQuery } from './request-meta' import './require-hook' import './node-polyfill-fetch' @@ -13,10 +15,10 @@ import { resolve } from 'path' import { NON_STANDARD_NODE_ENV } from '../lib/constants' import { PHASE_DEVELOPMENT_SERVER } from '../shared/lib/constants' import { PHASE_PRODUCTION_SERVER } from '../shared/lib/constants' -import { IncomingMessage, ServerResponse } from 'http' -import { NextUrlWithParsedQuery } from './request-meta' import { getTracer } from './lib/trace/tracer' import { NextServerSpan } from './lib/trace/constants' +import { formatUrl } from '../shared/lib/router/utils/format-url' +import { findPagesDir } from '../lib/find-pages-dir' let ServerImpl: typeof Server @@ -29,6 +31,7 @@ const getServerImpl = async () => { export type NextServerOptions = Partial & { preloadedConfig?: NextConfigComplete + internal_setStandaloneConfig?: boolean } export interface RequestHandler { @@ -127,6 +130,8 @@ export class NextServer { async prepare() { const server = await this.getServer() + if (this.options.internal_setStandaloneConfig) return + // We shouldn't prepare the server in production, // because this code won't be executed when deployed if (this.options.dev) { @@ -167,6 +172,10 @@ export class NextServer { private async getServer() { if (!this.serverPromise) { this.serverPromise = this.loadConfig().then(async (conf) => { + if (this.options.internal_setStandaloneConfig) { + process.env.__NEXT_PRIVATE_STANDALONE_CONFIG = JSON.stringify(conf) + } + if (!this.options.dev) { if (conf.output === 'standalone') { if (!process.env.__NEXT_PRIVATE_STANDALONE_CONFIG) { @@ -181,16 +190,16 @@ export class NextServer { } } - if (this.options.customServer !== false) { - // When running as a custom server with app dir, we must set this env - // to correctly alias the React versions. - if (conf.experimental.appDir) { - process.env.__NEXT_PRIVATE_PREBUNDLED_REACT = conf.experimental - .serverActions - ? 'experimental' - : 'next' - } - } + // if (this.options.customServer !== false) { + // // When running as a custom server with app dir, we must set this env + // // to correctly alias the React versions. + // if (conf.experimental.appDir) { + // process.env.__NEXT_PRIVATE_PREBUNDLED_REACT = conf.experimental + // .serverActions + // ? 'experimental' + // : 'next' + // } + // } this.server = await this.createServer({ ...this.options, @@ -250,6 +259,79 @@ function createServer(options: NextServerOptions): NextServer { ) } + if (options.customServer !== false) { + const dir = resolve(options.dir || '.') + const { appDir } = findPagesDir(dir, true) + + // If the `app` dir exists, we'll need to run the standalone server to have + // both types of renderers (pages, app) running in separated processes, + // instead of having the Next server only. + if (appDir) { + const { createServerHandler } = + require('./lib/render-server-standalone') as typeof import('./lib/render-server-standalone') + + const fallbackServer = new NextServer({ + ...options, + internal_setStandaloneConfig: true, + }) + + let handlerPromise: Promise> + + const app = new Proxy( + {}, + { + get: function (_, propKey) { + switch (propKey) { + case 'getRequestHandler': { + return () => { + handlerPromise = + handlerPromise || + createServerHandler({ + port: options.port || 3000, + dev: options.dev, + dir, + hostname: options.hostname || 'localhost', + minimalMode: false, + }) + + return async (req: IncomingMessage, res: ServerResponse) => { + const handler = await handlerPromise + return handler(req, res) + } + } + } + case 'render': { + return async ( + req: IncomingMessage, + res: ServerResponse, + pathname: string, + query?: NextParsedUrlQuery, + parsedUrl?: NextUrlWithParsedQuery + ) => { + const handler = await handlerPromise + req.url = formatUrl({ + ...parsedUrl, + pathname, + query, + }) + return handler(req, res) + } + } + default: { + const method = fallbackServer[propKey as keyof NextServer] + if (typeof method === 'function') { + return method.bind(fallbackServer) + } + } + } + }, + } + ) + + return app as any + } + } + return new NextServer(options) } diff --git a/packages/next/src/server/require-hook.ts b/packages/next/src/server/require-hook.ts index 64c287dab0a70..617c0c50150d8 100644 --- a/packages/next/src/server/require-hook.ts +++ b/packages/next/src/server/require-hook.ts @@ -177,6 +177,13 @@ mod._resolveFilename = function ( } const hookResolved = requestMap.get(request) + // if (request == 'react') + // console.trace( + // process.env.__NEXT_PRIVATE_PREBUNDLED_REACT, + // process.env.__NEXT_PRIVATE_RENDER_WORKER, + // request, + // hookResolved + // ) if (hookResolved) request = hookResolved return originalResolveFilename.call(mod, request, parent, isMain, options) diff --git a/test/production/custom-server/app/1/page.js b/test/production/custom-server/app/1/page.js new file mode 100644 index 0000000000000..e9f5bca049b4d --- /dev/null +++ b/test/production/custom-server/app/1/page.js @@ -0,0 +1,5 @@ +import { version } from 'react' + +export default function Page() { + return
app: {version}
+} diff --git a/test/production/custom-server/app/layout.js b/test/production/custom-server/app/layout.js new file mode 100644 index 0000000000000..8525f5f8c0b2a --- /dev/null +++ b/test/production/custom-server/app/layout.js @@ -0,0 +1,12 @@ +export const metadata = { + title: 'Next.js', + description: 'Generated by Next.js', +} + +export default function RootLayout({ children }) { + return ( + + {children} + + ) +} diff --git a/test/production/custom-server/custom-server.test.ts b/test/production/custom-server/custom-server.test.ts index 532f219e4cab0..e7169bf50ee16 100644 --- a/test/production/custom-server/custom-server.test.ts +++ b/test/production/custom-server/custom-server.test.ts @@ -11,5 +11,18 @@ createNextDescribe( const $ = await next.render$(`/${page}`) expect($('p').text()).toBe(`Page ${page}`) }) + + describe('with app dir', () => { + it('should render app with react canary', async () => { + const $ = await next.render$(`/1`) + expect($('body').text()).toMatch(/app: .+-canary/) + }) + + it('should render pages with react stable', async () => { + const $ = await next.render$(`/2`) + expect($('body').text()).toMatch(/pages:/) + expect($('body').text()).not.toMatch(/canary/) + }) + }) } ) diff --git a/test/production/custom-server/pages/2.js b/test/production/custom-server/pages/2.js new file mode 100644 index 0000000000000..532d8319a9a40 --- /dev/null +++ b/test/production/custom-server/pages/2.js @@ -0,0 +1,5 @@ +import { version } from 'react' + +export default function Page() { + return
pages: {version}
+} From 2b80b8566f79ce70369e5b007519b2e7a274a2af Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Mon, 15 May 2023 13:52:12 +0200 Subject: [PATCH 2/8] remove comment --- packages/next/src/server/next.ts | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/packages/next/src/server/next.ts b/packages/next/src/server/next.ts index 3dffcc7be8255..0cd32321626b8 100644 --- a/packages/next/src/server/next.ts +++ b/packages/next/src/server/next.ts @@ -190,17 +190,6 @@ export class NextServer { } } - // if (this.options.customServer !== false) { - // // When running as a custom server with app dir, we must set this env - // // to correctly alias the React versions. - // if (conf.experimental.appDir) { - // process.env.__NEXT_PRIVATE_PREBUNDLED_REACT = conf.experimental - // .serverActions - // ? 'experimental' - // : 'next' - // } - // } - this.server = await this.createServer({ ...this.options, conf, From 2d6df9e3036600ea10787ca21085fde6f476395f Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Mon, 15 May 2023 13:53:23 +0200 Subject: [PATCH 3/8] remove logs --- packages/next/src/server/require-hook.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/packages/next/src/server/require-hook.ts b/packages/next/src/server/require-hook.ts index 617c0c50150d8..64c287dab0a70 100644 --- a/packages/next/src/server/require-hook.ts +++ b/packages/next/src/server/require-hook.ts @@ -177,13 +177,6 @@ mod._resolveFilename = function ( } const hookResolved = requestMap.get(request) - // if (request == 'react') - // console.trace( - // process.env.__NEXT_PRIVATE_PREBUNDLED_REACT, - // process.env.__NEXT_PRIVATE_RENDER_WORKER, - // request, - // hookResolved - // ) if (hookResolved) request = hookResolved return originalResolveFilename.call(mod, request, parent, isMain, options) From 948c51034eb3f2655fe1afa0c01e7776337770ed Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Tue, 16 May 2023 21:26:29 +0200 Subject: [PATCH 4/8] fix --- packages/next/src/server/next.ts | 135 ++++++++++++++++++++----------- 1 file changed, 88 insertions(+), 47 deletions(-) diff --git a/packages/next/src/server/next.ts b/packages/next/src/server/next.ts index 0cd32321626b8..6f578d53b6023 100644 --- a/packages/next/src/server/next.ts +++ b/packages/next/src/server/next.ts @@ -11,14 +11,17 @@ import './node-polyfill-crypto' import { default as Server } from './next-server' import * as log from '../build/output/log' import loadConfig from './config' -import { resolve } from 'path' +import { join, resolve } from 'path' import { NON_STANDARD_NODE_ENV } from '../lib/constants' -import { PHASE_DEVELOPMENT_SERVER } from '../shared/lib/constants' +import { + PHASE_DEVELOPMENT_SERVER, + SERVER_DIRECTORY, +} from '../shared/lib/constants' import { PHASE_PRODUCTION_SERVER } from '../shared/lib/constants' import { getTracer } from './lib/trace/tracer' import { NextServerSpan } from './lib/trace/constants' import { formatUrl } from '../shared/lib/router/utils/format-url' -import { findPagesDir } from '../lib/find-pages-dir' +import { findDir } from '../lib/find-pages-dir' let ServerImpl: typeof Server @@ -42,11 +45,17 @@ export interface RequestHandler { ): Promise } +const SYMBOL_SET_STANDALONE_MODE = Symbol('next.set_standalone_mode') +const SYMBOL_LOAD_CONFIG = Symbol('next.load_config') + export class NextServer { private serverPromise?: Promise private server?: Server private reqHandlerPromise?: Promise private preparedAssetPrefix?: string + + private standaloneMode?: boolean + public options: NextServerOptions constructor(options: NextServerOptions) { @@ -61,6 +70,10 @@ export class NextServer { return this.options.port } + [SYMBOL_SET_STANDALONE_MODE]() { + this.standaloneMode = true + } + getRequestHandler(): RequestHandler { return async ( req: IncomingMessage, @@ -130,7 +143,7 @@ export class NextServer { async prepare() { const server = await this.getServer() - if (this.options.internal_setStandaloneConfig) return + if (this.standaloneMode) return // We shouldn't prepare the server in production, // because this code won't be executed when deployed @@ -156,7 +169,7 @@ export class NextServer { return server } - private async loadConfig() { + private async [SYMBOL_LOAD_CONFIG]() { return ( this.options.preloadedConfig || loadConfig( @@ -171,8 +184,8 @@ export class NextServer { private async getServer() { if (!this.serverPromise) { - this.serverPromise = this.loadConfig().then(async (conf) => { - if (this.options.internal_setStandaloneConfig) { + this.serverPromise = this[SYMBOL_LOAD_CONFIG]().then(async (conf) => { + if (this.standaloneMode) { process.env.__NEXT_PRIVATE_STANDALONE_CONFIG = JSON.stringify(conf) } @@ -249,30 +262,47 @@ function createServer(options: NextServerOptions): NextServer { } if (options.customServer !== false) { - const dir = resolve(options.dir || '.') - const { appDir } = findPagesDir(dir, true) - // If the `app` dir exists, we'll need to run the standalone server to have // both types of renderers (pages, app) running in separated processes, // instead of having the Next server only. - if (appDir) { - const { createServerHandler } = - require('./lib/render-server-standalone') as typeof import('./lib/render-server-standalone') - - const fallbackServer = new NextServer({ - ...options, - internal_setStandaloneConfig: true, - }) + let shouldUseStandaloneMode = false - let handlerPromise: Promise> + const dir = resolve(options.dir || '.') + const server = new NextServer(options) + + const { createServerHandler } = + require('./lib/render-server-standalone') as typeof import('./lib/render-server-standalone') + + let handlerPromise: Promise> + + return new Proxy( + {}, + { + get: function (_, propKey) { + switch (propKey) { + case 'prepare': + return async () => { + // Instead of running Next Server's `prepare`, we'll run the loadConfig first to determine + // if we should run the standalone server or not. + const config = await server[SYMBOL_LOAD_CONFIG]() + + // Check if the application has app dir or not. This depends on the mode (dev or prod). + // For dev, `app` should be existing in the sources and for prod it should be existing + // in the dist folder. + const distDir = + process.env.NEXT_RUNTIME === 'edge' + ? config.distDir + : join(dir, config.distDir) + const serverDistDir = join(distDir, SERVER_DIRECTORY) + const hasAppDir = !!findDir( + options.dev ? dir : serverDistDir, + 'app' + ) + + if (hasAppDir) { + shouldUseStandaloneMode = true + server[SYMBOL_SET_STANDALONE_MODE]() - const app = new Proxy( - {}, - { - get: function (_, propKey) { - switch (propKey) { - case 'getRequestHandler': { - return () => { handlerPromise = handlerPromise || createServerHandler({ @@ -282,21 +312,32 @@ function createServer(options: NextServerOptions): NextServer { hostname: options.hostname || 'localhost', minimalMode: false, }) - - return async (req: IncomingMessage, res: ServerResponse) => { + } else { + return server.prepare() + } + } + case 'getRequestHandler': { + return () => { + let handler: RequestHandler + return async (req: IncomingMessage, res: ServerResponse) => { + if (shouldUseStandaloneMode) { const handler = await handlerPromise return handler(req, res) } + handler = handler || server.getRequestHandler() + return handler(req, res) } } - case 'render': { - return async ( - req: IncomingMessage, - res: ServerResponse, - pathname: string, - query?: NextParsedUrlQuery, - parsedUrl?: NextUrlWithParsedQuery - ) => { + } + case 'render': { + return async ( + req: IncomingMessage, + res: ServerResponse, + pathname: string, + query?: NextParsedUrlQuery, + parsedUrl?: NextUrlWithParsedQuery + ) => { + if (shouldUseStandaloneMode) { const handler = await handlerPromise req.url = formatUrl({ ...parsedUrl, @@ -305,20 +346,20 @@ function createServer(options: NextServerOptions): NextServer { }) return handler(req, res) } + + return server.render(req, res, pathname, query, parsedUrl) } - default: { - const method = fallbackServer[propKey as keyof NextServer] - if (typeof method === 'function') { - return method.bind(fallbackServer) - } + } + default: { + const method = server[propKey as keyof NextServer] + if (typeof method === 'function') { + return method.bind(server) } } - }, - } - ) - - return app as any - } + } + }, + } + ) as any } return new NextServer(options) From 44a30c368ea6aa591199acdeb38ca4e1448ed924 Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Wed, 17 May 2023 14:51:11 +0200 Subject: [PATCH 5/8] fix lint errors --- packages/next/src/server/next.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/next/src/server/next.ts b/packages/next/src/server/next.ts index 6f578d53b6023..0a6f8e29a4bbe 100644 --- a/packages/next/src/server/next.ts +++ b/packages/next/src/server/next.ts @@ -321,8 +321,8 @@ function createServer(options: NextServerOptions): NextServer { let handler: RequestHandler return async (req: IncomingMessage, res: ServerResponse) => { if (shouldUseStandaloneMode) { - const handler = await handlerPromise - return handler(req, res) + const standaloneHandler = await handlerPromise + return standaloneHandler(req, res) } handler = handler || server.getRequestHandler() return handler(req, res) From 6d6a926c5e3312b9018e4628280181f8ffd730f8 Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Wed, 17 May 2023 17:57:18 +0200 Subject: [PATCH 6/8] fix issuerLayer --- packages/next/src/build/webpack-config.ts | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/packages/next/src/build/webpack-config.ts b/packages/next/src/build/webpack-config.ts index 63530ed60df57..2e63bd5bbe187 100644 --- a/packages/next/src/build/webpack-config.ts +++ b/packages/next/src/build/webpack-config.ts @@ -1937,15 +1937,7 @@ export default async function getBaseWebpackConfig( }, }, { - issuerLayer: { - or: [ - WEBPACK_LAYERS.server, - WEBPACK_LAYERS.action, - WEBPACK_LAYERS.client, - WEBPACK_LAYERS.appClient, - WEBPACK_LAYERS.shared, - ], - }, + issuerLayer: WEBPACK_LAYERS.appClient, test: codeCondition.test, resolve: { alias: { From 1475548b92f48027811129e4414b27d5a1f8faeb Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Wed, 17 May 2023 22:01:38 +0200 Subject: [PATCH 7/8] try disabling layer --- packages/next/src/build/webpack-config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/next/src/build/webpack-config.ts b/packages/next/src/build/webpack-config.ts index 2e63bd5bbe187..7c68955fe5e3b 100644 --- a/packages/next/src/build/webpack-config.ts +++ b/packages/next/src/build/webpack-config.ts @@ -1937,7 +1937,7 @@ export default async function getBaseWebpackConfig( }, }, { - issuerLayer: WEBPACK_LAYERS.appClient, + // issuerLayer: WEBPACK_LAYERS.appClient, test: codeCondition.test, resolve: { alias: { From 8f4706d368dd3b7abd4df7df7ebd3b30e9a6356e Mon Sep 17 00:00:00 2001 From: Shu Ding Date: Wed, 17 May 2023 22:52:10 +0200 Subject: [PATCH 8/8] remove comment --- packages/next/src/build/webpack-config.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/next/src/build/webpack-config.ts b/packages/next/src/build/webpack-config.ts index 7c68955fe5e3b..cdd5c43279903 100644 --- a/packages/next/src/build/webpack-config.ts +++ b/packages/next/src/build/webpack-config.ts @@ -1937,7 +1937,6 @@ export default async function getBaseWebpackConfig( }, }, { - // issuerLayer: WEBPACK_LAYERS.appClient, test: codeCondition.test, resolve: { alias: {