@@ -2,12 +2,12 @@ import { readFile } from 'node:fs/promises'
2
2
import { join } from 'node:path'
3
3
4
4
import { NetlifyPluginOptions } from '@netlify/build'
5
- import { expect , test , vi } from 'vitest'
5
+ import { expect , test , vi , describe , beforeEach } from 'vitest'
6
6
7
7
import { mockFileSystem } from '../../../tests/index.js'
8
8
import { PluginContext } from '../plugin-context.js'
9
9
10
- import { copyNextServerCode } from './server.js'
10
+ import { copyNextServerCode , verifyHandlerDirStructure } from './server.js'
11
11
12
12
vi . mock ( 'node:fs' , async ( ) => {
13
13
// eslint-disable-next-line @typescript-eslint/no-explicit-any, unicorn/no-await-expression-member
@@ -23,73 +23,249 @@ vi.mock('node:fs', async () => {
23
23
24
24
vi . mock ( 'node:fs/promises' , async ( ) => {
25
25
const fs = await import ( 'node:fs' )
26
- return fs . promises
26
+ return {
27
+ ...fs . promises ,
28
+ // seems like this is not exposed with unionFS (?) as we are not asserting on it,
29
+ // this is just a no-op stub for now
30
+ cp : vi . fn ( ) ,
31
+ }
27
32
} )
28
33
29
- test ( 'should not modify the required-server-files.json distDir on simple next app' , async ( ) => {
30
- const reqServerFiles = JSON . stringify ( { config : { distDir : '.next' } } )
31
- const reqServerPath = '.next/required-server-files.json'
32
- const reqServerPathStandalone = join ( '.next/standalone' , reqServerPath )
33
- const { cwd } = mockFileSystem ( {
34
- [ reqServerPath ] : reqServerFiles ,
35
- [ reqServerPathStandalone ] : reqServerFiles ,
36
- } )
37
- const ctx = new PluginContext ( { constants : { } } as NetlifyPluginOptions )
38
- await copyNextServerCode ( ctx )
39
- expect ( await readFile ( join ( cwd , reqServerPathStandalone ) , 'utf-8' ) ) . toBe ( reqServerFiles )
34
+ let mockFS : ReturnType < typeof mockFileSystem > | undefined
35
+
36
+ vi . mock ( 'fast-glob' , async ( ) => {
37
+ const { default : fastGlob } = ( await vi . importActual ( 'fast-glob' ) ) as {
38
+ default : typeof import ( 'fast-glob' )
39
+ }
40
+
41
+ const patchedGlob = async ( ...args : Parameters < ( typeof fastGlob ) [ 'glob' ] > ) => {
42
+ if ( mockFS ) {
43
+ const fs = mockFS . vol
44
+ // https://github.com/mrmlnc/fast-glob/issues/421
45
+ args [ 1 ] = {
46
+ ...args [ 1 ] ,
47
+ fs : {
48
+ lstat : fs . lstat . bind ( fs ) ,
49
+ // eslint-disable-next-line n/no-sync
50
+ lstatSync : fs . lstatSync . bind ( fs ) ,
51
+ stat : fs . stat . bind ( fs ) ,
52
+ // eslint-disable-next-line n/no-sync
53
+ statSync : fs . statSync . bind ( fs ) ,
54
+ readdir : fs . readdir . bind ( fs ) ,
55
+ // eslint-disable-next-line n/no-sync
56
+ readdirSync : fs . readdirSync . bind ( fs ) ,
57
+ } ,
58
+ }
59
+ }
60
+
61
+ return fastGlob . glob ( ...args )
62
+ }
63
+
64
+ patchedGlob . glob = patchedGlob
65
+
66
+ return {
67
+ default : patchedGlob ,
68
+ }
40
69
} )
41
70
42
- test ( 'should not modify the required-server-files.json distDir on monorepo' , async ( ) => {
43
- const reqServerFiles = JSON . stringify ( { config : { distDir : '.next' } } )
44
- const reqServerPath = 'apps/my-app/.next/required-server-files.json'
45
- const reqServerPathStandalone = join ( 'apps/my-app/.next/standalone' , reqServerPath )
46
- const { cwd } = mockFileSystem ( {
47
- [ reqServerPath ] : reqServerFiles ,
48
- [ reqServerPathStandalone ] : reqServerFiles ,
49
- } )
50
- const ctx = new PluginContext ( {
51
- constants : {
52
- PACKAGE_PATH : 'apps/my-app' ,
71
+ const mockFailBuild = vi . fn ( )
72
+ const mockPluginOptions = {
73
+ utils : {
74
+ build : {
75
+ failBuild : mockFailBuild ,
53
76
} ,
54
- } as NetlifyPluginOptions )
55
- await copyNextServerCode ( ctx )
56
- expect ( await readFile ( join ( cwd , reqServerPathStandalone ) , 'utf-8' ) ) . toBe ( reqServerFiles )
77
+ } ,
78
+ } as unknown as NetlifyPluginOptions
79
+
80
+ const fixtures = {
81
+ get simple ( ) {
82
+ const reqServerFiles = JSON . stringify ( { config : { distDir : '.next' } } )
83
+ const reqServerPath = '.next/required-server-files.json'
84
+ const reqServerPathStandalone = join ( '.next/standalone' , reqServerPath )
85
+ const buildIDPath = join ( '.netlify/functions-internal/___netlify-server-handler/.next/BUILD_ID' )
86
+ mockFS = mockFileSystem ( {
87
+ [ reqServerPath ] : reqServerFiles ,
88
+ [ reqServerPathStandalone ] : reqServerFiles ,
89
+ [ buildIDPath ] : 'build-id' ,
90
+ } )
91
+ const ctx = new PluginContext ( { ...mockPluginOptions , constants : { } } as NetlifyPluginOptions )
92
+ return { ...mockFS , reqServerFiles, reqServerPathStandalone, ctx }
93
+ } ,
94
+ get monorepo ( ) {
95
+ const reqServerFiles = JSON . stringify ( { config : { distDir : '.next' } } )
96
+ const reqServerPath = 'apps/my-app/.next/required-server-files.json'
97
+ const reqServerPathStandalone = join ( 'apps/my-app/.next/standalone' , reqServerPath )
98
+ const buildIDPath = join (
99
+ 'apps/my-app/.netlify/functions-internal/___netlify-server-handler/apps/my-app/.next/BUILD_ID' ,
100
+ )
101
+ mockFS = mockFileSystem ( {
102
+ [ reqServerPath ] : reqServerFiles ,
103
+ [ reqServerPathStandalone ] : reqServerFiles ,
104
+ [ buildIDPath ] : 'build-id' ,
105
+ } )
106
+ const ctx = new PluginContext ( {
107
+ ...mockPluginOptions ,
108
+ constants : {
109
+ PACKAGE_PATH : 'apps/my-app' ,
110
+ } ,
111
+ } as NetlifyPluginOptions )
112
+ return { ...mockFS , reqServerFiles, reqServerPathStandalone, ctx }
113
+ } ,
114
+ get nxIntegrated ( ) {
115
+ const reqServerFiles = JSON . stringify ( {
116
+ config : { distDir : '../../dist/apps/my-app/.next' } ,
117
+ } )
118
+ const reqServerPath = 'dist/apps/my-app/.next/required-server-files.json'
119
+ const reqServerPathStandalone = join ( 'dist/apps/my-app/.next/standalone' , reqServerPath )
120
+ const buildIDPath = join (
121
+ 'apps/my-app/.netlify/functions-internal/___netlify-server-handler/dist/apps/my-app/.next/BUILD_ID' ,
122
+ )
123
+ mockFS = mockFileSystem ( {
124
+ [ reqServerPath ] : reqServerFiles ,
125
+ [ reqServerPathStandalone ] : reqServerFiles ,
126
+ [ buildIDPath ] : 'build-id' ,
127
+ } )
128
+ const ctx = new PluginContext ( {
129
+ ...mockPluginOptions ,
130
+ constants : {
131
+ PACKAGE_PATH : 'apps/my-app' ,
132
+ PUBLISH_DIR : 'dist/apps/my-app/.next' ,
133
+ } ,
134
+ } as NetlifyPluginOptions )
135
+ return { ...mockFS , reqServerFiles, reqServerPathStandalone, ctx }
136
+ } ,
137
+ get monorepoMissingPackagePath ( ) {
138
+ const reqServerFiles = JSON . stringify ( { config : { distDir : '.next' } } )
139
+ const reqServerPath = 'apps/my-app/.next/required-server-files.json'
140
+ const reqServerPathStandalone = join ( 'apps/my-app/.next/standalone' , reqServerPath )
141
+ const buildIDPath = join (
142
+ '.netlify/functions-internal/___netlify-server-handler/apps/my-app/.next/BUILD_ID' ,
143
+ )
144
+ mockFS = mockFileSystem ( {
145
+ [ reqServerPath ] : reqServerFiles ,
146
+ [ reqServerPathStandalone ] : reqServerFiles ,
147
+ [ buildIDPath ] : 'build-id' ,
148
+ } )
149
+ const ctx = new PluginContext ( {
150
+ ...mockPluginOptions ,
151
+ constants : {
152
+ PUBLISH_DIR : 'apps/my-app/.next' ,
153
+ } ,
154
+ } as NetlifyPluginOptions )
155
+ return { ...mockFS , reqServerFiles, reqServerPathStandalone, ctx }
156
+ } ,
157
+ get simpleMissingBuildID ( ) {
158
+ const reqServerFiles = JSON . stringify ( { config : { distDir : '.next' } } )
159
+ const reqServerPath = 'apps/my-app/.next/required-server-files.json'
160
+ const reqServerPathStandalone = join ( 'apps/my-app/.next/standalone' , reqServerPath )
161
+ mockFS = mockFileSystem ( {
162
+ [ reqServerPath ] : reqServerFiles ,
163
+ [ reqServerPathStandalone ] : reqServerFiles ,
164
+ } )
165
+ const ctx = new PluginContext ( {
166
+ ...mockPluginOptions ,
167
+ constants : {
168
+ PACKAGE_PATH : 'apps/my-app' ,
169
+ } ,
170
+ } as NetlifyPluginOptions )
171
+ return { ...mockFS , reqServerFiles, reqServerPathStandalone, ctx }
172
+ } ,
173
+ }
174
+
175
+ beforeEach ( ( ) => {
176
+ mockFS = undefined
177
+ mockFailBuild . mockReset ( ) . mockImplementation ( ( ) => {
178
+ expect . fail ( 'failBuild should not be called' )
179
+ } )
57
180
} )
58
181
59
- test ( 'should not modify the required-server-files.json distDir on monorepo' , async ( ) => {
60
- const reqServerFiles = JSON . stringify ( { config : { distDir : '.next' } } )
61
- const reqServerPath = 'apps/my-app/.next/required-server-files.json'
62
- const reqServerPathStandalone = join ( 'apps/my-app/.next/standalone' , reqServerPath )
63
- const { cwd } = mockFileSystem ( {
64
- [ reqServerPath ] : reqServerFiles ,
65
- [ reqServerPathStandalone ] : reqServerFiles ,
182
+ describe ( 'copyNextServerCode' , ( ) => {
183
+ test ( 'should not modify the required-server-files.json distDir on simple next app' , async ( ) => {
184
+ const { cwd, ctx, reqServerPathStandalone, reqServerFiles } = fixtures . simple
185
+ await copyNextServerCode ( ctx )
186
+ expect ( await readFile ( join ( cwd , reqServerPathStandalone ) , 'utf-8' ) ) . toBe ( reqServerFiles )
187
+ } )
188
+
189
+ test ( 'should not modify the required-server-files.json distDir on monorepo' , async ( ) => {
190
+ const { cwd, ctx, reqServerPathStandalone, reqServerFiles } = fixtures . monorepo
191
+ await copyNextServerCode ( ctx )
192
+ expect ( await readFile ( join ( cwd , reqServerPathStandalone ) , 'utf-8' ) ) . toBe ( reqServerFiles )
193
+ } )
194
+
195
+ // case of nx-integrated
196
+ test ( 'should modify the required-server-files.json distDir on distDir outside of packagePath' , async ( ) => {
197
+ const { cwd, ctx, reqServerPathStandalone } = fixtures . nxIntegrated
198
+ await copyNextServerCode ( ctx )
199
+ expect ( await readFile ( join ( cwd , reqServerPathStandalone ) , 'utf-8' ) ) . toBe (
200
+ '{"config":{"distDir":".next"}}' ,
201
+ )
66
202
} )
67
- const ctx = new PluginContext ( {
68
- constants : {
69
- PACKAGE_PATH : 'apps/my-app' ,
70
- } ,
71
- } as NetlifyPluginOptions )
72
- await copyNextServerCode ( ctx )
73
- expect ( await readFile ( join ( cwd , reqServerPathStandalone ) , 'utf-8' ) ) . toBe ( reqServerFiles )
74
203
} )
75
204
76
- // case of nx-integrated
77
- test ( 'should modify the required-server-files.json distDir on distDir outside of packagePath' , async ( ) => {
78
- const reqServerFiles = JSON . stringify ( { config : { distDir : '../../dist/apps/my-app/.next' } } )
79
- const reqServerPath = 'dist/apps/my-app/.next/required-server-files.json'
80
- const reqServerPathStandalone = join ( 'dist/apps/my-app/.next/standalone' , reqServerPath )
81
- const { cwd } = mockFileSystem ( {
82
- [ reqServerPath ] : reqServerFiles ,
83
- [ reqServerPathStandalone ] : reqServerFiles ,
205
+ describe ( 'verifyHandlerDirStructure' , ( ) => {
206
+ beforeEach ( ( ) => {
207
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
208
+ mockFailBuild . mockImplementation ( ( ) => { } )
209
+ } )
210
+
211
+ test ( 'should not fail build on simple next app' , async ( ) => {
212
+ const { ctx } = fixtures . simple
213
+ await copyNextServerCode ( ctx )
214
+ await verifyHandlerDirStructure ( ctx )
215
+ expect ( mockFailBuild ) . not . toHaveBeenCalled ( )
216
+ } )
217
+
218
+ test ( 'should not fail build on monorepo' , async ( ) => {
219
+ const { ctx } = fixtures . monorepo
220
+ await copyNextServerCode ( ctx )
221
+ await verifyHandlerDirStructure ( ctx )
222
+ expect ( mockFailBuild ) . not . toHaveBeenCalled ( )
223
+ } )
224
+
225
+ // case of nx-integrated
226
+ test ( 'should not fail build on distDir outside of packagePath' , async ( ) => {
227
+ const { ctx } = fixtures . nxIntegrated
228
+ await copyNextServerCode ( ctx )
229
+ await verifyHandlerDirStructure ( ctx )
230
+ expect ( mockFailBuild ) . not . toHaveBeenCalled ( )
231
+ } )
232
+
233
+ // case of misconfigured monorepo (no PACKAGE_PATH)
234
+ test ( 'should fail build in monorepo with PACKAGE_PATH missing with helpful guidance' , async ( ) => {
235
+ const { ctx } = fixtures . monorepoMissingPackagePath
236
+ await copyNextServerCode ( ctx )
237
+ await verifyHandlerDirStructure ( ctx )
238
+
239
+ expect ( mockFailBuild ) . toBeCalledTimes ( 1 )
240
+ expect ( mockFailBuild ) . toHaveBeenCalledWith (
241
+ `Failed creating server handler. BUILD_ID file not found at expected location "${ join (
242
+ process . cwd ( ) ,
243
+ '.netlify/functions-internal/___netlify-server-handler/.next/BUILD_ID' ,
244
+ ) } ".
245
+
246
+ It looks like your site is part of monorepo and Netlify is currently not configured correctly for this case.
247
+
248
+ Current package path: <not set>
249
+ Package path candidates:
250
+ - "apps/my-app"
251
+
252
+ Refer to https://docs.netlify.com/configure-builds/monorepos/ for more information about monorepo configuration.` ,
253
+ undefined ,
254
+ )
255
+ } )
256
+
257
+ // just missing BUILD_ID
258
+ test ( 'should fail build if BUILD_ID is missing' , async ( ) => {
259
+ const { ctx } = fixtures . simpleMissingBuildID
260
+ await copyNextServerCode ( ctx )
261
+ await verifyHandlerDirStructure ( ctx )
262
+ expect ( mockFailBuild ) . toBeCalledTimes ( 1 )
263
+ expect ( mockFailBuild ) . toHaveBeenCalledWith (
264
+ `Failed creating server handler. BUILD_ID file not found at expected location "${ join (
265
+ process . cwd ( ) ,
266
+ 'apps/my-app/.netlify/functions-internal/___netlify-server-handler/apps/my-app/.next/BUILD_ID' ,
267
+ ) } ".`,
268
+ undefined ,
269
+ )
84
270
} )
85
- const ctx = new PluginContext ( {
86
- constants : {
87
- PACKAGE_PATH : 'apps/my-app' ,
88
- PUBLISH_DIR : 'dist/apps/my-app/.next' ,
89
- } ,
90
- } as NetlifyPluginOptions )
91
- await copyNextServerCode ( ctx )
92
- expect ( await readFile ( join ( cwd , reqServerPathStandalone ) , 'utf-8' ) ) . toBe (
93
- '{"config":{"distDir":".next"}}' ,
94
- )
95
271
} )
0 commit comments