diff --git a/docs/advanced-features/react-18/server-components.md b/docs/advanced-features/react-18/server-components.md index 4426f7eda2e7c..2a919d97598bd 100644 --- a/docs/advanced-features/react-18/server-components.md +++ b/docs/advanced-features/react-18/server-components.md @@ -94,15 +94,7 @@ export default function Document() { ### `next/app` -If you're using `_app.js`, the usage is the same as [Custom App](/docs/advanced-features/custom-app). -If you're using `_app.server.js` as a server component, see the example below where it only receives the `children` prop as React elements. You can wrap any other client or server components around `children` to customize the layout of your app. - -```js -// pages/_app.server.js -export default function App({ children }) { - return children -} -``` +The usage of `_app.js` is the same as [Custom App](/docs/advanced-features/custom-app). Using custom app as server component such as `_app.server.js` is not recommended, to keep align with non server components apps for client specific things like global CSS imports. ### Routing diff --git a/packages/next/build/entries.ts b/packages/next/build/entries.ts index 0dc2a33bcc4ed..f6f7bec71a5e8 100644 --- a/packages/next/build/entries.ts +++ b/packages/next/build/entries.ts @@ -28,8 +28,8 @@ import { import { __ApiPreviewProps } from '../server/api-utils' import { isTargetLikeServerless } from '../server/utils' import { warn } from './output/log' +import { isServerComponentPage } from './utils' import { getPageStaticInfo } from './analysis/get-page-static-info' -import { isServerComponentPage, withoutRSCExtensions } from './utils' import { normalizePathSep } from '../shared/lib/page-path/normalize-path-sep' import { normalizePagePath } from '../shared/lib/page-path/normalize-page-path' import { serverComponentRegex } from './webpack/loaders/utils' @@ -37,17 +37,11 @@ import { serverComponentRegex } from './webpack/loaders/utils' type ObjectValue = T extends { [key: string]: infer V } ? V : never /** - * For a given page path removes the provided extensions. `/_app.server` is a - * special case because it is the only page where we want to preserve the RSC - * server extension. + * For a given page path removes the provided extensions. */ export function getPageFromPath(pagePath: string, pageExtensions: string[]) { - const extensions = pagePath.includes('/_app.server.') - ? withoutRSCExtensions(pageExtensions) - : pageExtensions - let page = normalizePathSep( - pagePath.replace(new RegExp(`\\.+(${extensions.join('|')})$`), '') + pagePath.replace(new RegExp(`\\.+(${pageExtensions.join('|')})$`), '') ) page = page.replace(/\/index$/, '') @@ -118,7 +112,6 @@ export function createPagesMapping({ if (isDev) { delete pages['/_app'] - delete pages['/_app.server'] delete pages['/_error'] delete pages['/_document'] } @@ -132,7 +125,6 @@ export function createPagesMapping({ '/_app': `${root}/_app`, '/_error': `${root}/_error`, '/_document': `${root}/_document`, - ...(hasServerComponents ? { '/_app.server': `${root}/_app.server` } : {}), ...pages, } } @@ -175,7 +167,6 @@ export function getEdgeServerEntry(opts: { const loaderParams: MiddlewareSSRLoaderQuery = { absolute500Path: opts.pages['/500'] || '', absoluteAppPath: opts.pages['/_app'], - absoluteAppServerPath: opts.pages['/_app.server'], absoluteDocumentPath: opts.pages['/_document'], absoluteErrorPath: opts.pages['/_error'], absolutePagePath: opts.absolutePagePath, @@ -219,7 +210,6 @@ export function getServerlessEntry(opts: { const loaderParams: ServerlessLoaderQuery = { absolute404Path: opts.pages['/404'] || '', absoluteAppPath: opts.pages['/_app'], - absoluteAppServerPath: opts.pages['/_app.server'], absoluteDocumentPath: opts.pages['/_document'], absoluteErrorPath: opts.pages['/_error'], absolutePagePath: opts.absolutePagePath, diff --git a/packages/next/build/utils.ts b/packages/next/build/utils.ts index 7639b9a4721cc..5ab0c775c5ef9 100644 --- a/packages/next/build/utils.ts +++ b/packages/next/build/utils.ts @@ -141,12 +141,6 @@ export async function printTreeView( ] const hasCustomApp = await findPageFile(pagesDir, '/_app', pageExtensions) - const hasCustomAppServer = await findPageFile( - pagesDir, - '/_app.server', - pageExtensions - ) - pageInfos.set('/404', { ...(pageInfos.get('/404') || pageInfos.get('/_error')), static: useStatic404, @@ -172,8 +166,7 @@ export async function printTreeView( !( e === '/_document' || e === '/_error' || - (!hasCustomApp && e === '/_app') || - (!hasCustomAppServer && e === '/_app.server') + (!hasCustomApp && e === '/_app') ) ) .sort((a, b) => a.localeCompare(b)) diff --git a/packages/next/build/webpack-config.ts b/packages/next/build/webpack-config.ts index 810674d8fdd91..bb73efb05c804 100644 --- a/packages/next/build/webpack-config.ts +++ b/packages/next/build/webpack-config.ts @@ -606,19 +606,12 @@ export default async function getBaseWebpackConfig( if (dev) { customAppAliases[`${PAGES_DIR_ALIAS}/_app`] = [ - ...rawPageExtensions.reduce((prev, ext) => { + ...config.pageExtensions.reduce((prev, ext) => { prev.push(path.join(pagesDir, `_app.${ext}`)) return prev }, [] as string[]), 'next/dist/pages/_app.js', ] - customAppAliases[`${PAGES_DIR_ALIAS}/_app.server`] = [ - ...rawPageExtensions.reduce((prev, ext) => { - prev.push(path.join(pagesDir, `_app.server.${ext}`)) - return prev - }, [] as string[]), - 'next/dist/pages/_app.server.js', - ] customAppAliases[`${PAGES_DIR_ALIAS}/_error`] = [ ...config.pageExtensions.reduce((prev, ext) => { prev.push(path.join(pagesDir, `_error.${ext}`)) diff --git a/packages/next/build/webpack/loaders/next-middleware-ssr-loader/index.ts b/packages/next/build/webpack/loaders/next-middleware-ssr-loader/index.ts index 5ac54d0992ecd..5feb565ac9043 100644 --- a/packages/next/build/webpack/loaders/next-middleware-ssr-loader/index.ts +++ b/packages/next/build/webpack/loaders/next-middleware-ssr-loader/index.ts @@ -4,7 +4,6 @@ import { stringifyRequest } from '../../stringify-request' export type MiddlewareSSRLoaderQuery = { absolute500Path: string absoluteAppPath: string - absoluteAppServerPath: string absoluteDocumentPath: string absoluteErrorPath: string absolutePagePath: string @@ -22,7 +21,6 @@ export default async function middlewareSSRLoader(this: any) { buildId, absolutePagePath, absoluteAppPath, - absoluteAppServerPath, absoluteDocumentPath, absolute500Path, absoluteErrorPath, @@ -42,10 +40,6 @@ export default async function middlewareSSRLoader(this: any) { const stringifiedPagePath = stringifyRequest(this, absolutePagePath) const stringifiedAppPath = stringifyRequest(this, absoluteAppPath) - const stringifiedAppServerPath = absoluteAppServerPath - ? stringifyRequest(this, absoluteAppServerPath) - : null - const stringifiedErrorPath = stringifyRequest(this, absoluteErrorPath) const stringifiedDocumentPath = stringifyRequest(this, absoluteDocumentPath) const stringified500Path = absolute500Path @@ -61,9 +55,6 @@ export default async function middlewareSSRLoader(this: any) { import Document from ${stringifiedDocumentPath} const appMod = require(${stringifiedAppPath}) - const appServerMod = ${ - stringifiedAppServerPath ? `require(${stringifiedAppServerPath})` : 'null' - } const pageMod = require(${stringifiedPagePath}) const errorMod = require(${stringifiedErrorPath}) const error500Mod = ${ @@ -85,7 +76,6 @@ export default async function middlewareSSRLoader(this: any) { buildManifest, reactLoadableManifest, serverComponentManifest: ${isServerComponent} ? rscManifest : null, - appServerMod, config: ${stringifiedConfig}, buildId: ${JSON.stringify(buildId)}, }) diff --git a/packages/next/build/webpack/loaders/next-middleware-ssr-loader/render.ts b/packages/next/build/webpack/loaders/next-middleware-ssr-loader/render.ts index e186b66087d20..2013b9058e2cf 100644 --- a/packages/next/build/webpack/loaders/next-middleware-ssr-loader/render.ts +++ b/packages/next/build/webpack/loaders/next-middleware-ssr-loader/render.ts @@ -27,7 +27,6 @@ export function getRender({ serverComponentManifest, config, buildId, - appServerMod, }: { dev: boolean page: string @@ -49,8 +48,6 @@ export function getRender({ reactLoadableManifest, Document, App: appMod.default as AppType, - AppMod: appMod, - AppServerMod: appServerMod, } const server = new WebServer({ diff --git a/packages/next/build/webpack/loaders/next-serverless-loader/index.ts b/packages/next/build/webpack/loaders/next-serverless-loader/index.ts index 0141539a841db..533f4d64aa7a2 100644 --- a/packages/next/build/webpack/loaders/next-serverless-loader/index.ts +++ b/packages/next/build/webpack/loaders/next-serverless-loader/index.ts @@ -18,7 +18,6 @@ export type ServerlessLoaderQuery = { distDir: string absolutePagePath: string absoluteAppPath: string - absoluteAppServerPath: string absoluteDocumentPath: string absoluteErrorPath: string absolute404Path: string diff --git a/packages/next/build/webpack/loaders/next-serverless-loader/page-handler.ts b/packages/next/build/webpack/loaders/next-serverless-loader/page-handler.ts index 806299a229f48..30b1daf40e415 100644 --- a/packages/next/build/webpack/loaders/next-serverless-loader/page-handler.ts +++ b/packages/next/build/webpack/loaders/next-serverless-loader/page-handler.ts @@ -65,7 +65,7 @@ export function getPageHandler(ctx: ServerlessHandlerCtx) { _params?: any ) { let Component - let AppMod + let App let config let Document let Error @@ -78,7 +78,7 @@ export function getPageHandler(ctx: ServerlessHandlerCtx) { getServerSideProps, getStaticPaths, Component, - AppMod, + App, config, { default: Document }, { default: Error }, @@ -103,7 +103,7 @@ export function getPageHandler(ctx: ServerlessHandlerCtx) { setLazyProp({ req: req as any }, 'cookies', getCookieParser(req.headers)) const options = { - AppMod, + App, Document, ComponentMod: { default: Component }, buildManifest, diff --git a/packages/next/build/webpack/plugins/flight-manifest-plugin.ts b/packages/next/build/webpack/plugins/flight-manifest-plugin.ts index f644db2dc9290..f0362f7c1b1a4 100644 --- a/packages/next/build/webpack/plugins/flight-manifest-plugin.ts +++ b/packages/next/build/webpack/plugins/flight-manifest-plugin.ts @@ -96,8 +96,6 @@ export class FlightManifestPlugin { // For each SC server compilation entry, we need to create its corresponding // client component entry. for (const [name, entry] of compilation.entries.entries()) { - if (name === 'pages/_app.server') continue - // Check if the page entry is a server component or not. const entryDependency = entry.dependencies?.[0] const request = entryDependency?.request diff --git a/packages/next/build/webpack/plugins/pages-manifest-plugin.ts b/packages/next/build/webpack/plugins/pages-manifest-plugin.ts index 4cb3e3640511c..068d732d13ba7 100644 --- a/packages/next/build/webpack/plugins/pages-manifest-plugin.ts +++ b/packages/next/build/webpack/plugins/pages-manifest-plugin.ts @@ -60,7 +60,7 @@ export default class PagesManifestPlugin implements webpack.Plugin { file.endsWith('.js') ) - // Skip _app.server entry which is empty + // Skip entries which are empty if (!files.length) { continue } diff --git a/packages/next/client/index.tsx b/packages/next/client/index.tsx index 662ba6b8bd147..f3b03a6a85d8a 100644 --- a/packages/next/client/index.tsx +++ b/packages/next/client/index.tsx @@ -86,7 +86,6 @@ let webpackHMR: any let CachedApp: AppComponent, onPerfEntry: (metric: any) => void let CachedComponent: React.ComponentType -let isRSCPage: boolean class Container extends React.Component<{ fn: (err: Error, info?: any) => void @@ -331,7 +330,6 @@ export async function hydrate(opts?: { beforeRender?: () => Promise }) { throw pageEntrypoint.error } CachedComponent = pageEntrypoint.component - isRSCPage = !!pageEntrypoint.exports.__next_rsc__ if (process.env.NODE_ENV !== 'production') { const { isValidElementType } = require('next/dist/compiled/react-is') @@ -647,12 +645,7 @@ function AppContainer({ } function renderApp(App: AppComponent, appProps: AppProps) { - if (process.env.__NEXT_RSC && isRSCPage) { - const { Component, err: _, router: __, ...props } = appProps - return - } else { - return - } + return } const wrapApp = diff --git a/packages/next/client/next-dev.js b/packages/next/client/next-dev.js index a98a45213902a..7faab941066a1 100644 --- a/packages/next/client/next-dev.js +++ b/packages/next/client/next-dev.js @@ -15,7 +15,7 @@ if (!window._nextSetupHydrationWarning) { const isHydrateError = args.some( (arg) => typeof arg === 'string' && - arg.match(/Warning:.*?did not match.*?Server:/) + arg.match(/(hydration|content does not match|did not match)/i) ) if (isHydrateError) { args = [ diff --git a/packages/next/export/index.ts b/packages/next/export/index.ts index fd08ec9ab0b6d..669a6a1c5708d 100644 --- a/packages/next/export/index.ts +++ b/packages/next/export/index.ts @@ -237,12 +237,7 @@ export default async function exportApp( continue } - if ( - page === '/_document' || - page === '/_app.server' || - page === '/_app' || - page === '/_error' - ) { + if (page === '/_document' || page === '/_app' || page === '/_error') { continue } diff --git a/packages/next/pages/_app.server.tsx b/packages/next/pages/_app.server.tsx deleted file mode 100644 index 8dbc25dc5634e..0000000000000 --- a/packages/next/pages/_app.server.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export default function AppServer({ children }: { children: React.ReactNode }) { - return children -} diff --git a/packages/next/server/load-components.ts b/packages/next/server/load-components.ts index b8cebe97259cc..7db624cfb4483 100644 --- a/packages/next/server/load-components.ts +++ b/packages/next/server/load-components.ts @@ -40,8 +40,6 @@ export type LoadComponentsReturnType = { getStaticPaths?: GetStaticPaths getServerSideProps?: GetServerSideProps ComponentMod: any - AppMod: any - AppServerMod: any isViewPath?: boolean } @@ -60,9 +58,6 @@ export async function loadDefaultErrorComponents(distDir: string) { buildManifest: require(join(distDir, `fallback-${BUILD_MANIFEST}`)), reactLoadableManifest: {}, ComponentMod, - AppMod, - // Use App for fallback - AppServerMod: AppMod, } } @@ -106,7 +101,7 @@ export async function loadComponents( } as LoadComponentsReturnType } - const [DocumentMod, AppMod, ComponentMod, AppServerMod] = await Promise.all([ + const [DocumentMod, AppMod, ComponentMod] = await Promise.all([ Promise.resolve().then(() => requirePage('/_document', distDir, serverless, rootEnabled) ), @@ -116,11 +111,6 @@ export async function loadComponents( Promise.resolve().then(() => requirePage(pathname, distDir, serverless, rootEnabled) ), - hasServerComponents - ? Promise.resolve().then(() => - requirePage('/_app.server', distDir, serverless, rootEnabled) - ) - : null, ]) const [buildManifest, reactLoadableManifest, serverComponentManifest] = @@ -175,8 +165,6 @@ export async function loadComponents( reactLoadableManifest, pageConfig: ComponentMod.config || {}, ComponentMod, - AppMod, - AppServerMod, getServerSideProps, getStaticProps, getStaticPaths, diff --git a/packages/next/server/render.tsx b/packages/next/server/render.tsx index f5d421ff47e04..bfb3a0628b838 100644 --- a/packages/next/server/render.tsx +++ b/packages/next/server/render.tsx @@ -79,7 +79,6 @@ import { continueFromInitialStream, } from './node-web-streams-helper' import { ImageConfigContext } from '../shared/lib/image-config-context' -import { interopDefault } from '../lib/interop-default' import stripAnsi from 'next/dist/compiled/strip-ansi' import { urlQueryToSearchParams } from '../shared/lib/router/utils/querystring' import { postProcessHTML } from './post-process' @@ -197,20 +196,10 @@ function enhanceComponents( } function renderPageTree( - App: any, - Component: any, - props: any, - isServerComponent: boolean + App: AppType, + Component: NextComponentType, + props: any ) { - const { router: _, ...rest } = props - if (isServerComponent) { - return ( - - - - ) - } - return } @@ -382,7 +371,6 @@ function useFlightResponse({ // Create the wrapper component for a Flight stream. function createServerComponentRenderer( - App: any, Component: any, { cachePrefix, @@ -400,9 +388,7 @@ function createServerComponentRenderer( const id = (React as any).useId() const reqStream: ReadableStream = renderToReadableStream( - - - , + , serverComponentManifest ) @@ -466,8 +452,7 @@ export async function renderToHTML( images, runtime: globalRuntime, ComponentMod, - AppMod, - AppServerMod, + App, } = renderOpts let Document = renderOpts.Document @@ -483,8 +468,6 @@ export async function renderToHTML( renderOpts.Component const OriginComponent = Component - const App = interopDefault(isServerComponent ? AppServerMod : AppMod) - let serverComponentsInlinedTransformStream: TransformStream< Uint8Array, Uint8Array @@ -504,7 +487,7 @@ export async function renderToHTML( globalThis.__webpack_require__ = ComponentMod.__next_rsc__.__webpack_require__ - Component = createServerComponentRenderer(App, Component, { + Component = createServerComponentRenderer(Component, { cachePrefix: pathname + (search ? `?${search}` : ''), inlinedTransformStream: serverComponentsInlinedTransformStream, pageData: serverComponentsStaticPageData, @@ -722,12 +705,7 @@ export async function renderToHTML( AppTree: (props: any) => { return ( - {renderPageTree( - App, - OriginComponent, - { ...props, router }, - isServerComponent - )} + {renderPageTree(App, OriginComponent, { ...props, router })} ) }, @@ -1191,15 +1169,7 @@ export async function renderToHTML( if (renderServerComponentData) { return new RenderResult( renderToReadableStream( - renderPageTree( - App, - OriginComponent, - { - ...props.pageProps, - ...serverComponentProps, - }, - true - ), + , serverComponentManifest ).pipeThrough(createBufferedTransformStream()) ) @@ -1327,12 +1297,10 @@ export async function renderToHTML( const html = ReactDOMServer.renderToString( - {renderPageTree( - EnhancedApp, - EnhancedComponent, - { ...props, router }, - false - )} + {renderPageTree(EnhancedApp, EnhancedComponent, { + ...props, + router, + })} ) @@ -1367,13 +1335,10 @@ export async function renderToHTML( ) : ( - {renderPageTree( - // AppServer is included in the EnhancedComponent in ServerComponentWrapper - isServerComponent ? React.Fragment : EnhancedApp, - EnhancedComponent, - { ...(isServerComponent ? props.pageProps : props), router }, - isServerComponent - )} + {renderPageTree(EnhancedApp, EnhancedComponent, { + ...props, + router, + })} ) diff --git a/packages/next/taskfile.js b/packages/next/taskfile.js index b912e726ff40e..58b343b2617bd 100644 --- a/packages/next/taskfile.js +++ b/packages/next/taskfile.js @@ -1858,13 +1858,6 @@ export async function pages_app(task, opts) { .target('dist/pages') } -export async function pages_app_server(task, opts) { - await task - .source('pages/_app.server.tsx') - .swc('client', { dev: opts.dev, keepImportAssertions: true }) - .target('dist/pages') -} - export async function pages_error(task, opts) { await task .source('pages/_error.tsx') @@ -1880,10 +1873,7 @@ export async function pages_document(task, opts) { } export async function pages(task, opts) { - await task.parallel( - ['pages_app', 'pages_app_server', 'pages_error', 'pages_document'], - opts - ) + await task.parallel(['pages_app', 'pages_error', 'pages_document'], opts) } export async function telemetry(task, opts) { diff --git a/packages/react-dev-overlay/src/client.ts b/packages/react-dev-overlay/src/client.ts index 8a4a83be0b500..abb495d076c8b 100644 --- a/packages/react-dev-overlay/src/client.ts +++ b/packages/react-dev-overlay/src/client.ts @@ -11,6 +11,12 @@ function onUnhandledError(ev: ErrorEvent) { return } + if ( + error.message.match(/(hydration|content does not match|did not match)/i) + ) { + error.message += `\n\nSee more info here: https://nextjs.org/docs/messages/react-hydration-error` + } + const e = error Bus.emit({ type: Bus.TYPE_UNHANDLED_ERROR, diff --git a/test/development/basic/hmr.test.ts b/test/development/basic/hmr.test.ts index bb54ce9f5b62b..a4f3f177e3f5a 100644 --- a/test/development/basic/hmr.test.ts +++ b/test/development/basic/hmr.test.ts @@ -26,6 +26,18 @@ describe('basic HMR', () => { }) afterAll(() => next.destroy()) + it('should show hydration error correctly', async () => { + const browser = await webdriver(next.url, '/hydration-error') + await check(async () => { + const logs = await browser.log() + return logs.some((log) => + log.message.includes('messages/react-hydration-error') + ) + ? 'success' + : JSON.stringify(logs, null, 2) + }, 'success') + }) + it('should have correct router.isReady for auto-export page', async () => { let browser = await webdriver(next.url, '/auto-export-is-ready') diff --git a/test/development/basic/hmr/pages/hydration-error.js b/test/development/basic/hmr/pages/hydration-error.js new file mode 100644 index 0000000000000..46ffdde96a2fb --- /dev/null +++ b/test/development/basic/hmr/pages/hydration-error.js @@ -0,0 +1,3 @@ +export default function Page(props) { + return

is server {typeof window}

+} diff --git a/test/integration/cli/test/index.test.js b/test/integration/cli/test/index.test.js index de5951fbcfef3..0b44cc527a1c2 100644 --- a/test/integration/cli/test/index.test.js +++ b/test/integration/cli/test/index.test.js @@ -491,12 +491,8 @@ describe('CLI Usage', () => { stdout: true, stderr: true, }) + expect((info.stderr || '').toLowerCase()).not.toContain('error') - // when a stable release is done the non-latest canary - // warning will show so skip this check for the stable release - if (pkg.version.includes('-canary')) { - expect(info.stderr || '').toBe('') - } expect(info.stdout).toMatch( new RegExp(` Operating System: diff --git a/test/integration/pnpm-support/app-multi-page/.babelrc b/test/integration/pnpm-support/app-multi-page/.babelrc deleted file mode 100644 index 1ff94f7ed28e1..0000000000000 --- a/test/integration/pnpm-support/app-multi-page/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["next/babel"] -} diff --git a/test/integration/pnpm-support/app-multi-page/package.json b/test/integration/pnpm-support/app-multi-page/package.json deleted file mode 100644 index ae87c0708d191..0000000000000 --- a/test/integration/pnpm-support/app-multi-page/package.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "pnpm-app-multi-page", - "version": "1.0.0", - "private": true, - "scripts": { - "dev": "next dev", - "build": "next build", - "start": "next start" - }, - "dependencies": { - "react": "^17.0.2", - "react-dom": "^17.0.2" - } -} diff --git a/test/integration/pnpm-support/app/.babelrc b/test/integration/pnpm-support/app/.babelrc deleted file mode 100644 index 1ff94f7ed28e1..0000000000000 --- a/test/integration/pnpm-support/app/.babelrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "presets": ["next/babel"] -} diff --git a/test/integration/pnpm-support/app/package.json b/test/integration/pnpm-support/app/package.json deleted file mode 100644 index b39d0e9d6bc63..0000000000000 --- a/test/integration/pnpm-support/app/package.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "pnpm-app", - "version": "1.0.0", - "private": true, - "scripts": { - "dev": "next dev", - "build": "next build", - "start": "next start" - }, - "dependencies": { - "react": "^17.0.2", - "react-dom": "^17.0.2" - } -} diff --git a/test/integration/pnpm-support/test/index.test.js b/test/integration/pnpm-support/test/index.test.js deleted file mode 100644 index 0fb6337bfafb5..0000000000000 --- a/test/integration/pnpm-support/test/index.test.js +++ /dev/null @@ -1,255 +0,0 @@ -/* eslint-env jest */ -import execa from 'execa' -import fs from 'fs-extra' -import os from 'os' -import path from 'path' -import { findPort, killProcess, renderViaHTTP, waitFor } from 'next-test-utils' -import webdriver from 'next-webdriver' - -const packagesDir = path.join(__dirname, '..', '..', '..', '..', 'packages') - -const APP_DIRS = { - app: path.join(__dirname, '..', 'app'), - 'app-multi-page': path.join(__dirname, '..', 'app-multi-page'), -} - -// runs a command showing logs and returning the stdout -const runCommand = (cwd, cmd, args) => { - const proc = execa(cmd, [...args], { - cwd, - stdio: [process.stdin, 'pipe', process.stderr], - }) - - let stdout = '' - proc.stdout.on('data', (data) => { - const s = data.toString() - process.stdout.write(s) - stdout += s - }) - return new Promise((resolve, reject) => { - proc.on('exit', (code) => { - if (code === 0) { - return resolve({ ...proc, stdout }) - } - reject( - new Error(`Command ${cmd} ${args.join(' ')} failed with code ${code}`) - ) - }) - }) -} - -const runNpm = (cwd, ...args) => execa('npm', [...args], { cwd }) -const runPnpm = (cwd, ...args) => runCommand(cwd, 'npx', ['pnpm', ...args]) - -async function usingTempDir(fn) { - const folder = path.join(os.tmpdir(), Math.random().toString(36).substring(2)) - await fs.mkdirp(folder) - try { - return await fn(folder) - } finally { - await fs.remove(folder) - } -} - -/** - * Using 'npm pack', create a tarball of the given package in - * directory `pkg` and write it to `cwd`. - * - * `pkg` is relative to the monorepo 'packages/' directory. - * - * Return the absolute path to the tarball. - */ -async function pack(cwd, pkg) { - const pkgDir = path.join(packagesDir, pkg) - const { stdout } = await runNpm( - cwd, - 'pack', - '--ignore-scripts', // Skip the prepublish script - path.join(packagesDir, pkg) - ) - const tarballFilename = stdout.match(/.*\.tgz/)[0] - - if (!tarballFilename) { - throw new Error( - `npm failed to pack "next" package tarball in directory ${pkgDir}.` - ) - } - - return path.join(cwd, tarballFilename) -} - -/** - * Create a Next.js app in a temporary directory, and install dependencies with pnpm. - * - * "next" and its monorepo dependencies are installed by `npm pack`-ing tarballs from - * 'next.js/packages/*', because installing "next" directly via - * `pnpm add path/to/next.js/packages/next` results in a symlink: - * 'app/node_modules/next' -> 'path/to/next.js/packages/next'. - * This is undesired since modules inside "next" would be resolved to the - * next.js monorepo 'node_modules' and lead to false test results; - * installing from a tarball avoids this issue. - * - * The "next" package's monorepo dependencies (e.g. "@next/env", "@next/polyfill-module") - * are not bundled with `npm pack next.js/packages/next`, - * so they need to be packed individually. - * To ensure that they are installed upon installing "next", a package.json "pnpm.overrides" - * field is used to override these dependency paths at install time. - */ -async function usingPnpmCreateNextApp(appDir, fn) { - await usingTempDir(async (tempDir) => { - const nextTarballPath = await pack(tempDir, 'next') - const dependencyTarballPaths = { - '@next/env': await pack(tempDir, 'next-env'), - '@next/polyfill-module': await pack(tempDir, 'next-polyfill-module'), - '@next/polyfill-nomodule': await pack(tempDir, 'next-polyfill-nomodule'), - '@next/react-dev-overlay': await pack(tempDir, 'react-dev-overlay'), - '@next/react-refresh-utils': await pack(tempDir, 'react-refresh-utils'), - } - - const tempAppDir = path.join(tempDir, 'app') - await fs.copy(appDir, tempAppDir) - - // Inject dependency tarball paths into a "pnpm.overrides" field in package.json, - // so that they are installed from packed tarballs rather than from the npm registry. - const packageJsonPath = path.join(tempAppDir, 'package.json') - const overrides = {} - for (const [dependency, tarballPath] of Object.entries( - dependencyTarballPaths - )) { - overrides[dependency] = `file:${tarballPath}` - } - const packageJsonWithOverrides = { - ...(await fs.readJson(packageJsonPath)), - pnpm: { overrides }, - } - await fs.writeFile( - packageJsonPath, - JSON.stringify(packageJsonWithOverrides, null, 2) - ) - - await runPnpm(tempAppDir, 'install') - await runPnpm(tempAppDir, 'add', `next@${nextTarballPath}`) - - await fs.copy( - path.join(__dirname, '../../../../packages/next-swc/native'), - path.join(tempAppDir, 'node_modules/@next/swc/native') - ) - - await fn(tempAppDir) - }) -} - -describe('pnpm support', () => { - it('should build with dependencies installed via pnpm', async () => { - await usingPnpmCreateNextApp(APP_DIRS['app'], async (appDir) => { - expect( - await fs.pathExists(path.join(appDir, 'pnpm-lock.yaml')) - ).toBeTruthy() - - const packageJsonPath = path.join(appDir, 'package.json') - const packageJson = await fs.readJson(packageJsonPath) - expect(packageJson.dependencies['next']).toMatch(/^file:/) - for (const dependency of [ - '@next/env', - '@next/polyfill-module', - '@next/polyfill-nomodule', - '@next/react-dev-overlay', - '@next/react-refresh-utils', - ]) { - expect(packageJson.pnpm.overrides[dependency]).toMatch(/^file:/) - } - - const { stdout } = await runPnpm(appDir, 'run', 'build') - - expect(stdout).toMatch(/Compiled successfully/) - }) - }) - - it('should execute client-side JS on each page in outputStandalone', async () => { - await usingPnpmCreateNextApp(APP_DIRS['app-multi-page'], async (appDir) => { - const { stdout } = await runPnpm(appDir, 'run', 'build') - - expect(stdout).toMatch(/Compiled successfully/) - - let appPort - let appProcess - let browser - try { - appPort = await findPort() - const standaloneDir = path.resolve(appDir, '.next/standalone/app') - - // simulate what happens in a Dockerfile - await fs.remove(path.join(appDir, 'node_modules')) - await fs.copy( - path.resolve(appDir, './.next/static'), - path.resolve(standaloneDir, './.next/static'), - { overwrite: true } - ) - appProcess = execa('node', ['server.js'], { - cwd: standaloneDir, - env: { - PORT: appPort, - }, - stdio: 'inherit', - }) - - await waitFor(1000) - - await renderViaHTTP(appPort, '/') - - browser = await webdriver(appPort, '/', { - waitHydration: false, - }) - expect(await browser.waitForElementByCss('#world').text()).toBe('World') - await browser.close() - - browser = await webdriver(appPort, '/about', { - waitHydration: false, - }) - expect(await browser.waitForElementByCss('#world').text()).toBe('World') - await browser.close() - } finally { - await killProcess(appProcess.pid) - await waitFor(5000) - } - }) - }) - - it('should execute client-side JS on each page', async () => { - await usingPnpmCreateNextApp(APP_DIRS['app-multi-page'], async (appDir) => { - const { stdout } = await runPnpm(appDir, 'run', 'build') - - expect(stdout).toMatch(/Compiled successfully/) - - let appPort - let appProcess - let browser - try { - appPort = await findPort() - appProcess = execa('pnpm', ['run', 'start', '--', '--port', appPort], { - cwd: appDir, - stdio: 'inherit', - }) - - await waitFor(5000) - - await renderViaHTTP(appPort, '/') - - browser = await webdriver(appPort, '/', { - waitHydration: false, - }) - expect(await browser.waitForElementByCss('#world').text()).toBe('World') - await browser.close() - - browser = await webdriver(appPort, '/about', { - waitHydration: false, - }) - expect(await browser.waitForElementByCss('#world').text()).toBe('World') - await browser.close() - } finally { - await killProcess(appProcess.pid) - await waitFor(5000) - } - }) - }) -}) diff --git a/test/integration/react-streaming-and-server-components/switchable-runtime/pages/_app.js b/test/integration/react-streaming-and-server-components/switchable-runtime/pages/_app.js index 57b0da8210940..ec15c6af8e6f9 100644 --- a/test/integration/react-streaming-and-server-components/switchable-runtime/pages/_app.js +++ b/test/integration/react-streaming-and-server-components/switchable-runtime/pages/_app.js @@ -1,6 +1,6 @@ export default function App({ Component, pageProps }) { return ( -
+
) diff --git a/test/integration/react-streaming-and-server-components/switchable-runtime/pages/_app.server.js b/test/integration/react-streaming-and-server-components/switchable-runtime/pages/_app.server.js deleted file mode 100644 index 15ab2f9fdc716..0000000000000 --- a/test/integration/react-streaming-and-server-components/switchable-runtime/pages/_app.server.js +++ /dev/null @@ -1,8 +0,0 @@ -export default function AppServer({ children }) { - return ( -
- - {children} -
- ) -} diff --git a/test/integration/react-streaming-and-server-components/test/switchable-runtime.test.js b/test/integration/react-streaming-and-server-components/test/switchable-runtime.test.js index f038bc168a19d..0a6cbade04325 100644 --- a/test/integration/react-streaming-and-server-components/test/switchable-runtime.test.js +++ b/test/integration/react-streaming-and-server-components/test/switchable-runtime.test.js @@ -28,43 +28,26 @@ function getOccurrence(text, matcher) { function flight(context) { describe('flight response', () => { - it('should contain _app.server in flight response (node)', async () => { + it('should not contain _app.js in flight response (node)', async () => { const html = await renderViaHTTP(context.appPort, '/node-rsc') const flightResponse = await renderViaHTTP( context.appPort, '/node-rsc?__flight__=1' ) expect( - getOccurrence(html, new RegExp(`class="app-server-root"`, 'g')) + getOccurrence(html, new RegExp(`class="app-client-root"`, 'g')) ).toBe(1) expect( getOccurrence( flightResponse, - new RegExp(`"className":\\s*"app-server-root"`, 'g') + new RegExp(`"className":\\s*"app-client-root"`, 'g') ) - ).toBe(1) + ).toBe(0) }) }) - - it('should contain _app.server in flight response (edge)', async () => { - const html = await renderViaHTTP(context.appPort, '/edge-rsc') - const flightResponse = await renderViaHTTP( - context.appPort, - '/edge-rsc?__flight__=1' - ) - expect( - getOccurrence(html, new RegExp(`class="app-server-root"`, 'g')) - ).toBe(1) - expect( - getOccurrence( - flightResponse, - new RegExp(`"className":\\s*"app-server-root"`, 'g') - ) - ).toBe(1) - }) } -async function testRoute(appPort, url, { isStatic, isEdge, isRSC }) { +async function testRoute(appPort, url, { isStatic, isEdge }) { const html1 = await renderViaHTTP(appPort, url) const renderedAt1 = +html1.match(/Time: (\d+)/)[1] expect(html1).toContain(`Runtime: ${isEdge ? 'Edge' : 'Node.js'}`) @@ -80,15 +63,6 @@ async function testRoute(appPort, url, { isStatic, isEdge, isRSC }) { // Should be re-rendered. expect(renderedAt1).toBeLessThan(renderedAt2) } - // If the page is using 1 root, it won't use the other. - // e.g. server component page won't have client app root. - const rootClasses = ['app-server-root', 'app-client-root'] - const [rootClass, oppositeRootClass] = isRSC - ? [rootClasses[0], rootClasses[1]] - : [rootClasses[1], rootClasses[0]] - - expect(getOccurrence(html1, new RegExp(`class="${rootClass}"`, 'g'))).toBe(1) - expect(html1).not.toContain(`"${oppositeRootClass}"`) } describe('Switchable runtime (prod)', () => { @@ -114,7 +88,6 @@ describe('Switchable runtime (prod)', () => { await testRoute(context.appPort, '/static', { isStatic: true, isEdge: false, - isRSC: false, }) }) @@ -122,7 +95,6 @@ describe('Switchable runtime (prod)', () => { await testRoute(context.appPort, '/node', { isStatic: true, isEdge: false, - isRSC: false, }) }) @@ -130,7 +102,6 @@ describe('Switchable runtime (prod)', () => { await testRoute(context.appPort, '/node-ssr', { isStatic: false, isEdge: false, - isRSC: false, }) }) @@ -138,7 +109,6 @@ describe('Switchable runtime (prod)', () => { await testRoute(context.appPort, '/node-ssg', { isStatic: true, isEdge: false, - isRSC: false, }) }) @@ -146,7 +116,6 @@ describe('Switchable runtime (prod)', () => { await testRoute(context.appPort, '/node-rsc', { isStatic: true, isEdge: false, - isRSC: true, }) const html = await renderViaHTTP(context.appPort, '/node-rsc') @@ -157,7 +126,6 @@ describe('Switchable runtime (prod)', () => { await testRoute(context.appPort, '/node-rsc-ssr', { isStatic: false, isEdge: false, - isRSC: true, }) }) @@ -165,7 +133,6 @@ describe('Switchable runtime (prod)', () => { await testRoute(context.appPort, '/node-rsc-ssg', { isStatic: true, isEdge: false, - isRSC: true, }) }) @@ -197,7 +164,6 @@ describe('Switchable runtime (prod)', () => { await testRoute(context.appPort, '/edge', { isStatic: false, isEdge: true, - isRSC: false, }) }) @@ -205,7 +171,6 @@ describe('Switchable runtime (prod)', () => { await testRoute(context.appPort, '/edge-rsc', { isStatic: false, isEdge: true, - isRSC: true, }) }) @@ -215,7 +180,6 @@ describe('Switchable runtime (prod)', () => { ) const expectedOutputLines = splitLines(` ┌ /_app - ├ /_app.server ├ ○ /404 ├ ℇ /edge ├ ℇ /edge-rsc diff --git a/test/lib/next-webdriver.ts b/test/lib/next-webdriver.ts index e05ada504130b..dd3f7a67faae3 100644 --- a/test/lib/next-webdriver.ts +++ b/test/lib/next-webdriver.ts @@ -44,7 +44,7 @@ export const USE_SELENIUM = Boolean( /** * - * @param appPort can either be the port or the full URL + * @param appPortOrUrl can either be the port or the full URL * @param url the path/query to append when using appPort * @param options.waitHydration whether to wait for react hydration to finish * @param options.retryWaitHydration allow retrying hydration wait if reload occurs diff --git a/test/integration/pnpm-support/app-multi-page/.npmrc b/test/production/pnpm-support/app-multi-page/.npmrc similarity index 100% rename from test/integration/pnpm-support/app-multi-page/.npmrc rename to test/production/pnpm-support/app-multi-page/.npmrc diff --git a/test/integration/pnpm-support/app-multi-page/next.config.js b/test/production/pnpm-support/app-multi-page/next.config.js similarity index 100% rename from test/integration/pnpm-support/app-multi-page/next.config.js rename to test/production/pnpm-support/app-multi-page/next.config.js diff --git a/test/integration/pnpm-support/app-multi-page/pages/about.js b/test/production/pnpm-support/app-multi-page/pages/about.js similarity index 100% rename from test/integration/pnpm-support/app-multi-page/pages/about.js rename to test/production/pnpm-support/app-multi-page/pages/about.js diff --git a/test/integration/pnpm-support/app-multi-page/pages/index.js b/test/production/pnpm-support/app-multi-page/pages/index.js similarity index 100% rename from test/integration/pnpm-support/app-multi-page/pages/index.js rename to test/production/pnpm-support/app-multi-page/pages/index.js diff --git a/test/integration/pnpm-support/app/next.config.js b/test/production/pnpm-support/app/next.config.js similarity index 100% rename from test/integration/pnpm-support/app/next.config.js rename to test/production/pnpm-support/app/next.config.js diff --git a/test/integration/pnpm-support/app/pages/index.js b/test/production/pnpm-support/app/pages/index.js similarity index 100% rename from test/integration/pnpm-support/app/pages/index.js rename to test/production/pnpm-support/app/pages/index.js diff --git a/test/integration/pnpm-support/app/pages/regenerator.js b/test/production/pnpm-support/app/pages/regenerator.js similarity index 100% rename from test/integration/pnpm-support/app/pages/regenerator.js rename to test/production/pnpm-support/app/pages/regenerator.js diff --git a/test/production/pnpm-support/test/index.test.ts b/test/production/pnpm-support/test/index.test.ts new file mode 100644 index 0000000000000..f691c62ad618b --- /dev/null +++ b/test/production/pnpm-support/test/index.test.ts @@ -0,0 +1,133 @@ +/* eslint-env jest */ +import path from 'path' +import execa from 'execa' +import fs from 'fs-extra' +import webdriver from 'next-webdriver' +import { createNext, FileRef } from 'e2e-utils' +import { NextInstance } from 'test/lib/next-modes/base' +import { + findPort, + initNextServerScript, + killApp, + renderViaHTTP, +} from 'next-test-utils' + +describe('pnpm support', () => { + let next: NextInstance | undefined + + beforeAll(async () => { + try { + const version = await execa('pnpm', ['--version']) + console.warn(`using pnpm version`, version.stdout) + } catch (_) { + // install pnpm if not available + await execa('npm', ['i', '-g', 'pnpm@latest']) + } + }) + afterEach(async () => { + try { + await next?.destroy() + } catch (_) {} + }) + + it('should build with dependencies installed via pnpm', async () => { + next = await createNext({ + files: { + pages: new FileRef(path.join(__dirname, '..', 'app/pages')), + 'next.config.js': new FileRef( + path.join(__dirname, '..', 'app/next.config.js') + ), + }, + packageJson: { + scripts: { + build: 'next build', + start: 'next start', + }, + }, + installCommand: 'pnpm install', + buildCommand: 'pnpm run build', + }) + + expect(await next.readFile('pnpm-lock.yaml')).toBeTruthy() + + expect(next.cliOutput).toMatch(/Compiled successfully/) + + const html = await renderViaHTTP(next.url, '/') + expect(html).toContain('Hello World') + }) + + it('should execute client-side JS on each page in outputStandalone', async () => { + next = await createNext({ + files: { + pages: new FileRef(path.join(__dirname, '..', 'app-multi-page/pages')), + '.npmrc': new FileRef( + path.join(__dirname, '..', 'app-multi-page/.npmrc') + ), + 'next.config.js': new FileRef( + path.join(__dirname, '..', 'app-multi-page/next.config.js') + ), + }, + packageJson: { + scripts: { + dev: 'next dev', + build: 'next build', + start: 'next start', + }, + }, + buildCommand: 'pnpm run build', + installCommand: '', + }) + await next.stop() + expect(next.cliOutput).toMatch(/Compiled successfully/) + + let appPort + let server + let browser + try { + appPort = await findPort() + const standaloneDir = path.join( + next.testDir, + '.next/standalone/', + path.basename(next.testDir) + ) + + // simulate what happens in a Dockerfile + await fs.remove(path.join(next.testDir, 'node_modules')) + await fs.copy( + path.join(next.testDir, './.next/static'), + path.join(standaloneDir, './.next/static'), + { overwrite: true } + ) + server = await initNextServerScript( + path.join(standaloneDir, 'server.js'), + /Listening/, + { + ...process.env, + PORT: appPort, + }, + undefined, + { + cwd: standaloneDir, + } + ) + + await renderViaHTTP(appPort, '/') + + browser = await webdriver(appPort, '/', { + waitHydration: false, + }) + expect(await browser.waitForElementByCss('#world').text()).toBe('World') + await browser.close() + + browser = await webdriver(appPort, '/about', { + waitHydration: false, + }) + expect(await browser.waitForElementByCss('#world').text()).toBe('World') + await browser.close() + } finally { + if (server) { + await killApp(server) + } + } + }) +})