Repository: vercel/next.js
Pull request: Inline handler dependencies instead of tracing

This updates to remove extra tracing that was being done in `build-complete` for deployment adapters to allow importing internal `node-environment` modules. Instead this exposes `next/setup-node-env` to adapters outputs that can be used instead and are traced through our normal bundling/tracing flow. 

Changed files:
filename: packages/next/src/build/adapter/build-complete.ts
patch: @@ -45,9 +45,6 @@ import { getRedirectStatus, modifyRouteRegex } from '../../lib/redirect-status'
 import { getNamedRouteRegex } from '../../shared/lib/router/utils/route-regex'
 import { escapeStringRegexp } from '../../shared/lib/escape-regexp'
 import { sortSortableRoutes } from '../../shared/lib/router/utils/sortable-routes'
-import { nodeFileTrace } from 'next/dist/compiled/@vercel/nft'
-import { defaultOverrides } from '../../server/require-hook'
-import { makeIgnoreFn } from '../collect-build-traces'
 import { generateRoutesManifest } from '../generate-routes-manifest'
 
 interface SharedRouteFields {
@@ -492,105 +489,25 @@ export async function handleBuildComplete({
       }
 
       const sharedNodeAssets: Record<string, string> = {}
-      const pagesSharedNodeAssets: Record<string, string> = {}
-      const appPagesSharedNodeAssets: Record<string, string> = {}
-
-      const sharedTraceIgnores = [
-        '**/next/dist/compiled/next-server/**/*.dev.js',
-        '**/next/dist/compiled/webpack/*',
-        '**/node_modules/webpack5/**/*',
-        '**/next/dist/server/lib/route-resolver*',
-        'next/dist/compiled/semver/semver/**/*.js',
-        '**/node_modules/react{,-dom,-dom-server-turbopack}/**/*.development.js',
-        '**/*.d.ts',
-        '**/*.map',
-        '**/next/dist/pages/**/*',
-        '**/node_modules/sharp/**/*',
-        '**/@img/sharp-libvips*/**/*',
-        '**/next/dist/compiled/edge-runtime/**/*',
-        '**/next/dist/server/web/sandbox/**/*',
-        '**/next/dist/server/post-process.js',
-      ]
-      const sharedIgnoreFn = makeIgnoreFn(tracingRoot, sharedTraceIgnores)
 
       for (const file of requiredServerFiles) {
         // add to shared node assets
         const filePath = path.join(dir, file)
         const fileOutputPath = path.relative(tracingRoot, filePath)
         sharedNodeAssets[fileOutputPath] = filePath
       }
-
-      const moduleTypes = ['app-page', 'pages'] as const
-
-      for (const type of moduleTypes) {
-        const currentDependencies: string[] = []
-        const modulePath = require.resolve(
-          `next/dist/server/route-modules/${type}/module.compiled`
-        )
-        const contextDir = path.join(
-          path.dirname(modulePath),
-          'vendored',
-          'contexts'
-        )
-
-        for (const item of await fs.readdir(contextDir)) {
-          if (item.match(/\.(mjs|cjs|js)$/)) {
-            currentDependencies.push(path.join(contextDir, item))
-          }
-        }
-
-        const { fileList, esmFileList } = await nodeFileTrace(
-          currentDependencies,
-          {
-            base: tracingRoot,
-            ignore: sharedIgnoreFn,
-          }
-        )
-        esmFileList.forEach((item) => fileList.add(item))
-
-        for (const rootRelativeFilePath of fileList) {
-          if (type === 'pages') {
-            pagesSharedNodeAssets[rootRelativeFilePath] = path.join(
-              tracingRoot,
-              rootRelativeFilePath
-            )
-          } else {
-            appPagesSharedNodeAssets[rootRelativeFilePath] = path.join(
-              tracingRoot,
-              rootRelativeFilePath
-            )
-          }
-        }
-      }
-
-      // These are modules that are necessary for bootstrapping node env
-      const necessaryNodeDependencies = [
-        require.resolve('next/dist/server/node-environment'),
-        require.resolve('next/dist/server/require-hook'),
-        require.resolve('next/dist/server/node-polyfill-crypto'),
-        ...Object.values(defaultOverrides).filter((item) => path.extname(item)),
-      ]
-
-      const { fileList, esmFileList } = await nodeFileTrace(
-        necessaryNodeDependencies,
-        {
-          base: tracingRoot,
-          ignore: sharedIgnoreFn,
-        }
+      // add "next/setup-node-env" stub so it can be required via
+      // TODO: should we make this always available without adapters
+      const setupNodeStubPath = path.join(
+        path.dirname(require.resolve('next/package.json')),
+        'setup-node-env.js'
       )
-      esmFileList.forEach((item) => fileList.add(item))
-
-      for (const rootRelativeFilePath of fileList) {
-        sharedNodeAssets[rootRelativeFilePath] = path.join(
-          tracingRoot,
-          rootRelativeFilePath
-        )
-      }
+      sharedNodeAssets[path.relative(tracingRoot, setupNodeStubPath)] =
+        require.resolve('next/dist/build/adapter/setup-node-env.external')
 
       if (hasInstrumentationHook) {
         const assets = await handleTraceFiles(
-          path.join(distDir, 'server', 'instrumentation.js.nft.json'),
-          'neutral'
+          path.join(distDir, 'server', 'instrumentation.js.nft.json')
         )
         const fileOutputPath = path.relative(
           tracingRoot,
@@ -605,14 +522,11 @@ export async function handleBuildComplete({
       }
 
       async function handleTraceFiles(
-        traceFilePath: string,
-        type: 'pages' | 'app' | 'neutral'
+        traceFilePath: string
       ): Promise<Record<string, string>> {
         const assets: Record<string, string> = Object.assign(
           {},
-          sharedNodeAssets,
-          type === 'pages' ? pagesSharedNodeAssets : {},
-          type === 'app' ? appPagesSharedNodeAssets : {}
+          sharedNodeAssets
         )
         const traceData = JSON.parse(
           await fs.readFile(traceFilePath, 'utf8')
@@ -849,14 +763,12 @@ export async function handleBuildComplete({
         }
 
         const pageTraceFile = `${pageFile}.nft.json`
-        const assets = await handleTraceFiles(pageTraceFile, 'pages').catch(
-          (err) => {
-            if (err.code !== 'ENOENT' || (page !== '/404' && page !== '/500')) {
-              Log.warn(`Failed to locate traced assets for ${pageFile}`, err)
-            }
-            return {} as Record<string, string>
+        const assets = await handleTraceFiles(pageTraceFile).catch((err) => {
+          if (err.code !== 'ENOENT' || (page !== '/404' && page !== '/500')) {
+            Log.warn(`Failed to locate traced assets for ${pageFile}`, err)
           }
-        )
+          return {} as Record<string, string>
+        })
         const functionConfig = functionsConfigManifest.functions[route] || {}
         let sourcePage = route.replace(/^\//, '')
 
@@ -946,7 +858,7 @@ export async function handleBuildComplete({
       if (hasNodeMiddleware) {
         const middlewareFile = path.join(distDir, 'server', 'middleware.js')
         const middlewareTrace = `${middlewareFile}.nft.json`
-        const assets = await handleTraceFiles(middlewareTrace, 'neutral')
+        const assets = await handleTraceFiles(middlewareTrace)
         const functionConfig =
           functionsConfigManifest.functions['/_middleware'] || {}
 
@@ -993,12 +905,10 @@ export async function handleBuildComplete({
           const normalizedPage = normalizeAppPath(page)
           const pageFile = path.join(appDistDir, `${page}.js`)
           const pageTraceFile = `${pageFile}.nft.json`
-          const assets = await handleTraceFiles(pageTraceFile, 'app').catch(
-            (err) => {
-              Log.warn(`Failed to copy traced files for ${pageFile}`, err)
-              return {} as Record<string, string>
-            }
-          )
+          const assets = await handleTraceFiles(pageTraceFile).catch((err) => {
+            Log.warn(`Failed to copy traced files for ${pageFile}`, err)
+            return {} as Record<string, string>
+          })
 
           // If this is a parallel route we just need to merge
           // the assets as they share the same pathname

filename: packages/next/src/build/adapter/setup-node-env.external.ts
patch: @@ -0,0 +1,14 @@
+// This is a minimal import that initializes the node
+// environment, it is traced automatically for entries
+// and can be used to ensure Node.js APIs are setup
+// as expected without require `next-server`
+if (process.env.NEXT_RUNTIME !== 'edge') {
+  // eslint-disable-next-line @next/internal/typechecked-require
+  require('next/dist/server/node-environment')
+  try {
+    // eslint-disable-next-line @next/internal/typechecked-require
+    require('next/dist/server/require-hook')
+  } catch (_) {}
+  // eslint-disable-next-line @next/internal/typechecked-require
+  require('next/dist/server/node-polyfill-crypto')
+}

filename: packages/next/src/build/templates/middleware.ts
patch: @@ -1,5 +1,5 @@
 import type { AdapterOptions, EdgeHandler } from '../../server/web/adapter'
-
+import '../adapter/setup-node-env.external'
 import '../../server/web/globals'
 
 import { adapter } from '../../server/web/adapter'

filename: packages/next/src/server/route-modules/route-module.ts
patch: @@ -1,3 +1,4 @@
+import '../../build/adapter/setup-node-env.external'
 import type { IncomingMessage, ServerResponse } from 'node:http'
 import type {
   InstrumentationOnRequestError,

filename: test/production/next-server-nft/next-server-nft.test.ts
patch: @@ -134,6 +134,7 @@ async function readNormalizedNFT(next, name) {
            "/node_modules/@swc/helpers/*",
            "/node_modules/client-only/*",
            "/node_modules/detect-libc/*",
+           "/node_modules/next/dist/build/adapter/setup-node-env.external.js",
            "/node_modules/next/dist/build/define-env.js",
            "/node_modules/next/dist/build/duration-to-string.js",
            "/node_modules/next/dist/build/next-config-ts/require-hook.js",