From 2eaa3ae14651779116ed2ccd5c27a4a9d1d05752 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ng=C3=B4=20=C4=90=E1=BB=A9c=20Anh?= <75556609+DuCanhGH@users.noreply.github.com> Date: Thu, 24 Nov 2022 09:45:19 +0700 Subject: [PATCH] fix appDir returning 404 in production with `"output": "standalone"` (#43268) Fixes: https://github.com/vercel/next.js/issues/42812 Fixes: https://github.com/vercel/next.js/issues/43037 ## Bug - [x] Related issues linked using `fixes #number` - [x] Integration tests added - [ ] Errors have a helpful link attached, see [`contributing.md`](https://github.com/vercel/next.js/blob/canary/contributing.md) ## Feature - [ ] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. - [ ] Related issues linked using `fixes #number` - [ ] [e2e](https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs) tests added - [ ] Documentation added - [ ] Telemetry added. In case of a feature if it's used or not. - [ ] Errors have a helpful link attached, see [`contributing.md`](https://github.com/vercel/next.js/blob/canary/contributing.md) ## Documentation / Examples - [ ] Make sure the linting passes by running `pnpm build && pnpm lint` - [ ] The "examples guidelines" are followed from [our contributing doc](https://github.com/vercel/next.js/blob/canary/contributing/examples/adding-examples.md) This PR fixes the issue in which urls from appDir will always not be found in production when built with `"output": "standalone"` by copying .next/server/app and .next/server/app-paths-manifest.json into .next/standalone/server. Co-authored-by: JJ Kasper --- packages/next/build/index.ts | 64 ++++++++++++++++++++++++----- packages/next/build/utils.ts | 10 +++++ test/e2e/app-dir/app/next.config.js | 1 + test/e2e/app-dir/index.test.ts | 45 ++++++++++++++++++++ 4 files changed, 109 insertions(+), 11 deletions(-) diff --git a/packages/next/build/index.ts b/packages/next/build/index.ts index 5ea05ac8cec216b..3d35c16653390c3 100644 --- a/packages/next/build/index.ts +++ b/packages/next/build/index.ts @@ -61,6 +61,9 @@ import { FONT_LOADER_MANIFEST, CLIENT_STATIC_FILES_RUNTIME_MAIN_APP, APP_CLIENT_INTERNALS, + SUBRESOURCE_INTEGRITY_MANIFEST, + MIDDLEWARE_BUILD_MANIFEST, + MIDDLEWARE_REACT_LOADABLE_MANIFEST, } from '../shared/lib/constants' import { getSortedRoutes, isDynamicRoute } from '../shared/lib/router/utils' import { __ApiPreviewProps } from '../server/api-utils' @@ -865,6 +868,11 @@ export default async function build( ) const manifestPath = path.join(distDir, SERVER_DIRECTORY, PAGES_MANIFEST) + const appManifestPath = path.join( + distDir, + SERVER_DIRECTORY, + APP_PATHS_MANIFEST + ) const requiredServerFiles = nextBuildSpan .traceChild('generate-required-server-files') @@ -885,8 +893,26 @@ export default async function build( BUILD_MANIFEST, PRERENDER_MANIFEST, path.join(SERVER_DIRECTORY, MIDDLEWARE_MANIFEST), + path.join(SERVER_DIRECTORY, MIDDLEWARE_BUILD_MANIFEST + '.js'), + path.join( + SERVER_DIRECTORY, + MIDDLEWARE_REACT_LOADABLE_MANIFEST + '.js' + ), ...(appDir ? [ + ...(config.experimental.sri + ? [ + path.join( + SERVER_DIRECTORY, + SUBRESOURCE_INTEGRITY_MANIFEST + '.js' + ), + path.join( + SERVER_DIRECTORY, + SUBRESOURCE_INTEGRITY_MANIFEST + '.json' + ), + ] + : []), + path.relative(distDir, appManifestPath), path.join(SERVER_DIRECTORY, FLIGHT_MANIFEST + '.js'), path.join(SERVER_DIRECTORY, FLIGHT_MANIFEST + '.json'), path.join( @@ -2046,6 +2072,7 @@ export default async function build( dir, distDir, pageKeys.pages, + pageKeys.app, outputFileTracingRoot, requiredServerFiles.config, middlewareManifest @@ -2750,17 +2777,32 @@ export default async function build( }) await promises.copyFile(filePath, outputPath) } - await recursiveCopy( - path.join(distDir, SERVER_DIRECTORY, 'pages'), - path.join( - distDir, - 'standalone', - path.relative(outputFileTracingRoot, distDir), - SERVER_DIRECTORY, - 'pages' - ), - { overwrite: true } - ) + if (pagesDir) { + await recursiveCopy( + path.join(distDir, SERVER_DIRECTORY, 'pages'), + path.join( + distDir, + 'standalone', + path.relative(outputFileTracingRoot, distDir), + SERVER_DIRECTORY, + 'pages' + ), + { overwrite: true } + ) + } + if (appDir) { + await recursiveCopy( + path.join(distDir, SERVER_DIRECTORY, 'app'), + path.join( + distDir, + 'standalone', + path.relative(outputFileTracingRoot, distDir), + SERVER_DIRECTORY, + 'app' + ), + { overwrite: true } + ) + } } staticPages.forEach((pg) => allStaticPages.add(pg)) diff --git a/packages/next/build/utils.ts b/packages/next/build/utils.ts index aea1ace28871cad..ea5c9b929d1439a 100644 --- a/packages/next/build/utils.ts +++ b/packages/next/build/utils.ts @@ -1598,6 +1598,7 @@ export async function copyTracedFiles( dir: string, distDir: string, pageKeys: ReadonlyArray, + appPageKeys: readonly string[] | undefined, tracingRoot: string, serverConfig: { [key: string]: any }, middlewareManifest: MiddlewareManifest @@ -1680,6 +1681,15 @@ export async function copyTracedFiles( Log.warn(`Failed to copy traced files for ${pageFile}`, err) }) } + if (appPageKeys) { + for (const page of appPageKeys) { + const pageFile = path.join(distDir, 'server', 'app', `${page}`, 'page.js') + const pageTraceFile = `${pageFile}.nft.json` + await handleTraceFiles(pageTraceFile).catch((err) => { + Log.warn(`Failed to copy traced files for ${pageFile}`, err) + }) + } + } await handleTraceFiles(path.join(distDir, 'next-server.js.nft.json')) const serverOutputPath = path.join( outputPath, diff --git a/test/e2e/app-dir/app/next.config.js b/test/e2e/app-dir/app/next.config.js index 81e780526f0f36a..2abdf182752e414 100644 --- a/test/e2e/app-dir/app/next.config.js +++ b/test/e2e/app-dir/app/next.config.js @@ -5,6 +5,7 @@ module.exports = { algorithm: 'sha256', }, }, + output: 'standalone', rewrites: async () => { return { afterFiles: [ diff --git a/test/e2e/app-dir/index.test.ts b/test/e2e/app-dir/index.test.ts index 21cea2b2ccf86fd..c9f0bcd1b34f05f 100644 --- a/test/e2e/app-dir/index.test.ts +++ b/test/e2e/app-dir/index.test.ts @@ -1,17 +1,22 @@ +import os from 'os' import { createNext, FileRef } from 'e2e-utils' import crypto from 'crypto' import { NextInstance } from 'test/lib/next-modes/base' import { check, fetchViaHTTP, + findPort, getRedboxHeader, hasRedbox, + initNextServerScript, + killApp, renderViaHTTP, waitFor, } from 'next-test-utils' import path from 'path' import cheerio from 'cheerio' import webdriver from 'next-webdriver' +import fs from 'fs-extra' describe('app dir', () => { const isDev = (global as any).isNextDev @@ -2554,4 +2559,44 @@ describe('app dir', () => { } runTests() + + if ((global as any).isNextStart) { + it('should work correctly with output standalone', async () => { + const tmpFolder = path.join(os.tmpdir(), 'next-standalone-' + Date.now()) + await fs.move(path.join(next.testDir, '.next/standalone'), tmpFolder) + let server + + try { + const testServer = path.join(tmpFolder, 'server.js') + const appPort = await findPort() + server = await initNextServerScript( + testServer, + /Listening on/, + { + ...process.env, + PORT: appPort, + }, + undefined, + { + cwd: tmpFolder, + } + ) + + for (const testPath of [ + '/', + '/api/hello', + '/blog/first', + '/dashboard', + '/dashboard/deployments/123', + '/catch-all/first', + ]) { + const res = await fetchViaHTTP(appPort, testPath) + expect(res.status).toBe(200) + } + } finally { + if (server) await killApp(server) + await fs.remove(tmpFolder) + } + }) + } })