Skip to content

Commit beee190

Browse files
authored
Fix repoRoot for adapters (#94478)
`repoRoot` shouldn't be `outputFileTracingRoot`. The user can configure that to arbitrary values, which shouldn't change the adapter output. The adapter now uses `NextConfigComplete.repoRoot` instead of `outputFileTracingRoot`. Closes NEXT-4942
1 parent ad97ca5 commit beee190

9 files changed

Lines changed: 162 additions & 33 deletions

File tree

packages/next/src/build/adapter/build-complete.ts

Lines changed: 43 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -469,7 +469,8 @@ export async function handleBuildComplete({
469469
distDir,
470470
pageKeys,
471471
bundler,
472-
tracingRoot,
472+
repoRoot,
473+
outputFileTracingRoot,
473474
adapterPath,
474475
appPageKeys,
475476
staticPages,
@@ -485,13 +486,18 @@ export async function handleBuildComplete({
485486
hasInstrumentationHook,
486487
functionsConfigManifest,
487488
}: {
489+
/** The folder containing the next.config.js file */
488490
dir: string
489491
appType: 'app' | 'pages' | 'hybrid'
492+
/** The .next folder */
490493
distDir: string
491494
buildId: string
492495
configOutDir: string
493496
adapterPath: string
494-
tracingRoot: string
497+
/** The repository root. The base for relative output paths. */
498+
repoRoot: string
499+
/** A normalized version of config.outputFileTracingRoot */
500+
outputFileTracingRoot: string
495501
nextVersion: string
496502
hasStatic404: boolean
497503
hasStatic500: boolean
@@ -580,7 +586,8 @@ export async function handleBuildComplete({
580586
distDir,
581587
requiredServerFiles,
582588
dir,
583-
tracingRoot,
589+
repoRoot,
590+
outputFileTracingRoot,
584591
bundler,
585592
hasInstrumentationHook,
586593
config,
@@ -595,7 +602,7 @@ export async function handleBuildComplete({
595602
const { entryHash } = await loadNFT(
596603
assets,
597604
assetsHashes,
598-
tracingRoot,
605+
repoRoot,
599606
`${entryFilePath}.nft.json`
600607
)
601608
Object.assign(
@@ -611,7 +618,7 @@ export async function handleBuildComplete({
611618
type === 'app' ? appPagesSharedNodeAssetsHashes : {}
612619
)
613620
if (entryHash) {
614-
assetsHashes[path.relative(tracingRoot, entryFilePath)] = entryHash
621+
assetsHashes[path.relative(repoRoot, entryFilePath)] = entryHash
615622
}
616623
return { assets, assetsHashes, entryHash }
617624
}
@@ -684,7 +691,7 @@ export async function handleBuildComplete({
684691
const originalPath = path.join(distDir, file)
685692
const fileOutputPath = path.relative(
686693
config.distDir,
687-
path.join(path.relative(tracingRoot, distDir), file)
694+
path.join(path.relative(repoRoot, distDir), file)
688695
)
689696
output.assets[fileOutputPath] = originalPath
690697
}
@@ -1026,7 +1033,7 @@ export async function handleBuildComplete({
10261033
await pushAsset(
10271034
existingOutput.assets,
10281035
existingOutput.assetsHashes,
1029-
path.relative(tracingRoot, pageFile),
1036+
path.relative(repoRoot, pageFile),
10301037
pageFile,
10311038
bundler
10321039
)
@@ -2112,7 +2119,7 @@ export async function handleBuildComplete({
21122119
buildId,
21132120
nextVersion,
21142121
projectDir: dir,
2115-
repoRoot: tracingRoot,
2122+
repoRoot: repoRoot,
21162123
})
21172124
} catch (err) {
21182125
Log.error(`Failed to run onBuildComplete from ${adapterMod.name}`)
@@ -2125,15 +2132,17 @@ async function getSharedNodeAssets({
21252132
dir,
21262133
bundler,
21272134
distDir,
2128-
tracingRoot,
2135+
repoRoot,
2136+
outputFileTracingRoot,
21292137
requiredServerFiles,
21302138
hasInstrumentationHook,
21312139
config,
21322140
}: {
21332141
dir: string
21342142
bundler: Bundler
21352143
distDir: string
2136-
tracingRoot: string
2144+
repoRoot: string
2145+
outputFileTracingRoot: string
21372146
requiredServerFiles: string[]
21382147
hasInstrumentationHook: boolean
21392148
config: NextConfigComplete
@@ -2167,22 +2176,22 @@ async function getSharedNodeAssets({
21672176
}
21682177

21692178
for (const dependencyPath of currentDependencies) {
2170-
const rootRelativeFilePath = path.relative(tracingRoot, dependencyPath)
2179+
const rootRelativeFilePath = path.relative(repoRoot, dependencyPath)
21712180

21722181
if (type === 'pages') {
21732182
await pushAsset(
21742183
pagesSharedNodeAssets,
21752184
pagesSharedNodeAssetsHashes,
21762185
rootRelativeFilePath,
2177-
path.join(tracingRoot, rootRelativeFilePath),
2186+
path.join(repoRoot, rootRelativeFilePath),
21782187
bundler
21792188
)
21802189
} else {
21812190
await pushAsset(
21822191
appPagesSharedNodeAssets,
21832192
appPagesSharedNodeAssetsHashes,
21842193
rootRelativeFilePath,
2185-
path.join(tracingRoot, rootRelativeFilePath),
2194+
path.join(repoRoot, rootRelativeFilePath),
21862195
bundler
21872196
)
21882197
}
@@ -2198,7 +2207,7 @@ async function getSharedNodeAssets({
21982207
await pushAsset(
21992208
sharedNodeAssets,
22002209
sharedNodeAssetsHashes,
2201-
path.relative(tracingRoot, setupNodeStubPath),
2210+
path.relative(repoRoot, setupNodeStubPath),
22022211
require.resolve('next/dist/build/adapter/setup-node-env.external'),
22032212
bundler
22042213
)
@@ -2226,7 +2235,10 @@ async function getSharedNodeAssets({
22262235
'**/next/dist/server/web/sandbox/**/*',
22272236
'**/next/dist/server/post-process.js',
22282237
]
2229-
const sharedIgnoreFn = makeIgnoreFn(tracingRoot, sharedTraceIgnores)
2238+
const sharedIgnoreFn = makeIgnoreFn(
2239+
outputFileTracingRoot,
2240+
sharedTraceIgnores
2241+
)
22302242

22312243
// These are modules that are necessary for bootstrapping node env
22322244
const necessaryNodeDependencies = [
@@ -2266,19 +2278,26 @@ async function getSharedNodeAssets({
22662278
const { fileList, esmFileList } = await nodeFileTrace(
22672279
necessaryNodeDependencies,
22682280
{
2269-
base: tracingRoot,
2281+
base: outputFileTracingRoot,
22702282
ignore: sharedIgnoreFn,
22712283
moduleSyncCatchall: true,
22722284
}
22732285
)
22742286
esmFileList.forEach((item) => fileList.add(item))
22752287

2276-
for (const rootRelativeFilePath of fileList) {
2288+
for (const tracingRootRelativeFilePath of fileList) {
2289+
// nodeFileTrace returns paths relative to `base` (outputFileTracingRoot),
2290+
// so resolve to an absolute path and re-relativize against repoRoot, which
2291+
// is the root all adapter output keys/source paths are based on.
2292+
const absoluteFilePath = path.join(
2293+
outputFileTracingRoot,
2294+
tracingRootRelativeFilePath
2295+
)
22772296
await pushAsset(
22782297
sharedNodeAssets,
22792298
sharedNodeAssetsHashes,
2280-
rootRelativeFilePath,
2281-
path.join(tracingRoot, rootRelativeFilePath),
2299+
path.relative(repoRoot, absoluteFilePath),
2300+
absoluteFilePath,
22822301
bundler
22832302
)
22842303
}
@@ -2288,12 +2307,12 @@ async function getSharedNodeAssets({
22882307
const { entryHash: instrumentationEntryHash } = await loadNFT(
22892308
sharedNodeAssets,
22902309
sharedNodeAssetsHashes,
2291-
tracingRoot,
2310+
repoRoot,
22922311
path.join(distDir, 'server', 'instrumentation.js.nft.json')
22932312
)
22942313

22952314
const fileOutputPath = path.relative(
2296-
tracingRoot,
2315+
repoRoot,
22972316
path.join(distDir, 'server', 'instrumentation.js')
22982317
)
22992318
await pushAsset(
@@ -2310,7 +2329,7 @@ async function getSharedNodeAssets({
23102329
for (const file of requiredServerFiles) {
23112330
// add to shared node assets
23122331
const filePath = path.join(dir, file)
2313-
const fileOutputPath = path.relative(tracingRoot, filePath)
2332+
const fileOutputPath = path.relative(repoRoot, filePath)
23142333
await pushAsset(
23152334
sharedNodeAssets,
23162335
sharedNodeAssetsHashes,
@@ -2350,7 +2369,7 @@ async function pushAsset(
23502369
async function loadNFT(
23512370
assets: Record<string, string>,
23522371
assetsHashes: Record<string, string>,
2353-
tracingRoot: string,
2372+
repoRoot: string,
23542373
traceFilePath: string
23552374
): Promise<{ entryHash?: string }> {
23562375
const { files, fileHashes, entryHash } = (await JSON.parse(
@@ -2366,7 +2385,7 @@ async function loadNFT(
23662385
const relativeFile = files[i]
23672386
const contentHash = fileHashes?.[i]
23682387
const tracedFilePath = path.join(traceFileDir, relativeFile)
2369-
const fileOutputPath = path.relative(tracingRoot, tracedFilePath)
2388+
const fileOutputPath = path.relative(repoRoot, tracedFilePath)
23702389
assets[fileOutputPath] = tracedFilePath
23712390
if (contentHash) {
23722391
assetsHashes[fileOutputPath] = contentHash

packages/next/src/build/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4195,7 +4195,8 @@ export default async function build(
41954195
staticPages,
41964196
serverPropsPages,
41974197
nextVersion: process.env.__NEXT_VERSION as string,
4198-
tracingRoot: outputFileTracingRoot,
4198+
repoRoot: config.repoRoot,
4199+
outputFileTracingRoot,
41994200
hasNodeMiddleware,
42004201
hasInstrumentationHook,
42014202
adapterPath,

packages/next/src/server/config-shared.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ export type NextConfigComplete = Required<Omit<NextConfig, 'configFile'>> & {
4848
// since development builds use `{distDir}/dev`. This is used to ensure that the bundler doesn't
4949
// traverse into the output directory.
5050
distDirRoot: string
51+
// The repository root, regardless of overwritten outputFileTracingRoot or turbopack.root.
52+
repoRoot: string
5153
}
5254

5355
export type I18NDomains = readonly DomainLocale[]
@@ -1960,7 +1962,7 @@ export const defaultConfig = Object.freeze({
19601962
staticPageGenerationTimeout: 60,
19611963
output: !!process.env.NEXT_PRIVATE_STANDALONE ? 'standalone' : undefined,
19621964
modularizeImports: undefined,
1963-
outputFileTracingRoot: process.env.NEXT_PRIVATE_OUTPUT_TRACE_ROOT || '',
1965+
outputFileTracingRoot: '',
19641966
allowedDevOrigins: undefined,
19651967
enablePrerenderSourceMaps: true,
19661968
cacheComponents: false,

packages/next/src/server/config.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1173,29 +1173,34 @@ function assignDefaultsAndValidate(
11731173
const tracingRoot = result?.outputFileTracingRoot
11741174
const turbopackRoot = result?.turbopack?.root
11751175

1176+
let repoRoot = process.env.NEXT_PRIVATE_OUTPUT_TRACE_ROOT
1177+
let lockFiles: string[] | undefined = undefined
1178+
if (!repoRoot) {
1179+
const rootDirResult = findRootDirAndLockFiles(dir)
1180+
repoRoot = rootDirResult.rootDir
1181+
lockFiles = rootDirResult.lockFiles
1182+
}
1183+
;(result as NextConfigComplete).repoRoot = repoRoot
1184+
11761185
// If both provided, validate they match. If not, use outputFileTracingRoot.
11771186
if (tracingRoot && turbopackRoot && tracingRoot !== turbopackRoot) {
11781187
Log.warn(
11791188
`Both \`outputFileTracingRoot\` and \`turbopack.root\` are set, but they must have the same value.\n` +
11801189
`Using \`outputFileTracingRoot\` value: ${tracingRoot}.`
11811190
)
11821191
}
1183-
11841192
let rootDir = tracingRoot || turbopackRoot
11851193
if (!rootDir) {
1186-
const { rootDir: foundRootDir, lockFiles } = findRootDirAndLockFiles(dir)
1187-
rootDir = foundRootDir
1188-
if (!silent) {
1194+
rootDir = repoRoot
1195+
if (lockFiles && !silent) {
11891196
warnDuplicatedLockFiles(lockFiles)
11901197
}
11911198
}
1192-
11931199
if (!rootDir) {
11941200
throw new Error(
11951201
'Failed to find the root directory of the project. This is a bug in Next.js.'
11961202
)
11971203
}
1198-
11991204
// Ensure both properties are set to the same value
12001205
result.outputFileTracingRoot = rootDir
12011206
dset(result, ['turbopack', 'root'], rootDir)
@@ -2120,7 +2125,7 @@ export default async function loadConfig(
21202125
{ ...clonedDefaultConfig, configFileName },
21212126
silent,
21222127
phase
2123-
) as NextConfigComplete
2128+
)
21242129

21252130
setHttpClientAndAgentOptions(completeConfig)
21262131

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import fs from 'fs'
2+
import path from 'path'
3+
import { nextTestSetup } from 'e2e-utils'
4+
import type { NextAdapter } from 'next'
5+
6+
describe('adapter-root', () => {
7+
describe.each([
8+
{ name: 'via lockfile', env: {} },
9+
// The Vercel CLI sets this to the repo root
10+
{ name: 'via NEXT_PRIVATE_OUTPUT_TRACE_ROOT', setEnvVar: true },
11+
])('$name', ({ setEnvVar }) => {
12+
const { next } = nextTestSetup({
13+
files: path.join(__dirname, 'fixture'),
14+
subDir: 'sub',
15+
skipStart: true,
16+
overrideFiles: setEnvVar
17+
? undefined
18+
: {
19+
'../package-lock.json': JSON.stringify({
20+
name: 'parent-workspace',
21+
version: '1.0.0',
22+
lockfileVersion: 3,
23+
}),
24+
},
25+
})
26+
27+
it('should correctly determine repoRoot', async () => {
28+
const expectedRepoRoot = path.dirname(next.testDir)
29+
30+
if (setEnvVar) {
31+
next.env.NEXT_PRIVATE_OUTPUT_TRACE_ROOT = expectedRepoRoot
32+
}
33+
await next.build()
34+
35+
expect(next.cliOutput).not.toContain(
36+
'We detected multiple lockfiles and selected the directory'
37+
)
38+
39+
const {
40+
outputs,
41+
repoRoot,
42+
projectDir,
43+
}: Parameters<NextAdapter['onBuildComplete']>[0] = await next.readJSON(
44+
'build-complete.json'
45+
)
46+
47+
expect(projectDir).toBe(next.testDir)
48+
expect(repoRoot).toBe(expectedRepoRoot)
49+
50+
const combinedRouteOutputs = [
51+
...outputs.appPages,
52+
...outputs.appRoutes,
53+
...outputs.pages,
54+
...outputs.pagesApi,
55+
]
56+
for (const output of combinedRouteOutputs) {
57+
for (const [asset, source] of Object.entries(output.assets)) {
58+
expect(asset).toStartWith('sub/')
59+
expect(fs.existsSync(source)).toBeTrue()
60+
}
61+
}
62+
})
63+
})
64+
})
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export default function Layout({ children }) {
2+
return (
3+
<html lang="en">
4+
<head />
5+
<body>{children}</body>
6+
</html>
7+
)
8+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export default function Page() {
2+
return (
3+
<>
4+
<p>Hello</p>
5+
</>
6+
)
7+
}

0 commit comments

Comments
 (0)