Skip to content

Commit 219588e

Browse files
authored
fix: fixes the runtime inside monorepos like turborepo (#204)
* fix: fixes the runtime inside monorepos like turborepo * chore: update * chore: pr feedback
1 parent 8e9dbb5 commit 219588e

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

89 files changed

+10876
-64
lines changed

src/build/content/server.ts

+28-16
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { PluginContext } from '../plugin-context.js'
1010
* Copy App/Pages Router Javascript needed by the server handler
1111
*/
1212
export const copyNextServerCode = async (ctx: PluginContext): Promise<void> => {
13-
const srcDir = join(ctx.publishDir, 'standalone/.next')
13+
const srcDir = join(ctx.standaloneDir, '.next')
1414
const destDir = join(ctx.serverHandlerDir, '.next')
1515

1616
const paths = await glob(
@@ -81,27 +81,39 @@ async function recreateNodeModuleSymlinks(src: string, dest: string, org?: strin
8181
}
8282

8383
export const copyNextDependencies = async (ctx: PluginContext): Promise<void> => {
84-
const entries = await readdir(join(ctx.publishDir, 'standalone'))
84+
const entries = await readdir(ctx.standaloneDir)
85+
const promises: Promise<void>[] = entries.map(async (entry) => {
86+
// copy all except the package.json and .next folder as this is handled in a separate function
87+
// this will include the node_modules folder as well
88+
if (entry === 'package.json' || entry === '.next') {
89+
return
90+
}
91+
const src = join(ctx.standaloneDir, entry)
92+
const dest = join(ctx.serverHandlerDir, entry)
93+
await cp(src, dest, { recursive: true })
8594

86-
await Promise.all(
87-
entries.map(async (entry) => {
88-
if (entry === 'package.json' || entry === '.next') {
89-
return
90-
}
91-
const src = join(ctx.publishDir, 'standalone', entry)
92-
const dest = join(ctx.serverHandlerDir, entry)
93-
await cp(src, dest, { recursive: true })
94-
}),
95-
)
95+
if (entry === 'node_modules') {
96+
await recreateNodeModuleSymlinks(ctx.resolve('node_modules'), dest)
97+
}
98+
})
99+
100+
// inside a monorepo there is a root `node_modules` folder that contains all the dependencies
101+
const rootSrcDir = join(ctx.standaloneRootDir, 'node_modules')
102+
const rootDestDir = join(ctx.serverHandlerRootDir, 'node_modules')
96103

97104
// TODO: @Lukas Holzer test this in monorepos
98105
// use the node_modules tree from the process.cwd() and not the one from the standalone output
99106
// as the standalone node_modules are already wrongly assembled by Next.js.
100107
// see: https://github.com/vercel/next.js/issues/50072
101-
await recreateNodeModuleSymlinks(
102-
ctx.resolve('node_modules'),
103-
join(ctx.serverHandlerDir, 'node_modules'),
104-
)
108+
if (existsSync(rootSrcDir) && ctx.standaloneRootDir !== ctx.standaloneDir) {
109+
promises.push(
110+
cp(rootSrcDir, rootDestDir, { recursive: true }).then(() =>
111+
recreateNodeModuleSymlinks(ctx.resolve('node_modules'), rootDestDir),
112+
),
113+
)
114+
}
115+
116+
await Promise.all(promises)
105117
}
106118

107119
export const writeTagsManifest = async (ctx: PluginContext): Promise<void> => {

src/build/functions/edge.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ const writeHandlerFile = async (ctx: PluginContext, { matchers, name }: NextDefi
7979

8080
const copyHandlerDependencies = async (ctx: PluginContext, { name, files }: NextDefinition) => {
8181
const edgeRuntimePath = join(ctx.pluginDir, 'edge-runtime')
82-
const srcDir = ctx.resolve('.next/standalone/.next')
82+
const srcDir = join(ctx.standaloneDir, '.next')
8383
const shimPath = join(edgeRuntimePath, 'shim/index.js')
8484
const shim = await readFile(shimPath, 'utf8')
8585
const imports = `import './edge-runtime-webpack.js';`

src/build/functions/server.ts

+38-14
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,15 @@ const copyHandlerDependencies = async (ctx: PluginContext) => {
1717

1818
const writeHandlerManifest = async (ctx: PluginContext) => {
1919
await writeFile(
20-
join(ctx.serverHandlerDir, `${SERVER_HANDLER_NAME}.json`),
20+
join(ctx.serverHandlerRootDir, `${SERVER_HANDLER_NAME}.json`),
2121
JSON.stringify({
2222
config: {
2323
name: 'Next.js Server Handler',
2424
generator: `${ctx.pluginName}@${ctx.pluginVersion}`,
2525
nodeBundler: 'none',
26+
// the folders can vary in monorepos based on the folder structure of the user so we have to glob all
2627
includedFiles: ['**'],
27-
includedFilesBasePath: ctx.serverHandlerDir,
28+
includedFilesBasePath: ctx.serverHandlerRootDir,
2829
},
2930
version: 1,
3031
}),
@@ -33,21 +34,44 @@ const writeHandlerManifest = async (ctx: PluginContext) => {
3334
}
3435

3536
const writePackageMetadata = async (ctx: PluginContext) => {
36-
await writeFile(join(ctx.serverHandlerDir, 'package.json'), JSON.stringify({ type: 'module' }))
37+
await writeFile(
38+
join(ctx.serverHandlerRootDir, 'package.json'),
39+
JSON.stringify({ type: 'module' }),
40+
)
41+
}
42+
43+
/** Get's the content of the handler file that will be written to the lambda */
44+
const getHandlerFile = (ctx: PluginContext): string => {
45+
const config = `
46+
export const config = {
47+
path: '/*',
48+
preferStatic: true,
49+
}`
50+
51+
// In this case it is a monorepo and we need to change the process working directory
52+
if (ctx.packagePath.length !== 0) {
53+
return `process.chdir('${join('/var/task', ctx.packagePath)}');
54+
55+
let cachedHandler;
56+
export default async function(...args) {
57+
if (!cachedHandler) {
58+
const { default: handler } = await import('./${ctx.nextServerHandler}');
59+
cachedHandler = handler;
60+
}
61+
return cachedHandler(...args)
62+
};
63+
64+
${config}`
65+
}
66+
67+
// in non monorepo scenarios we don't have to change the process working directory
68+
return `import handler from './dist/run/handlers/server.js';
69+
export default handler;
70+
${config}`
3771
}
3872

3973
const writeHandlerFile = async (ctx: PluginContext) => {
40-
await writeFile(
41-
join(ctx.serverHandlerDir, `${SERVER_HANDLER_NAME}.js`),
42-
`
43-
import handler from './dist/run/handlers/server.js';
44-
export default handler;
45-
export const config = {
46-
path: '/*',
47-
preferStatic: true
48-
};
49-
`,
50-
)
74+
await writeFile(join(ctx.serverHandlerRootDir, `${SERVER_HANDLER_NAME}.mjs`), getHandlerFile(ctx))
5175
}
5276

5377
/**

src/build/plugin-context.ts

+30-1
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,27 @@ export class PluginContext {
7171
return resolve(this.constants.PUBLISH_DIR)
7272
}
7373

74+
/**
75+
* Relative package path in non monorepo setups this is an empty string
76+
* @example ''
77+
* @example 'apps/my-app'
78+
*/
79+
get packagePath(): string {
80+
return this.constants.PACKAGE_PATH || ''
81+
}
82+
83+
/**
84+
* Retrieves the root of the `.next/standalone` directory
85+
*/
86+
get standaloneRootDir(): string {
87+
return join(this.publishDir, 'standalone')
88+
}
89+
90+
/** Retrieves the `.next/standalone/` directory monorepo aware */
91+
get standaloneDir(): string {
92+
return join(this.standaloneRootDir, this.constants.PACKAGE_PATH || '')
93+
}
94+
7495
/**
7596
* Absolute path of the directory that is published and deployed to the Netlify CDN
7697
* Will be swapped with the publish directory
@@ -97,10 +118,18 @@ export class PluginContext {
97118
}
98119

99120
/** Absolute path of the server handler */
100-
get serverHandlerDir(): string {
121+
get serverHandlerRootDir(): string {
101122
return join(this.serverFunctionsDir, SERVER_HANDLER_NAME)
102123
}
103124

125+
get serverHandlerDir(): string {
126+
return join(this.serverHandlerRootDir, this.constants.PACKAGE_PATH || '')
127+
}
128+
129+
get nextServerHandler(): string {
130+
return join(this.constants.PACKAGE_PATH || '', 'dist/run/handlers/server.js')
131+
}
132+
104133
/**
105134
* Absolute path of the directory containing the files for deno edge functions
106135
* `.netlify/edge-functions`

src/run/config.ts

+13-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1+
import { existsSync } from 'node:fs'
12
import { readFile } from 'node:fs/promises'
2-
import { resolve } from 'node:path'
3+
import { join, resolve } from 'node:path'
34

45
import type { NextConfigComplete } from 'next/dist/server/config-shared.js'
56

@@ -16,12 +17,22 @@ export const getRunConfig = async () => {
1617
* Configure the custom cache handler at request time
1718
*/
1819
export const setRunConfig = (config: NextConfigComplete) => {
20+
const cacheHandler = join(PLUGIN_DIR, 'dist/run/handlers/cache.cjs')
21+
if (!existsSync(cacheHandler)) {
22+
throw new Error(`Cache handler not found at ${cacheHandler}`)
23+
}
24+
1925
// set the path to the cache handler
2026
config.experimental = {
2127
...config.experimental,
22-
incrementalCacheHandlerPath: `${PLUGIN_DIR}/dist/run/handlers/cache.cjs`,
28+
incrementalCacheHandlerPath: cacheHandler,
2329
}
2430

31+
// Next.js 14.1.0 moved the cache handler from experimental to stable
32+
// https://github.com/vercel/next.js/pull/57953/files#diff-c49c4767e6ed8627e6e1b8f96b141ee13246153f5e9142e1da03450c8e81e96fL311
33+
config.cacheHandler = cacheHandler
34+
config.cacheMaxMemorySize = 0
35+
2536
// set config
2637
process.env.__NEXT_PRIVATE_STANDALONE_CONFIG = JSON.stringify(config)
2738
}

tests/e2e/package-manager.test.ts

+13
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,19 @@ let ctx: Awaited<ReturnType<typeof createE2EFixture>>
66
// those tests have different fixtures and can run in parallel
77
test.describe.configure({ mode: 'parallel' })
88

9+
test.afterEach(async ({ page }, testInfo) => {
10+
if (testInfo.status !== testInfo.expectedStatus) {
11+
const screenshotPath = testInfo.outputPath(`failure.png`)
12+
// Add it to the report to see the failure immediately
13+
testInfo.attachments.push({
14+
name: 'failure',
15+
path: screenshotPath,
16+
contentType: 'image/png',
17+
})
18+
await page.screenshot({ path: screenshotPath, timeout: 5000 })
19+
}
20+
})
21+
922
test.describe('[Yarn] Package manager', () => {
1023
test.describe('simple-next-app', () => {
1124
test.beforeAll(async () => {

0 commit comments

Comments
 (0)