Skip to content

Commit af0473b

Browse files
authored
Fix tracing of server actions imported by client components (#78968)
This fixes us failing to trace actions imported by client components due to the transform removing the actions import so our tracing isn't able to discover the action import correctly. This was previously handled in https://github.com/vercel/next.js/pull/73710/files#diff-f39646a8a89ce97754025c8258dc0709f67e4340c07735a3a82a87595ba01c10L526 but removed as part of the experiment. A regression test for the `use client` importing an actions file with file needing to be traced has been added.
1 parent 9f4a7f0 commit af0473b

File tree

5 files changed

+96
-1
lines changed

5 files changed

+96
-1
lines changed

packages/next/src/build/webpack-config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1962,6 +1962,7 @@ export default async function getBaseWebpackConfig(
19621962
appDirEnabled: hasAppDir,
19631963
traceIgnores: [],
19641964
compilerType,
1965+
swcLoaderConfig: swcDefaultLoader,
19651966
}
19661967
),
19671968
// Moment.js is an extremely popular library that bundles large locale files

packages/next/src/build/webpack/plugins/next-trace-entrypoints-plugin.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,10 @@ import picomatch from 'next/dist/compiled/picomatch'
1818
import { getModuleBuildInfo } from '../loaders/get-module-build-info'
1919
import { getPageFilePath } from '../../entries'
2020
import { resolveExternal } from '../../handle-externals'
21+
import swcLoader, { type SWCLoaderOptions } from '../loaders/next-swc-loader'
2122
import { isMetadataRouteFile } from '../../../lib/metadata/is-metadata-route'
2223
import { getCompilationSpan } from '../utils'
24+
import { isClientComponentEntryModule } from '../loaders/utils'
2325

2426
const PLUGIN_NAME = 'TraceEntryPointsPlugin'
2527
export const TRACE_IGNORES = [
@@ -137,6 +139,10 @@ export class TraceEntryPointsPlugin implements webpack.WebpackPluginInstance {
137139
private traceIgnores: string[]
138140
private esmExternals?: NextConfigComplete['experimental']['esmExternals']
139141
private compilerType: CompilerNameValues
142+
private swcLoaderConfig: {
143+
loader: string
144+
options: Partial<SWCLoaderOptions>
145+
}
140146

141147
constructor({
142148
rootDir,
@@ -147,6 +153,7 @@ export class TraceEntryPointsPlugin implements webpack.WebpackPluginInstance {
147153
traceIgnores,
148154
esmExternals,
149155
outputFileTracingRoot,
156+
swcLoaderConfig,
150157
}: {
151158
rootDir: string
152159
compilerType: CompilerNameValues
@@ -156,6 +163,7 @@ export class TraceEntryPointsPlugin implements webpack.WebpackPluginInstance {
156163
traceIgnores?: string[]
157164
outputFileTracingRoot?: string
158165
esmExternals?: NextConfigComplete['experimental']['esmExternals']
166+
swcLoaderConfig: TraceEntryPointsPlugin['swcLoaderConfig']
159167
}) {
160168
this.rootDir = rootDir
161169
this.appDir = appDir
@@ -166,6 +174,7 @@ export class TraceEntryPointsPlugin implements webpack.WebpackPluginInstance {
166174
this.traceIgnores = traceIgnores || []
167175
this.tracingRoot = outputFileTracingRoot || rootDir
168176
this.compilerType = compilerType
177+
this.swcLoaderConfig = swcLoaderConfig
169178
}
170179

171180
// Here we output all traced assets and webpack chunks to a
@@ -400,6 +409,18 @@ export class TraceEntryPointsPlugin implements webpack.WebpackPluginInstance {
400409
}
401410
})
402411

412+
const readOriginalSource = (path: string) => {
413+
return new Promise<string | Buffer>((resolve) => {
414+
compilation.inputFileSystem.readFile(path, (err, result) => {
415+
if (err) {
416+
// we can't throw here as that crashes build un-necessarily
417+
return resolve('')
418+
}
419+
resolve(result || '')
420+
})
421+
})
422+
}
423+
403424
const readFile = async (
404425
path: string
405426
): Promise<Buffer | string | null> => {
@@ -408,6 +429,60 @@ export class TraceEntryPointsPlugin implements webpack.WebpackPluginInstance {
408429
// map the transpiled source when available to avoid
409430
// parse errors in node-file-trace
410431
let source: Buffer | string = mod?.originalSource?.()?.buffer()
432+
433+
try {
434+
// fallback to reading raw source file, this may fail
435+
// due to unsupported syntax but best effort attempt
436+
let usingOriginalSource = false
437+
if (!source || isClientComponentEntryModule(mod)) {
438+
source = await readOriginalSource(path)
439+
usingOriginalSource = true
440+
}
441+
const sourceString = source.toString()
442+
443+
// If this is a client component we need to trace the
444+
// original transpiled source not the client proxy which is
445+
// applied before this plugin is run due to the
446+
// client-module-loader
447+
if (
448+
usingOriginalSource &&
449+
// don't attempt transpiling CSS or image imports
450+
path.match(/\.(tsx|ts|js|cjs|mjs|jsx)$/)
451+
) {
452+
let transformResolve: (result: string) => void
453+
let transformReject: (error: unknown) => void
454+
const transformPromise = new Promise<string>(
455+
(resolve, reject) => {
456+
transformResolve = resolve
457+
transformReject = reject
458+
}
459+
)
460+
461+
// TODO: should we apply all loaders except the
462+
// client-module-loader?
463+
swcLoader.apply(
464+
{
465+
resourcePath: path,
466+
getOptions: () => {
467+
return this.swcLoaderConfig.options
468+
},
469+
async: () => {
470+
return (err: unknown, result: string) => {
471+
if (err) {
472+
return transformReject(err)
473+
}
474+
return transformResolve(result)
475+
}
476+
},
477+
},
478+
[sourceString, undefined]
479+
)
480+
source = await transformPromise
481+
}
482+
} catch {
483+
/* non-fatal */
484+
}
485+
411486
return source || ''
412487
}
413488

test/e2e/app-dir/actions/app-action.test.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
import type { Request, Response } from 'playwright'
1111
import fs from 'fs-extra'
1212
import nodeFs from 'fs'
13-
import { join } from 'path'
13+
import path, { join } from 'path'
1414
import { outdent } from 'outdent'
1515

1616
const GENERIC_RSC_ERROR =
@@ -31,6 +31,17 @@ describe('app-dir action handling', () => {
3131
},
3232
})
3333

34+
if (isNextStart) {
35+
it('should trace server action imported by client correctly', async () => {
36+
const traceData = await next.readJSON(
37+
path.join('.next', 'server', 'app', 'client', 'page.js.nft.json')
38+
)
39+
expect(traceData.files.some((file) => file.includes('data.txt'))).toBe(
40+
true
41+
)
42+
})
43+
}
44+
3445
it('should handle action correctly with middleware rewrite', async () => {
3546
const browser = await next.browser('/rewrite-to-static-first')
3647
let actionRequestStatus: number | undefined

test/e2e/app-dir/actions/app/client/actions.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@ import 'server-only'
55
import { redirect } from 'next/navigation'
66
import { headers, cookies } from 'next/headers'
77

8+
try {
9+
require('fs').readFileSync(
10+
require('path').join(process.cwd(), 'data.txt'),
11+
'utf8'
12+
)
13+
} catch {}
14+
815
export async function getHeaders() {
916
console.log('accept header:', (await headers()).get('accept'))
1017
;(await cookies()).set('test-cookie', Date.now())

test/e2e/app-dir/actions/data.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
hello world

0 commit comments

Comments
 (0)