Skip to content

Commit 4e2fa6b

Browse files
chargomeclaude
andauthored
fix(nextjs): Only include emitted chunk directories in Turbopack sourcemap upload (#24259)
Turbopack builds with `supportsImmutableAssets` only emit `static/immutable/chunks`, but the `after-production-compile-turbopack` asset list still added `static/chunks` unconditionally, so the sourcemap upload logged `Directory '.next/static/chunks' does not exist` on Vercel. This guards `static/chunks` with the same `existsSync` check that #21978 added for the immutable path, so only directories Turbopack actually emitted are uploaded. closes https://linear.app/getsentry/issue/JS-3641 Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
1 parent 31e0397 commit 4e2fa6b

2 files changed

Lines changed: 50 additions & 12 deletions

File tree

packages/nextjs/src/config/getBuildPluginOptions.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -98,15 +98,16 @@ function createSourcemapUploadAssetPatterns(
9898
assets.push(path.posix.join(normalizedDistPath, getServerPattern({ useDirectoryPath: true })));
9999

100100
if (buildTool === 'after-production-compile-turbopack') {
101-
// In turbopack we always want to upload the full static chunks directory
102-
// as the build output is not split into pages|app chunks
103-
assets.push(path.posix.join(normalizedDistPath, getStaticChunksPattern({ useDirectoryPath: true })));
104-
105-
// With `experimental.supportsImmutableAssets` (auto-enabled on Vercel preview), Turbopack emits
106-
// client chunks to `static/immutable/chunks` instead of `static/chunks`
101+
// Turbopack output is not split into pages|app chunks, so the whole chunks directory is uploaded.
102+
// With `experimental.supportsImmutableAssets` (e.g. on Vercel), Turbopack emits client chunks to
103+
// `static/immutable/chunks` instead of `static/chunks`. The upload errors on asset directories that
104+
// don't exist, so each directory is only included when Turbopack actually emitted it.
105+
const staticChunksPath = path.posix.join(normalizedDistPath, getStaticChunksPattern({ useDirectoryPath: true }));
107106
const immutableChunksPath = path.posix.join(normalizedDistPath, FILE_PATTERNS.STATIC_IMMUTABLE_CHUNKS.PATH);
108-
if (fs.existsSync(immutableChunksPath)) {
109-
assets.push(immutableChunksPath);
107+
for (const chunksPath of [staticChunksPath, immutableChunksPath]) {
108+
if (fs.existsSync(chunksPath)) {
109+
assets.push(chunksPath);
110+
}
110111
}
111112
} else {
112113
// Webpack client builds in after-production-compile mode

packages/nextjs/test/config/getBuildPluginOptions.test.ts

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as fs from 'fs';
2-
import { describe, expect, it, vi } from 'vitest';
2+
import { afterEach, describe, expect, it, vi } from 'vitest';
33
import { getBuildPluginOptions } from '../../src/config/getBuildPluginOptions';
44
import type { SentryBuildOptions } from '../../src/config/types';
55

@@ -12,6 +12,14 @@ describe('getBuildPluginOptions', () => {
1212
const mockReleaseName = 'test-release-1.0.0';
1313
const mockDistDirAbsPath = '/path/to/.next';
1414

15+
function mockExistingDirectories(existingDirectories: string[]): void {
16+
vi.mocked(fs.existsSync).mockImplementation(p => existingDirectories.includes(String(p)));
17+
}
18+
19+
afterEach(() => {
20+
vi.mocked(fs.existsSync).mockReset();
21+
});
22+
1523
describe('basic functionality', () => {
1624
it('returns correct build plugin options with minimal configuration for after-production-compile-webpack', () => {
1725
const sentryBuildOptions: SentryBuildOptions = {
@@ -273,6 +281,8 @@ describe('getBuildPluginOptions', () => {
273281
});
274282

275283
it('configures after-production-compile-turbopack build correctly', () => {
284+
mockExistingDirectories(['/path/to/.next/static/chunks']);
285+
276286
const result = getBuildPluginOptions({
277287
sentryBuildOptions: baseSentryOptions,
278288
releaseName: mockReleaseName,
@@ -302,8 +312,8 @@ describe('getBuildPluginOptions', () => {
302312
expect(result.reactComponentAnnotation).toBeUndefined();
303313
});
304314

305-
it('includes the immutable chunks directory in after-production-compile-turbopack assets when it exists', () => {
306-
vi.mocked(fs.existsSync).mockImplementationOnce(p => p === '/path/to/.next/static/immutable/chunks');
315+
it('includes both chunk directories in after-production-compile-turbopack assets when both exist', () => {
316+
mockExistingDirectories(['/path/to/.next/static/chunks', '/path/to/.next/static/immutable/chunks']);
307317

308318
const result = getBuildPluginOptions({
309319
sentryBuildOptions: baseSentryOptions,
@@ -320,7 +330,7 @@ describe('getBuildPluginOptions', () => {
320330
});
321331

322332
it('does not include the immutable chunks directory in after-production-compile-turbopack assets when it does not exist', () => {
323-
vi.mocked(fs.existsSync).mockReturnValueOnce(false);
333+
mockExistingDirectories(['/path/to/.next/static/chunks']);
324334

325335
const result = getBuildPluginOptions({
326336
sentryBuildOptions: baseSentryOptions,
@@ -331,6 +341,32 @@ describe('getBuildPluginOptions', () => {
331341

332342
expect(result.sourcemaps?.assets).toEqual(['/path/to/.next/server', '/path/to/.next/static/chunks']);
333343
});
344+
345+
it('does not include the static chunks directory in after-production-compile-turbopack assets when only the immutable chunks directory exists', () => {
346+
mockExistingDirectories(['/path/to/.next/static/immutable/chunks']);
347+
348+
const result = getBuildPluginOptions({
349+
sentryBuildOptions: baseSentryOptions,
350+
releaseName: mockReleaseName,
351+
distDirAbsPath: mockDistDirAbsPath,
352+
buildTool: 'after-production-compile-turbopack',
353+
});
354+
355+
expect(result.sourcemaps?.assets).toEqual(['/path/to/.next/server', '/path/to/.next/static/immutable/chunks']);
356+
});
357+
358+
it('only includes the server directory in after-production-compile-turbopack assets when no chunk directory exists', () => {
359+
mockExistingDirectories([]);
360+
361+
const result = getBuildPluginOptions({
362+
sentryBuildOptions: baseSentryOptions,
363+
releaseName: mockReleaseName,
364+
distDirAbsPath: mockDistDirAbsPath,
365+
buildTool: 'after-production-compile-turbopack',
366+
});
367+
368+
expect(result.sourcemaps?.assets).toEqual(['/path/to/.next/server']);
369+
});
334370
});
335371

336372
describe('useRunAfterProductionCompileHook functionality', () => {
@@ -1077,6 +1113,7 @@ describe('getBuildPluginOptions', () => {
10771113

10781114
it('handles complex nested path structures', () => {
10791115
const complexPath = '/very/deep/nested/path/with/multiple/segments/.next';
1116+
mockExistingDirectories([`${complexPath}/static/chunks`]);
10801117
const sentryBuildOptions: SentryBuildOptions = {
10811118
org: 'test-org',
10821119
project: 'test-project',

0 commit comments

Comments
 (0)