Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit c081cf7

Browse files
authored
[sourcemaps] Fully sourcemap stacks on the Server (#81904)
1 parent 8367fae commit c081cf7

File tree

8 files changed

+96
-43
lines changed

8 files changed

+96
-43
lines changed

packages/next/src/server/app-render/collect-segment-data.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,11 @@ const filterStackFrame =
6969
? (require('../lib/source-maps') as typeof import('../lib/source-maps'))
7070
.filterStackFrameDEV
7171
: undefined
72+
const findSourceMapURL =
73+
process.env.NODE_ENV !== 'production'
74+
? (require('../lib/source-maps') as typeof import('../lib/source-maps'))
75+
.findSourceMapURLDEV
76+
: undefined
7277

7378
function onSegmentPrerenderError(error: unknown) {
7479
const digest = getDigestForWellKnownError(error)
@@ -98,6 +103,7 @@ export async function collectSegmentData(
98103
//
99104
try {
100105
await createFromReadableStream(streamFromBuffer(fullPageDataBuffer), {
106+
findSourceMapURL,
101107
serverConsumerManifest,
102108
})
103109
await waitAtLeastOneReactRenderTask()
@@ -179,6 +185,7 @@ async function PrefetchTreeData({
179185
const initialRSCPayload: InitialRSCPayload = await createFromReadableStream(
180186
createUnclosingPrefetchStream(streamFromBuffer(fullPageDataBuffer)),
181187
{
188+
findSourceMapURL,
182189
serverConsumerManifest,
183190
}
184191
)

packages/next/src/server/app-render/encryption.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,17 @@ const isEdgeRuntime = process.env.NEXT_RUNTIME === 'edge'
3030
const textEncoder = new TextEncoder()
3131
const textDecoder = new TextDecoder()
3232

33+
const filterStackFrame =
34+
process.env.NODE_ENV !== 'production'
35+
? (require('../lib/source-maps') as typeof import('../lib/source-maps'))
36+
.filterStackFrameDEV
37+
: undefined
38+
const findSourceMapURL =
39+
process.env.NODE_ENV !== 'production'
40+
? (require('../lib/source-maps') as typeof import('../lib/source-maps'))
41+
.findSourceMapURLDEV
42+
: undefined
43+
3344
/**
3445
* Decrypt the serialized string with the action id as the salt.
3546
*/
@@ -140,12 +151,6 @@ export const encryptActionBoundArgs = React.cache(
140151
})
141152
}
142153

143-
const filterStackFrame =
144-
process.env.NODE_ENV !== 'production'
145-
? (require('../lib/source-maps') as typeof import('../lib/source-maps'))
146-
.filterStackFrameDEV
147-
: undefined
148-
149154
// Using Flight to serialize the args into a string.
150155
const serialized = await streamToString(
151156
renderToReadableStream(args, clientModules, {
@@ -282,6 +287,7 @@ export async function decryptActionBoundArgs(
282287
},
283288
}),
284289
{
290+
findSourceMapURL,
285291
serverConsumerManifest: {
286292
// moduleLoading must be null because we don't want to trigger preloads of ClientReferences
287293
// to be added to the current execution. Instead, we'll wait for any ClientReference

packages/next/src/server/app-render/use-flight-response.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ const INLINE_FLIGHT_PAYLOAD_BINARY = 3
1616
const flightResponses = new WeakMap<BinaryStreamOf<any>, Promise<any>>()
1717
const encoder = new TextEncoder()
1818

19+
const findSourceMapURL =
20+
process.env.NODE_ENV !== 'production'
21+
? (require('../lib/source-maps') as typeof import('../lib/source-maps'))
22+
.findSourceMapURLDEV
23+
: undefined
24+
1925
/**
2026
* Render Flight stream.
2127
* This is only used for renderToHTML, the Flight response does not need additional wrappers.
@@ -37,6 +43,7 @@ export function useFlightStream<T>(
3743
require('react-server-dom-webpack/client') as typeof import('react-server-dom-webpack/client')
3844

3945
const newResponse = createFromReadableStream<T>(flightStream, {
46+
findSourceMapURL,
4047
serverConsumerManifest: {
4148
moduleLoading: clientReferenceManifest.moduleLoading,
4249
moduleMap: isEdgeRuntime

packages/next/src/server/lib/source-maps.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { SourceMap } from 'module'
2+
import { LRUCache } from './lru-cache'
23

34
function noSourceMap(): SourceMap | undefined {
45
return undefined
@@ -164,6 +165,49 @@ export function filterStackFrameDEV(
164165
}
165166
}
166167

168+
const invalidSourceMap = Symbol('invalid-source-map')
169+
const sourceMapURLs = new LRUCache<string | typeof invalidSourceMap>(
170+
512 * 1024 * 1024,
171+
(url) =>
172+
url === invalidSourceMap
173+
? // Ideally we'd account for key length. So we just guestimate a small source map
174+
// so that we don't create a huge cache with empty source maps.
175+
8 * 1024
176+
: // these URLs contain only ASCII characters so .length is equal to Buffer.byteLength
177+
url.length
178+
)
179+
export function findSourceMapURLDEV(
180+
scriptNameOrSourceURL: string
181+
): string | null {
182+
let sourceMapURL = sourceMapURLs.get(scriptNameOrSourceURL)
183+
if (sourceMapURL === undefined) {
184+
let sourceMapPayload: ModernSourceMapPayload | undefined
185+
try {
186+
sourceMapPayload = findSourceMap(scriptNameOrSourceURL)?.payload
187+
} catch (cause) {
188+
console.error(
189+
`${scriptNameOrSourceURL}: Invalid source map. Only conformant source maps can be used to find the original code. Cause: ${cause}`
190+
)
191+
}
192+
193+
if (sourceMapPayload === undefined) {
194+
sourceMapURL = invalidSourceMap
195+
} else {
196+
// TODO: Might be more efficient to extract the relevant section from Index Maps.
197+
// Unclear if that search is worth the smaller payload we have to stringify.
198+
const sourceMapJSON = JSON.stringify(sourceMapPayload)
199+
const sourceMapURLData = Buffer.from(sourceMapJSON, 'utf8').toString(
200+
'base64'
201+
)
202+
sourceMapURL = `data:application/json;base64,${sourceMapURLData}`
203+
}
204+
205+
sourceMapURLs.set(scriptNameOrSourceURL, sourceMapURL)
206+
}
207+
208+
return sourceMapURL === invalidSourceMap ? null : sourceMapURL
209+
}
210+
167211
export function devirtualizeReactServerURL(sourceURL: string): string {
168212
if (sourceURL.startsWith('about://React/')) {
169213
// about://React/Server/file://<filename>?42 => file://<filename>

packages/next/src/server/use-cache/use-cache-wrapper.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,11 @@ const filterStackFrame =
111111
? (require('../lib/source-maps') as typeof import('../lib/source-maps'))
112112
.filterStackFrameDEV
113113
: undefined
114+
const findSourceMapURL =
115+
process.env.NODE_ENV !== 'production'
116+
? (require('../lib/source-maps') as typeof import('../lib/source-maps'))
117+
.findSourceMapURLDEV
118+
: undefined
114119

115120
function generateCacheEntry(
116121
workStore: WorkStore,
@@ -1469,6 +1474,7 @@ export function cache(
14691474
}
14701475

14711476
return createFromReadableStream(stream, {
1477+
findSourceMapURL,
14721478
serverConsumerManifest,
14731479
temporaryReferences,
14741480
replayConsoleLogs,

packages/next/types/$$compiled.internal.d.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,9 @@ declare module 'react-server-dom-webpack/client' {
6868
export function createServerReference(
6969
id: string,
7070
callServer: CallServerCallback,
71-
encodeFormAction?: EncodeFormActionCallback,
72-
findSourceMapURL?: FindSourceMapURLCallback, // DEV-only
73-
functionName?: string
71+
encodeFormAction: EncodeFormActionCallback | undefined,
72+
findSourceMapURL: FindSourceMapURLCallback | undefined, // DEV-only
73+
functionName: string | undefined
7474
): (...args: unknown[]) => Promise<unknown>
7575

7676
export function createTemporaryReferenceSet(
@@ -100,7 +100,8 @@ declare module 'react-server-dom-webpack/client.browser' {
100100
export interface Options {
101101
callServer?: CallServerCallback
102102
environmentName?: string
103-
findSourceMapURL?: FindSourceMapURLCallback
103+
// It's optional but we want to avoid accidentally omitting it.
104+
findSourceMapURL: FindSourceMapURLCallback | undefined
104105
replayConsoleLogs?: boolean
105106
temporaryReferences?: TemporaryReferenceSet
106107
}
@@ -300,7 +301,8 @@ declare module 'react-server-dom-webpack/client.edge' {
300301
nonce?: string
301302
encodeFormAction?: EncodeFormActionCallback
302303
temporaryReferences?: TemporaryReferenceSet
303-
findSourceMapURL?: FindSourceMapURLCallback
304+
// It's optional but we want to avoid accidentally omitting it.
305+
findSourceMapURL: FindSourceMapURLCallback | undefined
304306
replayConsoleLogs?: boolean
305307
environmentName?: string
306308
}

test/development/app-dir/cache-components-dev-errors/cache-components-dev-errors.test.ts

Lines changed: 11 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -85,37 +85,17 @@ describe('Cache Components Dev Errors', () => {
8585
)
8686
})
8787

88-
if (isTurbopack) {
89-
const normalizedCliOutput = stripAnsi(
90-
next.cliOutput.slice(outputIndex)
91-
).replaceAll(`file:` + next.testDir, '<FIXME-file-protocol>')
92-
93-
// TODO(veil): Source mapping breaks due to double-encoding of the square
94-
// brackets.
95-
expect(normalizedCliOutput).toContain(
96-
`\nError: Route "/no-accessed-data": ` +
97-
`A component accessed data, headers, params, searchParams, or a short-lived cache without a Suspense boundary nor a "use cache" above it. ` +
98-
`See more info: https://nextjs.org/docs/messages/next-prerender-missing-suspense` +
99-
'\n at Page (<FIXME-file-protocol>/app/no-accessed-data/page.js:1:31)' +
100-
'\n> 1 | export default async function Page() {' +
101-
'\n | ^' +
102-
'\n 2 | await new Promise((r) => setTimeout(r, 200))' +
103-
'\n 3 | return <p>Page</p>' +
104-
'\n 4 | }'
105-
)
106-
} else {
107-
expect(stripAnsi(next.cliOutput.slice(outputIndex))).toContain(
108-
`\nError: Route "/no-accessed-data": ` +
109-
`A component accessed data, headers, params, searchParams, or a short-lived cache without a Suspense boundary nor a "use cache" above it. ` +
110-
`See more info: https://nextjs.org/docs/messages/next-prerender-missing-suspense` +
111-
'\n at Page (app/no-accessed-data/page.js:1:31)' +
112-
'\n> 1 | export default async function Page() {' +
113-
'\n | ^' +
114-
'\n 2 | await new Promise((r) => setTimeout(r, 200))' +
115-
'\n 3 | return <p>Page</p>' +
116-
'\n 4 | }'
117-
)
118-
}
88+
expect(stripAnsi(next.cliOutput.slice(outputIndex))).toContain(
89+
`\nError: Route "/no-accessed-data": ` +
90+
`A component accessed data, headers, params, searchParams, or a short-lived cache without a Suspense boundary nor a "use cache" above it. ` +
91+
`See more info: https://nextjs.org/docs/messages/next-prerender-missing-suspense` +
92+
'\n at Page (app/no-accessed-data/page.js:1:31)' +
93+
'\n> 1 | export default async function Page() {' +
94+
'\n | ^' +
95+
'\n 2 | await new Promise((r) => setTimeout(r, 200))' +
96+
'\n 3 | return <p>Page</p>' +
97+
'\n 4 | }'
98+
)
11999

120100
await expect(browser).toDisplayCollapsedRedbox(`
121101
{

test/e2e/app-dir/server-source-maps/server-source-maps.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -416,11 +416,12 @@ describe('app-dir - server source maps', () => {
416416
// Expect the invalid sourcemap warning only once per render.
417417
// Dynamic I/O renders three times.
418418
// One from filterStackFrameDEV.
419+
// One from findSourceMapURLDEV.
419420
expect(
420421
normalizeCliOutput(next.cliOutput.slice(outputIndex)).split(
421422
'Invalid source map.'
422423
).length - 1
423-
).toEqual(4)
424+
).toEqual(5)
424425
}
425426
} else {
426427
// Bundlers silently drop invalid sourcemaps.

0 commit comments

Comments
 (0)