Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit5bd9d85

Browse files
authored
fix: handle node_modules in standalone's dist dir (#3282)
1 parent71456f2 commit5bd9d85

File tree

17 files changed

+203
-58
lines changed

17 files changed

+203
-58
lines changed

‎src/build/content/server.ts‎

Lines changed: 74 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
writeFile,
1111
}from'node:fs/promises'
1212
import{createRequire}from'node:module'
13-
import{dirname,join,resolve,sep}from'node:path'
13+
import{dirname,join,relative,sep}from'node:path'
1414
import{joinasposixJoin,sepasposixSep}from'node:path/posix'
1515

1616
import{trace}from'@opentelemetry/api'
@@ -116,36 +116,53 @@ export const copyNextServerCode = async (ctx: PluginContext): Promise<void> => {
116116
},
117117
)
118118

119-
awaitPromise.all(
120-
paths.map(async(path:string)=>{
121-
constsrcPath=join(srcDir,path)
122-
constdestPath=join(destDir,path)
123-
124-
// If this is the middleware manifest file, replace it with an empty
125-
// manifest to avoid running middleware again in the server handler.
126-
if(path==='server/middleware-manifest.json'){
127-
try{
128-
awaitreplaceMiddlewareManifest(srcPath,destPath)
129-
}catch(error){
130-
thrownewError('Could not patch middleware manifest file',{cause:error})
131-
}
119+
constpromises=paths.map(async(path:string)=>{
120+
constsrcPath=join(srcDir,path)
121+
constdestPath=join(destDir,path)
132122

133-
return
123+
// If this is the middleware manifest file, replace it with an empty
124+
// manifest to avoid running middleware again in the server handler.
125+
if(path==='server/middleware-manifest.json'){
126+
try{
127+
awaitreplaceMiddlewareManifest(srcPath,destPath)
128+
}catch(error){
129+
thrownewError('Could not patch middleware manifest file',{cause:error})
134130
}
135131

136-
if(path==='server/functions-config-manifest.json'){
137-
try{
138-
awaitreplaceFunctionsConfigManifest(srcPath,destPath)
139-
}catch(error){
140-
thrownewError('Could not patch functions config manifest file',{cause:error})
141-
}
132+
return
133+
}
142134

143-
return
135+
if(path==='server/functions-config-manifest.json'){
136+
try{
137+
awaitreplaceFunctionsConfigManifest(srcPath,destPath)
138+
}catch(error){
139+
thrownewError('Could not patch functions config manifest file',{cause:error})
144140
}
145141

146-
awaitcp(srcPath,destPath,{recursive:true,force:true})
147-
}),
148-
)
142+
return
143+
}
144+
145+
awaitcp(srcPath,destPath,{recursive:true,force:true})
146+
})
147+
148+
// this is different node_modules than ones handled by `copyNextDependencies`
149+
// this is under the standalone/.next folder (not standalone/node_modules or standalone/<some-workspace/node_modules)
150+
// and started to be created by Next.js in some cases in next@16.1.0-canary.3
151+
// this node_modules is artificially created and doesn't have equivalent in the repo
152+
// so we only copy it, without additional symlinks handling
153+
if(existsSync(join(srcDir,'node_modules'))){
154+
constfilter=ctx.constants.IS_LOCAL ?undefined :nodeModulesFilter
155+
constsrc=join(srcDir,'node_modules')
156+
constdest=join(destDir,'node_modules')
157+
awaitcp(src,dest,{
158+
recursive:true,
159+
verbatimSymlinks:true,
160+
force:true,
161+
filter,
162+
})
163+
}
164+
165+
awaitPromise.all(promises)
149166
})
150167
}
151168

@@ -290,42 +307,41 @@ async function patchNextModules(
290307

291308
exportconstcopyNextDependencies=async(ctx:PluginContext):Promise<void>=>{
292309
awaittracer.withActiveSpan('copyNextDependencies',async()=>{
293-
constentries=awaitreaddir(ctx.standaloneDir)
294-
constfilter=ctx.constants.IS_LOCAL ?undefined :nodeModulesFilter
310+
constpromises:Promise<void>[]=[]
311+
312+
constnodeModulesLocationsInStandalone=newSet<string>()
313+
constcommonFilter=ctx.constants.IS_LOCAL ?undefined :nodeModulesFilter
314+
315+
constdotNextDir=join(ctx.standaloneDir,ctx.nextDistDir)
316+
317+
awaitcp(ctx.standaloneRootDir,ctx.serverHandlerRootDir,{
318+
recursive:true,
319+
verbatimSymlinks:true,
320+
force:true,
321+
filter:async(sourcePath:string)=>{
322+
if(sourcePath===dotNextDir){
323+
// copy all except the distDir (.next) folder as this is handled in a separate function
324+
// this will include the node_modules folder as well
325+
returnfalse
326+
}
295327

296-
constpromises:Promise<void>[]=entries.map(async(entry)=>{
297-
// copy all except the distDir (.next) folder as this is handled in a separate function
298-
// this will include the node_modules folder as well
299-
if(entry===ctx.nextDistDir){
300-
return
301-
}
302-
constsrc=join(ctx.standaloneDir,entry)
303-
constdest=join(ctx.serverHandlerDir,entry)
304-
awaitcp(src,dest,{
305-
recursive:true,
306-
verbatimSymlinks:true,
307-
force:true,
308-
filter,
309-
})
328+
if(sourcePath.endsWith('node_modules')){
329+
// keep track of node_modules as we might need to recreate symlinks
330+
// we are still copying them
331+
nodeModulesLocationsInStandalone.add(sourcePath)
332+
}
310333

311-
if(entry==='node_modules'){
312-
awaitrecreateNodeModuleSymlinks(ctx.resolveFromSiteDir('node_modules'),dest)
313-
}
334+
// finally apply common filter if defined
335+
returncommonFilter?.(sourcePath)??true
336+
},
314337
})
315338

316-
// inside a monorepo there is a root `node_modules` folder that contains all the dependencies
317-
constrootSrcDir=join(ctx.standaloneRootDir,'node_modules')
318-
constrootDestDir=join(ctx.serverHandlerRootDir,'node_modules')
319-
320-
// use the node_modules tree from the process.cwd() and not the one from the standalone output
321-
// as the standalone node_modules are already wrongly assembled by Next.js.
322-
// see: https://github.com/vercel/next.js/issues/50072
323-
if(existsSync(rootSrcDir)&&ctx.standaloneRootDir!==ctx.standaloneDir){
324-
promises.push(
325-
cp(rootSrcDir,rootDestDir,{recursive:true,verbatimSymlinks:true, filter}).then(()=>
326-
recreateNodeModuleSymlinks(resolve('node_modules'),rootDestDir),
327-
),
328-
)
339+
for(constnodeModulesLocationInStandaloneofnodeModulesLocationsInStandalone){
340+
constrelativeToRoot=relative(ctx.standaloneRootDir,nodeModulesLocationInStandalone)
341+
constlocationInProject=join(ctx.outputFileTracingRoot,relativeToRoot)
342+
constlocationInServerHandler=join(ctx.serverHandlerRootDir,relativeToRoot)
343+
344+
promises.push(recreateNodeModuleSymlinks(locationInProject,locationInServerHandler))
329345
}
330346

331347
awaitPromise.all(promises)
@@ -451,7 +467,7 @@ export const verifyHandlerDirStructure = async (ctx: PluginContext) => {
451467
// https://github.com/pnpm/pnpm/issues/9654
452468
// https://github.com/pnpm/pnpm/issues/5928
453469
// https://github.com/pnpm/pnpm/issues/7362 (persisting even though ticket is closed)
454-
constnodeModulesFilter=async(sourcePath:string)=>{
470+
constnodeModulesFilter=(sourcePath:string)=>{
455471
// Filtering rule for the following packages:
456472
// - @rspack+binding-linux-x64-musl
457473
// - @swc+core-linux-x64-musl

‎src/build/plugin-context.ts‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,17 @@ export class PluginContext {
8484
returnthis.requiredServerFiles.relativeAppDir??''
8585
}
8686

87+
/**
88+
* The root directory for output file tracing. Paths inside standalone directory preserve paths of project, relative to this directory.
89+
*/
90+
getoutputFileTracingRoot():string{
91+
return(
92+
this.requiredServerFiles.config.outputFileTracingRoot??
93+
// fallback for older Next.js versions that don't have outputFileTracingRoot in the config, but had it in config.experimental
94+
this.requiredServerFiles.config.experimental.outputFileTracingRoot
95+
)
96+
}
97+
8798
/**
8899
* The working directory inside the lambda that is used for monorepos to execute the serverless function
89100
*/

‎tests/e2e/turborepo.test.ts‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,15 @@ test.describe('[PNPM] Package manager', () => {
107107
constdate3=awaitpage.getByTestId('date-now').textContent()
108108
expect(date3).not.toBe(date2)
109109
})
110+
111+
test('transitive external dependencies are supported',async({ page, turborepo})=>{
112+
constpageResponse=awaitpage.goto(newURL('/transitive-external-deps',turborepo.url).href)
113+
114+
expect(pageResponse?.status()).toBe(200)
115+
116+
awaitexpect(page.getByTestId('dep-a-version')).toHaveText('3.10.1')
117+
awaitexpect(page.getByTestId('dep-b-version')).toHaveText('4.17.21')
118+
})
110119
})
111120

112121
test.describe('[NPM] Package manager',()=>{
@@ -228,4 +237,15 @@ test.describe('[NPM] Package manager', () => {
228237
'.env.production.local':'defined in .env.production.local',
229238
})
230239
})
240+
241+
test('transitive external dependencies are supported',async({ page, turborepoNPM})=>{
242+
constpageResponse=awaitpage.goto(
243+
newURL('/transitive-external-deps',turborepoNPM.url).href,
244+
)
245+
246+
expect(pageResponse?.status()).toBe(200)
247+
248+
awaitexpect(page.getByTestId('dep-a-version')).toHaveText('3.10.1')
249+
awaitexpect(page.getByTestId('dep-b-version')).toHaveText('4.17.21')
250+
})
231251
})

‎tests/fixtures/turborepo-npm/apps/page-router/next.config.js‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const nextConfig = {
88
},
99
transpilePackages:['@repo/ui'],
1010
outputFileTracingRoot:join(__dirname,'..','..'),
11+
serverExternalPackages:['lodash'],
1112
}
1213

1314
module.exports=nextConfig

‎tests/fixtures/turborepo-npm/apps/page-router/package.json‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
},
99
"dependencies": {
1010
"@netlify/functions":"^2.7.0",
11+
"@repo/dep-a":"*",
12+
"@repo/dep-b":"*",
1113
"@repo/ui":"*",
1214
"next":"latest",
1315
"react":"^18.2.0",
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
importdepAfrom'@repo/dep-a'
2+
importdepBfrom'@repo/dep-b'
3+
4+
exportdefaultfunctionTransitiveDeps(){
5+
return(
6+
<body>
7+
<ul>
8+
<li>
9+
dep-a uses lodash version 3.10.1 and we should see this version here:{' '}
10+
<spandata-testId="dep-a-version">{depA}</span>
11+
</li>
12+
<li>
13+
dep-b uses lodash version 4.17.21 and we should see this version here:{' '}
14+
<spandata-testId="dep-b-version">{depB}</span>
15+
</li>
16+
</ul>
17+
</body>
18+
)
19+
}
20+
21+
// just to ensure this is rendered in runtime and not prerendered
22+
exportasyncfunctiongetServerSideProps(){
23+
return{
24+
props:{},
25+
}
26+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
importlodashfrom'lodash'
2+
3+
exportdefaultlodash.VERSION
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"name":"@repo/dep-a",
3+
"version":"1.0.0",
4+
"dependencies": {
5+
"lodash":"3.10.1"
6+
}
7+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
importlodashfrom'lodash'
2+
3+
exportdefaultlodash.VERSION
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"name":"@repo/dep-b",
3+
"version":"1.0.0",
4+
"dependencies": {
5+
"lodash":"4.17.21"
6+
}
7+
}

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp