From ab01d87ebcd7a994c8b295d1180be6d6318755c6 Mon Sep 17 00:00:00 2001 From: Michal Piechowiak Date: Thu, 27 Nov 2025 18:46:24 +0100 Subject: [PATCH 1/6] fix: handle node_modules in standalone's dist dir --- src/build/content/server.ts | 83 ++++++++++++++++++++++++++----------- 1 file changed, 59 insertions(+), 24 deletions(-) diff --git a/src/build/content/server.ts b/src/build/content/server.ts index ffa5dbbd4c..938355bb81 100644 --- a/src/build/content/server.ts +++ b/src/build/content/server.ts @@ -116,36 +116,71 @@ export const copyNextServerCode = async (ctx: PluginContext): Promise => { }, ) - await Promise.all( - paths.map(async (path: string) => { - const srcPath = join(srcDir, path) - const destPath = join(destDir, path) - - // If this is the middleware manifest file, replace it with an empty - // manifest to avoid running middleware again in the server handler. - if (path === 'server/middleware-manifest.json') { - try { - await replaceMiddlewareManifest(srcPath, destPath) - } catch (error) { - throw new Error('Could not patch middleware manifest file', { cause: error }) - } + const promises = paths.map(async (path: string) => { + const srcPath = join(srcDir, path) + const destPath = join(destDir, path) - return + // If this is the middleware manifest file, replace it with an empty + // manifest to avoid running middleware again in the server handler. + if (path === 'server/middleware-manifest.json') { + try { + await replaceMiddlewareManifest(srcPath, destPath) + } catch (error) { + throw new Error('Could not patch middleware manifest file', { cause: error }) } - if (path === 'server/functions-config-manifest.json') { - try { - await replaceFunctionsConfigManifest(srcPath, destPath) - } catch (error) { - throw new Error('Could not patch functions config manifest file', { cause: error }) - } + return + } - return + if (path === 'server/functions-config-manifest.json') { + try { + await replaceFunctionsConfigManifest(srcPath, destPath) + } catch (error) { + throw new Error('Could not patch functions config manifest file', { cause: error }) } - await cp(srcPath, destPath, { recursive: true, force: true }) - }), - ) + return + } + + await cp(srcPath, destPath, { recursive: true, force: true }) + }) + + // this is different node_modules than one handled `copyNextDependencies` + // this is under the standalone/.next folder (not standalone/node_modules) + // and started to be created by Next.js in some cases in next@16.1.0-canary.3 + if (existsSync(join(srcDir, 'node_modules'))) { + const filter = ctx.constants.IS_LOCAL ? undefined : nodeModulesFilter + const src = join(srcDir, 'node_modules') + const dest = join(destDir, 'node_modules') + await cp(src, dest, { + recursive: true, + verbatimSymlinks: true, + force: true, + filter, + }) + + const workspaceNodeModulesDir = ctx.resolveFromSiteDir('node_modules') + const rootNodeModulesDir = resolve('node_modules') + + // chain trying to fix potentially broken symlinks first using workspace node_modules if it exist + // and later root node_modules for monorepo cases + const workspacePromise = existsSync(workspaceNodeModulesDir) + ? recreateNodeModuleSymlinks(workspaceNodeModulesDir, dest) + : Promise.resolve() + + promises.push( + workspacePromise.then(() => { + if ( + rootNodeModulesDir !== workspaceNodeModulesDir && + existsSync(resolve('node_modules')) + ) { + return recreateNodeModuleSymlinks(rootNodeModulesDir, dest) + } + }), + ) + } + + await Promise.all(promises) }) } From 8d10761a974dd96b059d5b7e826e776e10035422 Mon Sep 17 00:00:00 2001 From: Michal Piechowiak Date: Fri, 28 Nov 2025 13:44:19 +0100 Subject: [PATCH 2/6] tmp: check which fixture hits recreateNodeModuleSymlinks case --- src/build/content/server.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/build/content/server.ts b/src/build/content/server.ts index 938355bb81..12692a2b45 100644 --- a/src/build/content/server.ts +++ b/src/build/content/server.ts @@ -191,6 +191,10 @@ export const copyNextServerCode = async (ctx: PluginContext): Promise => { * @returns */ async function recreateNodeModuleSymlinks(src: string, dest: string, org?: string): Promise { + if (!org) { + console.log('recreateNodeModuleSymlinks', { src, dest }) + } + const dirents = await readdir(join(src, org || ''), { withFileTypes: true }) await Promise.all( @@ -207,6 +211,8 @@ async function recreateNodeModuleSymlinks(src: string, dest: string, org?: strin // the location where the symlink points to const symlinkTarget = await readlink(join(src, org || '', dirent.name)) const symlinkDest = join(dest, org || '', symlinkTarget) + + console.log({ symlinkSrc, symlinkTarget, symlinkDest }) // only copy over symlinks that are traced through the nft bundle // and don't exist in the destination node_modules if (existsSync(symlinkDest) && !existsSync(symlinkSrc)) { @@ -214,7 +220,9 @@ async function recreateNodeModuleSymlinks(src: string, dest: string, org?: strin // if it is an organization folder let's create the folder first await mkdir(join(dest, org), { recursive: true }) } - await symlink(symlinkTarget, symlinkSrc) + + // await symlink(symlinkTarget, symlinkSrc) + throw new Error('(just for testing) hitting recreateNodeModuleSymlinks case') } } }), From b1be3180f60780f20b3734d8488953bbe84e16b0 Mon Sep 17 00:00:00 2001 From: Michal Piechowiak Date: Fri, 28 Nov 2025 14:46:44 +0100 Subject: [PATCH 3/6] Revert "tmp: check which fixture hits recreateNodeModuleSymlinks case" This reverts commit 8d10761a974dd96b059d5b7e826e776e10035422. --- src/build/content/server.ts | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/build/content/server.ts b/src/build/content/server.ts index 12692a2b45..938355bb81 100644 --- a/src/build/content/server.ts +++ b/src/build/content/server.ts @@ -191,10 +191,6 @@ export const copyNextServerCode = async (ctx: PluginContext): Promise => { * @returns */ async function recreateNodeModuleSymlinks(src: string, dest: string, org?: string): Promise { - if (!org) { - console.log('recreateNodeModuleSymlinks', { src, dest }) - } - const dirents = await readdir(join(src, org || ''), { withFileTypes: true }) await Promise.all( @@ -211,8 +207,6 @@ async function recreateNodeModuleSymlinks(src: string, dest: string, org?: strin // the location where the symlink points to const symlinkTarget = await readlink(join(src, org || '', dirent.name)) const symlinkDest = join(dest, org || '', symlinkTarget) - - console.log({ symlinkSrc, symlinkTarget, symlinkDest }) // only copy over symlinks that are traced through the nft bundle // and don't exist in the destination node_modules if (existsSync(symlinkDest) && !existsSync(symlinkSrc)) { @@ -220,9 +214,7 @@ async function recreateNodeModuleSymlinks(src: string, dest: string, org?: strin // if it is an organization folder let's create the folder first await mkdir(join(dest, org), { recursive: true }) } - - // await symlink(symlinkTarget, symlinkSrc) - throw new Error('(just for testing) hitting recreateNodeModuleSymlinks case') + await symlink(symlinkTarget, symlinkSrc) } } }), From e1d032a0630dbd9068986a42190322dc60444e86 Mon Sep 17 00:00:00 2001 From: Michal Piechowiak Date: Fri, 28 Nov 2025 14:47:21 +0100 Subject: [PATCH 4/6] tmp: skip recreateNodeModuleSymlinks in new code path for some testing --- src/build/content/server.ts | 38 ++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/src/build/content/server.ts b/src/build/content/server.ts index 938355bb81..a7cde4a3c8 100644 --- a/src/build/content/server.ts +++ b/src/build/content/server.ts @@ -159,25 +159,25 @@ export const copyNextServerCode = async (ctx: PluginContext): Promise => { filter, }) - const workspaceNodeModulesDir = ctx.resolveFromSiteDir('node_modules') - const rootNodeModulesDir = resolve('node_modules') - - // chain trying to fix potentially broken symlinks first using workspace node_modules if it exist - // and later root node_modules for monorepo cases - const workspacePromise = existsSync(workspaceNodeModulesDir) - ? recreateNodeModuleSymlinks(workspaceNodeModulesDir, dest) - : Promise.resolve() - - promises.push( - workspacePromise.then(() => { - if ( - rootNodeModulesDir !== workspaceNodeModulesDir && - existsSync(resolve('node_modules')) - ) { - return recreateNodeModuleSymlinks(rootNodeModulesDir, dest) - } - }), - ) + // const workspaceNodeModulesDir = ctx.resolveFromSiteDir('node_modules') + // const rootNodeModulesDir = resolve('node_modules') + + // // chain trying to fix potentially broken symlinks first using workspace node_modules if it exist + // // and later root node_modules for monorepo cases + // const workspacePromise = existsSync(workspaceNodeModulesDir) + // ? recreateNodeModuleSymlinks(workspaceNodeModulesDir, dest) + // : Promise.resolve() + + // promises.push( + // workspacePromise.then(() => { + // if ( + // rootNodeModulesDir !== workspaceNodeModulesDir && + // existsSync(resolve('node_modules')) + // ) { + // return recreateNodeModuleSymlinks(rootNodeModulesDir, dest) + // } + // }), + // ) } await Promise.all(promises) From 7d85c9457031bf06c5d8fbe4e835676d663ca16e Mon Sep 17 00:00:00 2001 From: Michal Piechowiak Date: Fri, 28 Nov 2025 18:24:56 +0100 Subject: [PATCH 5/6] test: add e2e test cases for external transitive deps --- tests/e2e/turborepo.test.ts | 20 ++++++++++++++ .../apps/page-router/next.config.js | 1 + .../apps/page-router/package.json | 2 ++ .../pages/transitive-external-deps.js | 26 +++++++++++++++++++ .../turborepo-npm/packages/dep-a/index.js | 3 +++ .../turborepo-npm/packages/dep-a/package.json | 7 +++++ .../turborepo-npm/packages/dep-b/index.js | 3 +++ .../turborepo-npm/packages/dep-b/package.json | 7 +++++ .../turborepo/apps/page-router/next.config.js | 1 + .../turborepo/apps/page-router/package.json | 2 ++ .../src/pages/transitive-external-deps.js | 26 +++++++++++++++++++ .../turborepo/packages/dep-a/index.js | 3 +++ .../turborepo/packages/dep-a/package.json | 7 +++++ .../turborepo/packages/dep-b/index.js | 3 +++ .../turborepo/packages/dep-b/package.json | 7 +++++ 15 files changed, 118 insertions(+) create mode 100644 tests/fixtures/turborepo-npm/apps/page-router/pages/transitive-external-deps.js create mode 100644 tests/fixtures/turborepo-npm/packages/dep-a/index.js create mode 100644 tests/fixtures/turborepo-npm/packages/dep-a/package.json create mode 100644 tests/fixtures/turborepo-npm/packages/dep-b/index.js create mode 100644 tests/fixtures/turborepo-npm/packages/dep-b/package.json create mode 100644 tests/fixtures/turborepo/apps/page-router/src/pages/transitive-external-deps.js create mode 100644 tests/fixtures/turborepo/packages/dep-a/index.js create mode 100644 tests/fixtures/turborepo/packages/dep-a/package.json create mode 100644 tests/fixtures/turborepo/packages/dep-b/index.js create mode 100644 tests/fixtures/turborepo/packages/dep-b/package.json diff --git a/tests/e2e/turborepo.test.ts b/tests/e2e/turborepo.test.ts index e362536995..7e3aad20f2 100644 --- a/tests/e2e/turborepo.test.ts +++ b/tests/e2e/turborepo.test.ts @@ -107,6 +107,15 @@ test.describe('[PNPM] Package manager', () => { const date3 = await page.getByTestId('date-now').textContent() expect(date3).not.toBe(date2) }) + + test('transitive external dependencies are supported', async ({ page, turborepo }) => { + const pageResponse = await page.goto(new URL('/transitive-external-deps', turborepo.url).href) + + expect(pageResponse?.status()).toBe(200) + + await expect(page.getByTestId('dep-a-version')).toHaveText('3.10.1') + await expect(page.getByTestId('dep-b-version')).toHaveText('4.17.21') + }) }) test.describe('[NPM] Package manager', () => { @@ -228,4 +237,15 @@ test.describe('[NPM] Package manager', () => { '.env.production.local': 'defined in .env.production.local', }) }) + + test('transitive external dependencies are supported', async ({ page, turborepoNPM }) => { + const pageResponse = await page.goto( + new URL('/transitive-external-deps', turborepoNPM.url).href, + ) + + expect(pageResponse?.status()).toBe(200) + + await expect(page.getByTestId('dep-a-version')).toHaveText('3.10.1') + await expect(page.getByTestId('dep-b-version')).toHaveText('4.17.21') + }) }) diff --git a/tests/fixtures/turborepo-npm/apps/page-router/next.config.js b/tests/fixtures/turborepo-npm/apps/page-router/next.config.js index 4d6842aa7a..da3921713d 100644 --- a/tests/fixtures/turborepo-npm/apps/page-router/next.config.js +++ b/tests/fixtures/turborepo-npm/apps/page-router/next.config.js @@ -8,6 +8,7 @@ const nextConfig = { }, transpilePackages: ['@repo/ui'], outputFileTracingRoot: join(__dirname, '..', '..'), + serverExternalPackages: ['lodash'], } module.exports = nextConfig diff --git a/tests/fixtures/turborepo-npm/apps/page-router/package.json b/tests/fixtures/turborepo-npm/apps/page-router/package.json index 306948b986..92a11caa6f 100644 --- a/tests/fixtures/turborepo-npm/apps/page-router/package.json +++ b/tests/fixtures/turborepo-npm/apps/page-router/package.json @@ -8,6 +8,8 @@ }, "dependencies": { "@netlify/functions": "^2.7.0", + "@repo/dep-a": "*", + "@repo/dep-b": "*", "@repo/ui": "*", "next": "latest", "react": "^18.2.0", diff --git a/tests/fixtures/turborepo-npm/apps/page-router/pages/transitive-external-deps.js b/tests/fixtures/turborepo-npm/apps/page-router/pages/transitive-external-deps.js new file mode 100644 index 0000000000..da1115a0ab --- /dev/null +++ b/tests/fixtures/turborepo-npm/apps/page-router/pages/transitive-external-deps.js @@ -0,0 +1,26 @@ +import depA from '@repo/dep-a' +import depB from '@repo/dep-b' + +export default function TransitiveDeps() { + return ( + +
    +
  • + dep-a uses lodash version 3.10.1 and we should see this version here:{' '} + {depA} +
  • +
  • + dep-b uses lodash version 4.17.21 and we should see this version here:{' '} + {depB} +
  • +
+ + ) +} + +// just to ensure this is rendered in runtime and not prerendered +export async function getServerSideProps() { + return { + props: {}, + } +} diff --git a/tests/fixtures/turborepo-npm/packages/dep-a/index.js b/tests/fixtures/turborepo-npm/packages/dep-a/index.js new file mode 100644 index 0000000000..6aee019fc7 --- /dev/null +++ b/tests/fixtures/turborepo-npm/packages/dep-a/index.js @@ -0,0 +1,3 @@ +import lodash from 'lodash' + +export default lodash.VERSION diff --git a/tests/fixtures/turborepo-npm/packages/dep-a/package.json b/tests/fixtures/turborepo-npm/packages/dep-a/package.json new file mode 100644 index 0000000000..665f6450f8 --- /dev/null +++ b/tests/fixtures/turborepo-npm/packages/dep-a/package.json @@ -0,0 +1,7 @@ +{ + "name": "@repo/dep-a", + "version": "1.0.0", + "dependencies": { + "lodash": "3.10.1" + } +} diff --git a/tests/fixtures/turborepo-npm/packages/dep-b/index.js b/tests/fixtures/turborepo-npm/packages/dep-b/index.js new file mode 100644 index 0000000000..6aee019fc7 --- /dev/null +++ b/tests/fixtures/turborepo-npm/packages/dep-b/index.js @@ -0,0 +1,3 @@ +import lodash from 'lodash' + +export default lodash.VERSION diff --git a/tests/fixtures/turborepo-npm/packages/dep-b/package.json b/tests/fixtures/turborepo-npm/packages/dep-b/package.json new file mode 100644 index 0000000000..071e4a5262 --- /dev/null +++ b/tests/fixtures/turborepo-npm/packages/dep-b/package.json @@ -0,0 +1,7 @@ +{ + "name": "@repo/dep-b", + "version": "1.0.0", + "dependencies": { + "lodash": "4.17.21" + } +} diff --git a/tests/fixtures/turborepo/apps/page-router/next.config.js b/tests/fixtures/turborepo/apps/page-router/next.config.js index 4d6842aa7a..da3921713d 100644 --- a/tests/fixtures/turborepo/apps/page-router/next.config.js +++ b/tests/fixtures/turborepo/apps/page-router/next.config.js @@ -8,6 +8,7 @@ const nextConfig = { }, transpilePackages: ['@repo/ui'], outputFileTracingRoot: join(__dirname, '..', '..'), + serverExternalPackages: ['lodash'], } module.exports = nextConfig diff --git a/tests/fixtures/turborepo/apps/page-router/package.json b/tests/fixtures/turborepo/apps/page-router/package.json index 474d31a1b1..1ec3a4f5bc 100644 --- a/tests/fixtures/turborepo/apps/page-router/package.json +++ b/tests/fixtures/turborepo/apps/page-router/package.json @@ -8,6 +8,8 @@ }, "dependencies": { "@netlify/functions": "^2.7.0", + "@repo/dep-a": "workspace:*", + "@repo/dep-b": "workspace:*", "@repo/ui": "workspace:*", "next": "latest", "react": "^18.2.0", diff --git a/tests/fixtures/turborepo/apps/page-router/src/pages/transitive-external-deps.js b/tests/fixtures/turborepo/apps/page-router/src/pages/transitive-external-deps.js new file mode 100644 index 0000000000..da1115a0ab --- /dev/null +++ b/tests/fixtures/turborepo/apps/page-router/src/pages/transitive-external-deps.js @@ -0,0 +1,26 @@ +import depA from '@repo/dep-a' +import depB from '@repo/dep-b' + +export default function TransitiveDeps() { + return ( + +
    +
  • + dep-a uses lodash version 3.10.1 and we should see this version here:{' '} + {depA} +
  • +
  • + dep-b uses lodash version 4.17.21 and we should see this version here:{' '} + {depB} +
  • +
+ + ) +} + +// just to ensure this is rendered in runtime and not prerendered +export async function getServerSideProps() { + return { + props: {}, + } +} diff --git a/tests/fixtures/turborepo/packages/dep-a/index.js b/tests/fixtures/turborepo/packages/dep-a/index.js new file mode 100644 index 0000000000..6aee019fc7 --- /dev/null +++ b/tests/fixtures/turborepo/packages/dep-a/index.js @@ -0,0 +1,3 @@ +import lodash from 'lodash' + +export default lodash.VERSION diff --git a/tests/fixtures/turborepo/packages/dep-a/package.json b/tests/fixtures/turborepo/packages/dep-a/package.json new file mode 100644 index 0000000000..665f6450f8 --- /dev/null +++ b/tests/fixtures/turborepo/packages/dep-a/package.json @@ -0,0 +1,7 @@ +{ + "name": "@repo/dep-a", + "version": "1.0.0", + "dependencies": { + "lodash": "3.10.1" + } +} diff --git a/tests/fixtures/turborepo/packages/dep-b/index.js b/tests/fixtures/turborepo/packages/dep-b/index.js new file mode 100644 index 0000000000..6aee019fc7 --- /dev/null +++ b/tests/fixtures/turborepo/packages/dep-b/index.js @@ -0,0 +1,3 @@ +import lodash from 'lodash' + +export default lodash.VERSION diff --git a/tests/fixtures/turborepo/packages/dep-b/package.json b/tests/fixtures/turborepo/packages/dep-b/package.json new file mode 100644 index 0000000000..071e4a5262 --- /dev/null +++ b/tests/fixtures/turborepo/packages/dep-b/package.json @@ -0,0 +1,7 @@ +{ + "name": "@repo/dep-b", + "version": "1.0.0", + "dependencies": { + "lodash": "4.17.21" + } +} From d58ed23a6ee1bdfd51a68eb6577e29b99744ffbf Mon Sep 17 00:00:00 2001 From: Michal Piechowiak Date: Fri, 28 Nov 2025 21:17:32 +0100 Subject: [PATCH 6/6] fix: ensure that we don't skip files in standalone root dir that are outside of project workspace --- src/build/content/server.ts | 93 +++++++++++++++---------------------- src/build/plugin-context.ts | 11 +++++ 2 files changed, 48 insertions(+), 56 deletions(-) diff --git a/src/build/content/server.ts b/src/build/content/server.ts index a7cde4a3c8..14ca3cc7b1 100644 --- a/src/build/content/server.ts +++ b/src/build/content/server.ts @@ -10,7 +10,7 @@ import { writeFile, } from 'node:fs/promises' import { createRequire } from 'node:module' -import { dirname, join, resolve, sep } from 'node:path' +import { dirname, join, relative, sep } from 'node:path' import { join as posixJoin, sep as posixSep } from 'node:path/posix' import { trace } from '@opentelemetry/api' @@ -145,9 +145,11 @@ export const copyNextServerCode = async (ctx: PluginContext): Promise => { await cp(srcPath, destPath, { recursive: true, force: true }) }) - // this is different node_modules than one handled `copyNextDependencies` - // this is under the standalone/.next folder (not standalone/node_modules) + // this is different node_modules than ones handled by `copyNextDependencies` + // this is under the standalone/.next folder (not standalone/node_modules or standalone/ => { force: true, filter, }) - - // const workspaceNodeModulesDir = ctx.resolveFromSiteDir('node_modules') - // const rootNodeModulesDir = resolve('node_modules') - - // // chain trying to fix potentially broken symlinks first using workspace node_modules if it exist - // // and later root node_modules for monorepo cases - // const workspacePromise = existsSync(workspaceNodeModulesDir) - // ? recreateNodeModuleSymlinks(workspaceNodeModulesDir, dest) - // : Promise.resolve() - - // promises.push( - // workspacePromise.then(() => { - // if ( - // rootNodeModulesDir !== workspaceNodeModulesDir && - // existsSync(resolve('node_modules')) - // ) { - // return recreateNodeModuleSymlinks(rootNodeModulesDir, dest) - // } - // }), - // ) } await Promise.all(promises) @@ -325,42 +307,41 @@ async function patchNextModules( export const copyNextDependencies = async (ctx: PluginContext): Promise => { await tracer.withActiveSpan('copyNextDependencies', async () => { - const entries = await readdir(ctx.standaloneDir) - const filter = ctx.constants.IS_LOCAL ? undefined : nodeModulesFilter + const promises: Promise[] = [] + + const nodeModulesLocationsInStandalone = new Set() + const commonFilter = ctx.constants.IS_LOCAL ? undefined : nodeModulesFilter + + const dotNextDir = join(ctx.standaloneDir, ctx.nextDistDir) + + await cp(ctx.standaloneRootDir, ctx.serverHandlerRootDir, { + recursive: true, + verbatimSymlinks: true, + force: true, + filter: async (sourcePath: string) => { + if (sourcePath === dotNextDir) { + // copy all except the distDir (.next) folder as this is handled in a separate function + // this will include the node_modules folder as well + return false + } - const promises: Promise[] = entries.map(async (entry) => { - // copy all except the distDir (.next) folder as this is handled in a separate function - // this will include the node_modules folder as well - if (entry === ctx.nextDistDir) { - return - } - const src = join(ctx.standaloneDir, entry) - const dest = join(ctx.serverHandlerDir, entry) - await cp(src, dest, { - recursive: true, - verbatimSymlinks: true, - force: true, - filter, - }) + if (sourcePath.endsWith('node_modules')) { + // keep track of node_modules as we might need to recreate symlinks + // we are still copying them + nodeModulesLocationsInStandalone.add(sourcePath) + } - if (entry === 'node_modules') { - await recreateNodeModuleSymlinks(ctx.resolveFromSiteDir('node_modules'), dest) - } + // finally apply common filter if defined + return commonFilter?.(sourcePath) ?? true + }, }) - // inside a monorepo there is a root `node_modules` folder that contains all the dependencies - const rootSrcDir = join(ctx.standaloneRootDir, 'node_modules') - const rootDestDir = join(ctx.serverHandlerRootDir, 'node_modules') - - // use the node_modules tree from the process.cwd() and not the one from the standalone output - // as the standalone node_modules are already wrongly assembled by Next.js. - // see: https://github.com/vercel/next.js/issues/50072 - if (existsSync(rootSrcDir) && ctx.standaloneRootDir !== ctx.standaloneDir) { - promises.push( - cp(rootSrcDir, rootDestDir, { recursive: true, verbatimSymlinks: true, filter }).then(() => - recreateNodeModuleSymlinks(resolve('node_modules'), rootDestDir), - ), - ) + for (const nodeModulesLocationInStandalone of nodeModulesLocationsInStandalone) { + const relativeToRoot = relative(ctx.standaloneRootDir, nodeModulesLocationInStandalone) + const locationInProject = join(ctx.outputFileTracingRoot, relativeToRoot) + const locationInServerHandler = join(ctx.serverHandlerRootDir, relativeToRoot) + + promises.push(recreateNodeModuleSymlinks(locationInProject, locationInServerHandler)) } await Promise.all(promises) @@ -486,7 +467,7 @@ export const verifyHandlerDirStructure = async (ctx: PluginContext) => { // https://github.com/pnpm/pnpm/issues/9654 // https://github.com/pnpm/pnpm/issues/5928 // https://github.com/pnpm/pnpm/issues/7362 (persisting even though ticket is closed) -const nodeModulesFilter = async (sourcePath: string) => { +const nodeModulesFilter = (sourcePath: string) => { // Filtering rule for the following packages: // - @rspack+binding-linux-x64-musl // - @swc+core-linux-x64-musl diff --git a/src/build/plugin-context.ts b/src/build/plugin-context.ts index 1fe088777b..97fb2d1571 100644 --- a/src/build/plugin-context.ts +++ b/src/build/plugin-context.ts @@ -84,6 +84,17 @@ export class PluginContext { return this.requiredServerFiles.relativeAppDir ?? '' } + /** + * The root directory for output file tracing. Paths inside standalone directory preserve paths of project, relative to this directory. + */ + get outputFileTracingRoot(): string { + return ( + this.requiredServerFiles.config.outputFileTracingRoot ?? + // fallback for older Next.js versions that don't have outputFileTracingRoot in the config, but had it in config.experimental + this.requiredServerFiles.config.experimental.outputFileTracingRoot + ) + } + /** * The working directory inside the lambda that is used for monorepos to execute the serverless function */