Skip to content

Commit d06ca25

Browse files
authoredMar 4, 2024
feat: fail build if BUILD_ID is not found at location it will be read from in runtime (#313)
* test: add smoke test of setup creating monorepo as part of build command * test: remove duplicate test * test: lift fixtures out of test to be reused * feat: add build time verifaction for BUILD_ID location
1 parent b3aaed6 commit d06ca25

File tree

15 files changed

+486
-67
lines changed

15 files changed

+486
-67
lines changed
 

‎src/build/content/server.test.ts

+236-60
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@ import { readFile } from 'node:fs/promises'
22
import { join } from 'node:path'
33

44
import { NetlifyPluginOptions } from '@netlify/build'
5-
import { expect, test, vi } from 'vitest'
5+
import { expect, test, vi, describe, beforeEach } from 'vitest'
66

77
import { mockFileSystem } from '../../../tests/index.js'
88
import { PluginContext } from '../plugin-context.js'
99

10-
import { copyNextServerCode } from './server.js'
10+
import { copyNextServerCode, verifyHandlerDirStructure } from './server.js'
1111

1212
vi.mock('node:fs', async () => {
1313
// eslint-disable-next-line @typescript-eslint/no-explicit-any, unicorn/no-await-expression-member
@@ -23,73 +23,249 @@ vi.mock('node:fs', async () => {
2323

2424
vi.mock('node:fs/promises', async () => {
2525
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+
}
2732
})
2833

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+
}
4069
})
4170

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,
5376
},
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+
})
57180
})
58181

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+
)
66202
})
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)
74203
})
75204

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+
)
84270
})
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-
)
95271
})

‎src/build/content/server.ts

+56-3
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { cp, mkdir, readFile, readdir, readlink, symlink, writeFile } from 'node
33
import { createRequire } from 'node:module'
44
// eslint-disable-next-line no-restricted-imports
55
import { dirname, join, resolve, sep } from 'node:path'
6-
import { sep as posixSep } from 'node:path/posix'
6+
import { sep as posixSep, relative as posixRelative } from 'node:path/posix'
77

88
import glob from 'fast-glob'
99

@@ -29,7 +29,8 @@ export const copyNextServerCode = async (ctx: PluginContext): Promise<void> => {
2929
// this means the path got altered by a plugin like nx and contained ../../ parts so we have to reset it
3030
// to point to the correct lambda destination
3131
if (
32-
toPosixPath(ctx.distDir).replace(new RegExp(`^${ctx.packagePath}/?`), '') !== reqServerFiles.config.distDir
32+
toPosixPath(ctx.distDir).replace(new RegExp(`^${ctx.packagePath}/?`), '') !==
33+
reqServerFiles.config.distDir
3334
) {
3435
// set the distDir to the latest path portion of the publish dir
3536
reqServerFiles.config.distDir = ctx.nextDistDir
@@ -48,7 +49,10 @@ export const copyNextServerCode = async (ctx: PluginContext): Promise<void> => {
4849

4950
const srcDir = join(ctx.standaloneDir, ctx.nextDistDir)
5051
// if the distDir got resolved and altered use the nextDistDir instead
51-
const nextFolder = toPosixPath(ctx.distDir) === toPosixPath(ctx.buildConfig.distDir) ? ctx.distDir : ctx.nextDistDir
52+
const nextFolder =
53+
toPosixPath(ctx.distDir) === toPosixPath(ctx.buildConfig.distDir)
54+
? ctx.distDir
55+
: ctx.nextDistDir
5256
const destDir = join(ctx.serverHandlerDir, nextFolder)
5357

5458
const paths = await glob(
@@ -230,3 +234,52 @@ const replaceMiddlewareManifest = async (sourcePath: string, destPath: string) =
230234

231235
await writeFile(destPath, newData)
232236
}
237+
238+
export const verifyHandlerDirStructure = async (ctx: PluginContext) => {
239+
const runConfig = JSON.parse(await readFile(join(ctx.serverHandlerDir, RUN_CONFIG), 'utf-8'))
240+
241+
const expectedBuildIDPath = join(ctx.serverHandlerDir, runConfig.distDir, 'BUILD_ID')
242+
if (!existsSync(expectedBuildIDPath)) {
243+
let additionalGuidance = ''
244+
try {
245+
const paths = await glob('**/BUILD_ID', {
246+
cwd: ctx.serverHandlerRootDir,
247+
dot: true,
248+
absolute: true,
249+
ignore: ['**/node_modules/**'],
250+
})
251+
252+
const potentiallyNeededPackagePaths = paths
253+
.map((path) => {
254+
const relativePathToBuildID = posixRelative(
255+
toPosixPath(ctx.serverHandlerDir),
256+
toPosixPath(path),
257+
)
258+
// remove <distDir>/BUILD_ID from the path to get the potential package path
259+
const removedDistDirBuildId = relativePathToBuildID.replace(
260+
new RegExp(`/?${toPosixPath(runConfig.distDir)}/BUILD_ID$`),
261+
'',
262+
)
263+
return removedDistDirBuildId === relativePathToBuildID ? '' : removedDistDirBuildId
264+
})
265+
.filter(Boolean)
266+
267+
if (potentiallyNeededPackagePaths.length !== 0) {
268+
additionalGuidance = `\n\nIt looks like your site is part of monorepo and Netlify is currently not configured correctly for this case.\n\nCurrent package path: ${
269+
ctx.packagePath ? `"${ctx.packagePath}"` : '<not set>'
270+
}\nPackage path candidates:\n${potentiallyNeededPackagePaths
271+
.map((path) => ` - "${path}"`)
272+
.join(
273+
'\n',
274+
)}\n\nRefer to https://docs.netlify.com/configure-builds/monorepos/ for more information about monorepo configuration.`
275+
}
276+
} catch {
277+
// ignore error when generating additional guidance - we do best effort to provide the guidance, but if generating guidance fails,
278+
// we still want to show proper error message (even if it lacks the additional guidance)
279+
}
280+
281+
ctx.failBuild(
282+
`Failed creating server handler. BUILD_ID file not found at expected location "${expectedBuildIDPath}".${additionalGuidance}`,
283+
)
284+
}
285+
}

‎src/build/functions/server.ts

+9-2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
1-
import { cp, mkdir, readFile, rm, writeFile } from 'fs/promises'
1+
import { cp, mkdir, readFile, rm, writeFile } from 'node:fs/promises'
22
import { join, relative } from 'node:path'
33
import { join as posixJoin } from 'node:path/posix'
44

55
import { glob } from 'fast-glob'
66

7-
import { copyNextDependencies, copyNextServerCode, writeTagsManifest } from '../content/server.js'
7+
import {
8+
copyNextDependencies,
9+
copyNextServerCode,
10+
verifyHandlerDirStructure,
11+
writeTagsManifest,
12+
} from '../content/server.js'
813
import { PluginContext, SERVER_HANDLER_NAME } from '../plugin-context.js'
914

1015
/** Copies the runtime dist folder to the lambda */
@@ -111,4 +116,6 @@ export const createServerHandler = async (ctx: PluginContext) => {
111116
writePackageMetadata(ctx),
112117
writeHandlerFile(ctx),
113118
])
119+
120+
await verifyHandlerDirStructure(ctx)
114121
}

‎tests/smoke/deploy.test.ts

+22-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
import { expect, test, beforeAll } from 'vitest'
1+
import { expect, test } from 'vitest'
22
import { Fixture, fixtureFactories } from '../utils/create-e2e-fixture'
3-
import { execaCommand } from 'execa'
43

54
async function smokeTest(createFixture: () => Promise<Fixture>) {
65
const fixture = await createFixture()
@@ -19,3 +18,24 @@ test('yarn@3 monorepo with pnpm linker', async () => {
1918
test('npm monorepo deploying from site directory without --filter', async () => {
2019
await smokeTest(fixtureFactories.npmMonorepoEmptyBaseNoPackagePath)
2120
})
21+
22+
test(
23+
'npm monorepo creating site workspace as part of build step (no packagePath set) should not deploy',
24+
{ retry: 0 },
25+
async () => {
26+
const deployPromise = fixtureFactories.npmMonorepoSiteCreatedAtBuild()
27+
28+
await expect(deployPromise).rejects.toThrow(
29+
/Failed creating server handler. BUILD_ID file not found at expected location/,
30+
)
31+
await expect(deployPromise).rejects.toThrow(
32+
/It looks like your site is part of monorepo and Netlify is currently not configured correctly for this case/,
33+
)
34+
await expect(deployPromise).rejects.toThrow(/Current package path: <not set>/)
35+
await expect(deployPromise).rejects.toThrow(/Package path candidates/)
36+
await expect(deployPromise).rejects.toThrow(/- "apps\/site"/)
37+
await expect(deployPromise).rejects.toThrow(
38+
new RegExp('https://docs.netlify.com/configure-builds/monorepos/'),
39+
)
40+
},
41+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/** @type {import('next').NextConfig} */
2+
const nextConfig = {
3+
output: 'standalone',
4+
eslint: {
5+
ignoreDuringBuilds: true,
6+
},
7+
transpilePackages: ['@repo/ui'],
8+
}
9+
10+
module.exports = nextConfig
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"name": "@apps/site",
3+
"version": "0.1.0",
4+
"private": true,
5+
"scripts": {
6+
"build": "next build"
7+
},
8+
"dependencies": {
9+
"@packages/ui": "*",
10+
"next": "^14.0.3",
11+
"react": "^18.2.0",
12+
"react-dom": "^18.2.0"
13+
}
14+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { TestElement } from '@packages/ui/test.jsx'
2+
3+
export default function Home({ ssr }) {
4+
return (
5+
<main>
6+
<TestElement testid="smoke">SSR: {ssr ? 'yes' : 'no'}</TestElement>
7+
</main>
8+
)
9+
}
10+
11+
export const getServerSideProps = async () => {
12+
return {
13+
props: {
14+
ssr: true,
15+
},
16+
}
17+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"name": "@packages/ui",
3+
"version": "0.0.0",
4+
"private": true,
5+
"exports": {
6+
"./test.jsx": "./src/test.jsx"
7+
},
8+
"devDependencies": {
9+
"react": "^18.2.0"
10+
}
11+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
'use client'
2+
3+
export const TestElement = ({ children, testid }) => {
4+
return <div data-testid={testid}>{children}</div>
5+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[build]
2+
command = "node setup-site.mjs && npm install && npm run build -w apps/site"
3+
publish = "./apps/site/.next"
4+
5+
[[plugins]]
6+
package = "@netlify/plugin-nextjs"

‎tests/smoke/fixtures/npm-monorepo-site-created-at-build/package-lock.json

+58
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"name": "no-package-path-set",
3+
"private": true,
4+
"engines": {
5+
"node": ">=18"
6+
},
7+
"packageManager": "npm@10.2.3",
8+
"workspaces": [
9+
"apps/*",
10+
"packages/*"
11+
]
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { rm } from 'node:fs/promises'
2+
3+
console.log('running pre-test.mjs')
4+
// ensure we don't have monorepo setup before starting deploy
5+
await rm('apps', { force: true, recursive: true })
6+
await rm('packages', { force: true, recursive: true })
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { existsSync } from 'node:fs'
2+
import { cp, mkdir } from 'node:fs/promises'
3+
4+
if (existsSync('apps/site')) {
5+
throw new Error('apps/site already exists. Run "node pre-test.mjs" to reset the test environment')
6+
}
7+
8+
if (existsSync('packages/ui')) {
9+
throw new Error(
10+
'packages/ui already exists. Run "node pre-test.mjs" to reset the test environment',
11+
)
12+
}
13+
14+
await mkdir('apps', { recursive: true })
15+
await mkdir('packages', { recursive: true })
16+
await cp('mock-download/apps/', 'apps', { recursive: true })
17+
await cp('mock-download/packages/', 'packages', { recursive: true })

‎tests/utils/create-e2e-fixture.ts

+7
Original file line numberDiff line numberDiff line change
@@ -310,4 +310,11 @@ export const fixtureFactories = {
310310
smoke: true,
311311
generateNetlifyToml: false,
312312
}),
313+
npmMonorepoSiteCreatedAtBuild: () =>
314+
createE2EFixture('npm-monorepo-site-created-at-build', {
315+
buildCommand: 'npm run build',
316+
publishDirectory: 'apps/site/.next',
317+
smoke: true,
318+
generateNetlifyToml: false,
319+
}),
313320
}

0 commit comments

Comments
 (0)
Please sign in to comment.