Skip to content

Commit

Permalink
fix(standalone): fixed output: "standalone" crashing build when the…
Browse files Browse the repository at this point in the history
…re is no `app/` page (#51993)

### What?
This PR fixes build crashing when `output: 'standalone'` and `experimental.appDir` is enabled but there is no app pages.

### How?
It does that by checking whether `.next/server/app` exists before copying the folder to `.next/standalone/...`

Closes #51828
Fixes #44442
Fixes #44120
  • Loading branch information
DuCanhGH committed Jul 10, 2023
1 parent 62c2c5a commit 98cc99d
Show file tree
Hide file tree
Showing 5 changed files with 84 additions and 60 deletions.
116 changes: 56 additions & 60 deletions packages/next/src/build/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { loadEnvConfig } from '@next/env'
import chalk from 'next/dist/compiled/chalk'
import crypto from 'crypto'
import { isMatch, makeRe } from 'next/dist/compiled/micromatch'
import { promises } from 'fs'
import { promises as fs, existsSync as fsExistsSync } from 'fs'
import os from 'os'
import { Worker } from '../lib/worker'
import { defaultConfig } from '../server/config-shared'
Expand Down Expand Up @@ -186,7 +186,7 @@ async function generateClientSsgManifest(
ssgPages
)};self.__SSG_MANIFEST_CB&&self.__SSG_MANIFEST_CB()`

await promises.writeFile(
await fs.writeFile(
path.join(distDir, CLIENT_STATIC_FILES_PATH, buildId, '_ssgManifest.js'),
clientSsgManifestContent
)
Expand Down Expand Up @@ -257,10 +257,7 @@ export default async function build(
let buildId: string = ''

if (isGenerate) {
buildId = await promises.readFile(
path.join(distDir, 'BUILD_ID'),
'utf8'
)
buildId = await fs.readFile(path.join(distDir, 'BUILD_ID'), 'utf8')
} else {
buildId = await nextBuildSpan
.traceChild('generate-buildid')
Expand Down Expand Up @@ -808,7 +805,7 @@ export default async function build(
.traceChild('create-dist-dir')
.traceAsyncFn(async () => {
try {
await promises.mkdir(distDir, { recursive: true })
await fs.mkdir(distDir, { recursive: true })
return true
} catch (err) {
if (isError(err) && err.code === 'EPERM') {
Expand All @@ -830,7 +827,7 @@ export default async function build(

// Ensure commonjs handling is used for files in the distDir (generally .next)
// Files outside of the distDir can be "type": "module"
await promises.writeFile(
await fs.writeFile(
path.join(distDir, 'package.json'),
'{"type": "commonjs"}'
)
Expand All @@ -839,7 +836,7 @@ export default async function build(
await nextBuildSpan
.traceChild('write-routes-manifest')
.traceAsyncFn(() =>
promises.writeFile(
fs.writeFile(
routesManifestPath,
JSON.stringify(routesManifest),
'utf8'
Expand Down Expand Up @@ -917,7 +914,6 @@ export default async function build(
? path.join(SERVER_DIRECTORY, FONT_MANIFEST)
: null,
BUILD_ID_FILE,
appDir ? path.join(SERVER_DIRECTORY, APP_PATHS_MANIFEST) : null,
path.join(SERVER_DIRECTORY, NEXT_FONT_MANIFEST + '.js'),
path.join(SERVER_DIRECTORY, NEXT_FONT_MANIFEST + '.json'),
...(hasInstrumentationHook
Expand Down Expand Up @@ -1052,7 +1048,7 @@ export default async function build(
})
await binding.turbo.startTrace(action, turboTasksForTrace)
if (turbotraceOutputPath && turbotraceFiles) {
const existedNftFile = await promises
const existedNftFile = await fs
.readFile(turbotraceOutputPath, 'utf8')
.then((existedContent) => JSON.parse(existedContent))
.catch(() => ({
Expand All @@ -1062,7 +1058,7 @@ export default async function build(
existedNftFile.files.push(...turbotraceFiles)
const filesSet = new Set(existedNftFile.files)
existedNftFile.files = [...filesSet]
await promises.writeFile(
await fs.writeFile(
turbotraceOutputPath,
JSON.stringify(existedNftFile),
'utf8'
Expand Down Expand Up @@ -1105,14 +1101,14 @@ export default async function build(
const appDefaultConfigs = new Map<string, AppConfig>()
const pageInfos = new Map<string, PageInfo>()
const pagesManifest = JSON.parse(
await promises.readFile(manifestPath, 'utf8')
await fs.readFile(manifestPath, 'utf8')
) as PagesManifest
const buildManifest = JSON.parse(
await promises.readFile(buildManifestPath, 'utf8')
await fs.readFile(buildManifestPath, 'utf8')
) as BuildManifest
const appBuildManifest = appDir
? (JSON.parse(
await promises.readFile(appBuildManifestPath, 'utf8')
await fs.readFile(appBuildManifestPath, 'utf8')
) as AppBuildManifest)
: undefined

Expand All @@ -1127,7 +1123,7 @@ export default async function build(

if (appDir) {
appPathsManifest = JSON.parse(
await promises.readFile(
await fs.readFile(
path.join(distDir, SERVER_DIRECTORY, APP_PATHS_MANIFEST),
'utf8'
)
Expand All @@ -1136,7 +1132,7 @@ export default async function build(
Object.keys(appPathsManifest).forEach((entry) => {
appPathRoutes[entry] = normalizeAppPath(entry)
})
await promises.writeFile(
await fs.writeFile(
path.join(distDir, APP_PATH_ROUTES_MANIFEST),
JSON.stringify(appPathRoutes, null, 2)
)
Expand Down Expand Up @@ -1739,7 +1735,7 @@ export default async function build(
}

if (Object.keys(functionsConfigManifest).length > 0) {
await promises.writeFile(
await fs.writeFile(
path.join(distDir, SERVER_DIRECTORY, FUNCTIONS_CONFIG_MANIFEST),
JSON.stringify(functionsConfigManifest, null, 2)
)
Expand Down Expand Up @@ -1833,7 +1829,7 @@ export default async function build(
)
const pageDir = path.dirname(traceFile)
const traceContent = JSON.parse(
await promises.readFile(traceFile, 'utf8')
await fs.readFile(traceFile, 'utf8')
)
const includes: string[] = []

Expand Down Expand Up @@ -1866,7 +1862,7 @@ export default async function build(
})
}

await promises.writeFile(
await fs.writeFile(
traceFile,
JSON.stringify({
version: traceContent.version,
Expand Down Expand Up @@ -1913,21 +1909,18 @@ export default async function build(

await Promise.all(
lockFiles.map(async (lockFile) => {
cacheHash.update(await promises.readFile(lockFile))
cacheHash.update(await fs.readFile(lockFile))
})
)
cacheKey = cacheHash.digest('hex')

try {
const existingTrace = JSON.parse(
await promises.readFile(cachedTracePath, 'utf8')
await fs.readFile(cachedTracePath, 'utf8')
)

if (existingTrace.cacheKey === cacheKey) {
await promises.copyFile(
cachedTracePath,
nextServerTraceOutput
)
await fs.copyFile(cachedTracePath, nextServerTraceOutput)
return
}
} catch (_) {}
Expand Down Expand Up @@ -2060,7 +2053,7 @@ export default async function build(
addToTracedFiles(root, file)
})
}
await promises.writeFile(
await fs.writeFile(
nextServerTraceOutput,
JSON.stringify({
version: 1,
Expand All @@ -2071,8 +2064,8 @@ export default async function build(
files: string[]
})
)
await promises.unlink(cachedTracePath).catch(() => {})
await promises
await fs.unlink(cachedTracePath).catch(() => {})
await fs
.copyFile(nextServerTraceOutput, cachedTracePath)
.catch(() => {})
})
Expand Down Expand Up @@ -2130,7 +2123,7 @@ export default async function build(
}
})

await promises.writeFile(
await fs.writeFile(
routesManifestPath,
JSON.stringify(routesManifest),
'utf8'
Expand Down Expand Up @@ -2206,14 +2199,14 @@ export default async function build(
})
)

await promises.writeFile(
await fs.writeFile(
path.join(distDir, SERVER_FILES_MANIFEST),
JSON.stringify(requiredServerFiles),
'utf8'
)

const middlewareManifest: MiddlewareManifest = JSON.parse(
await promises.readFile(
await fs.readFile(
path.join(distDir, SERVER_DIRECTORY, MIDDLEWARE_MANIFEST),
'utf8'
)
Expand Down Expand Up @@ -2440,7 +2433,7 @@ export default async function build(
// remove server bundles that were exported
for (const page of staticPages) {
const serverBundle = getPagePath(page, distDir, undefined, false)
await promises.unlink(serverBundle)
await fs.unlink(serverBundle)
}

for (const [originalAppPath, routes] of appStaticPaths) {
Expand Down Expand Up @@ -2623,8 +2616,8 @@ export default async function build(
// output with the locale prefixed so don't attempt moving
// without the prefix
if ((!i18n || additionalSsgFile) && !isNotFound) {
await promises.mkdir(path.dirname(dest), { recursive: true })
await promises.rename(orig, dest)
await fs.mkdir(path.dirname(dest), { recursive: true })
await fs.rename(orig, dest)
} else if (i18n && !isSsg) {
// this will be updated with the locale prefixed variant
// since all files are output with the locale prefix
Expand Down Expand Up @@ -2669,10 +2662,10 @@ export default async function build(
if (!isSsg) {
pagesManifest[curPath] = updatedRelativeDest
}
await promises.mkdir(path.dirname(updatedDest), {
await fs.mkdir(path.dirname(updatedDest), {
recursive: true,
})
await promises.rename(updatedOrig, updatedDest)
await fs.rename(updatedOrig, updatedDest)
}
}
})
Expand All @@ -2693,7 +2686,7 @@ export default async function build(
.replace(/\\/g, '/')

if (await fileExists(orig)) {
await promises.copyFile(
await fs.copyFile(
orig,
path.join(distDir, 'server', updatedRelativeDest)
)
Expand Down Expand Up @@ -2861,8 +2854,8 @@ export default async function build(

// remove temporary export folder
await recursiveDelete(exportOptions.outdir)
await promises.rmdir(exportOptions.outdir)
await promises.writeFile(
await fs.rmdir(exportOptions.outdir)
await fs.writeFile(
manifestPath,
JSON.stringify(pagesManifest, null, 2),
'utf8'
Expand Down Expand Up @@ -2954,12 +2947,12 @@ export default async function build(
NextBuildContext.allowedRevalidateHeaderKeys =
config.experimental.allowedRevalidateHeaderKeys

await promises.writeFile(
await fs.writeFile(
path.join(distDir, PRERENDER_MANIFEST),
JSON.stringify(prerenderManifest),
'utf8'
)
await promises.writeFile(
await fs.writeFile(
path.join(distDir, PRERENDER_MANIFEST).replace(/\.json$/, '.js'),
`self.__PRERENDER_MANIFEST=${JSON.stringify(
JSON.stringify(prerenderManifest)
Expand All @@ -2979,12 +2972,12 @@ export default async function build(
preview: previewProps,
notFoundRoutes: [],
}
await promises.writeFile(
await fs.writeFile(
path.join(distDir, PRERENDER_MANIFEST),
JSON.stringify(prerenderManifest),
'utf8'
)
await promises.writeFile(
await fs.writeFile(
path.join(distDir, PRERENDER_MANIFEST).replace(/\.json$/, '.js'),
`self.__PRERENDER_MANIFEST=${JSON.stringify(
JSON.stringify(prerenderManifest)
Expand All @@ -3006,15 +2999,15 @@ export default async function build(
pathname: makeRe(p.pathname ?? '**').source,
}))

await promises.writeFile(
await fs.writeFile(
path.join(distDir, IMAGES_MANIFEST),
JSON.stringify({
version: 1,
images,
}),
'utf8'
)
await promises.writeFile(
await fs.writeFile(
path.join(distDir, EXPORT_MARKER),
JSON.stringify({
version: 1,
Expand All @@ -3024,7 +3017,7 @@ export default async function build(
}),
'utf8'
)
await promises.unlink(path.join(distDir, EXPORT_DETAIL)).catch((err) => {
await fs.unlink(path.join(distDir, EXPORT_DETAIL)).catch((err) => {
if (err.code === 'ENOENT') {
return Promise.resolve()
}
Expand All @@ -3048,10 +3041,10 @@ export default async function build(
'standalone',
path.relative(outputFileTracingRoot, filePath)
)
await promises.mkdir(path.dirname(outputPath), {
await fs.mkdir(path.dirname(outputPath), {
recursive: true,
})
await promises.copyFile(filePath, outputPath)
await fs.copyFile(filePath, outputPath)
}
await recursiveCopy(
path.join(distDir, SERVER_DIRECTORY, 'pages'),
Expand All @@ -3065,17 +3058,20 @@ export default async function build(
{ 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 }
)
const originalServerApp = path.join(distDir, SERVER_DIRECTORY, 'app')
if (fsExistsSync(originalServerApp)) {
await recursiveCopy(
originalServerApp,
path.join(
distDir,
'standalone',
path.relative(outputFileTracingRoot, distDir),
SERVER_DIRECTORY,
'app'
),
{ overwrite: true }
)
}
}
}

Expand Down
8 changes: 8 additions & 0 deletions test/production/standalone-mode/no-app-routes/app/layout.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export default function Root({ children }) {
return (
<html>
<head></head>
<body>{children}</body>
</html>
)
}
14 changes: 14 additions & 0 deletions test/production/standalone-mode/no-app-routes/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { createNextDescribe } from 'e2e-utils'

createNextDescribe(
'standalone mode - no app routes',
{
files: __dirname,
},
({ next }) => {
it('should handle pages rendering correctly', async () => {
const browser = await next.browser('/hello')
expect(await browser.elementByCss('#index').text()).toBe('index-page')
})
}
)
3 changes: 3 additions & 0 deletions test/production/standalone-mode/no-app-routes/next.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = {
output: 'standalone',
}

0 comments on commit 98cc99d

Please sign in to comment.