@@ -1880,6 +1880,224 @@ describe('ReactFlightDOMNode', () => {
18801880 ) ;
18811881 } ) ;
18821882
1883+ // @gate __DEV__
1884+ it ( 'can preserve debug info when decoding and re-encoding a stream with two components blocked on the same IO' , async ( ) => {
1885+ let resolveDynamicData ;
1886+
1887+ // Used in two components, so dedupe with React.cache so we resolve them both
1888+ const getDynamicData = ReactServer . cache ( function getDynamicData ( ) {
1889+ return new Promise ( resolve => {
1890+ resolveDynamicData = resolve ;
1891+ } ) ;
1892+ } ) ;
1893+
1894+ async function Dynamic1 ( ) {
1895+ const data = await getDynamicData ( ) ;
1896+ return ReactServer . createElement ( 'p' , null , data ) ;
1897+ }
1898+ async function Dynamic2 ( ) {
1899+ const data = await getDynamicData ( ) ;
1900+ return ReactServer . createElement ( 'p' , null , data ) ;
1901+ }
1902+
1903+ function App ( ) {
1904+ return ReactServer . createElement (
1905+ 'html' ,
1906+ null ,
1907+ ReactServer . createElement (
1908+ 'body' ,
1909+ null ,
1910+ ReactServer . createElement (
1911+ ReactServer . Suspense ,
1912+ { fallback : 'Loading...' } ,
1913+ // TODO: having a wrapper <section> here seems load-bearing.
1914+ // ReactServer.createElement(ReactServer.createElement(Dynamic)),
1915+ ReactServer . createElement (
1916+ 'section' ,
1917+ null ,
1918+ ReactServer . createElement ( Dynamic1 ) ,
1919+ ) ,
1920+ ) ,
1921+ ReactServer . createElement (
1922+ ReactServer . Suspense ,
1923+ { fallback : 'Loading...' } ,
1924+ // TODO: having a wrapper <section> here seems load-bearing.
1925+ // ReactServer.createElement(ReactServer.createElement(Dynamic)),
1926+ ReactServer . createElement (
1927+ 'section' ,
1928+ null ,
1929+ ReactServer . createElement ( Dynamic2 ) ,
1930+ ) ,
1931+ ) ,
1932+ ) ,
1933+ ) ;
1934+ }
1935+
1936+ const resolveDynamic = ( ) => {
1937+ resolveDynamicData ( 'Hi Janka' ) ;
1938+ } ;
1939+
1940+ // 1. Render <App />, dividing the output into static and dynamic content.
1941+
1942+ let startTime = - 1 ;
1943+ let isStatic = true ;
1944+ const chunks1 = {
1945+ static : [ ] ,
1946+ dynamic : [ ] ,
1947+ } ;
1948+
1949+ await new Promise ( resolve => {
1950+ setTimeout ( async ( ) => {
1951+ startTime = performance . now ( ) + performance . timeOrigin ;
1952+ const stream = ReactServerDOMServer . renderToPipeableStream (
1953+ ReactServer . createElement ( App ) ,
1954+ webpackMap ,
1955+ {
1956+ filterStackFrame,
1957+ startTime,
1958+ environmentName ( ) {
1959+ return isStatic ? 'Prerender' : 'Server' ;
1960+ } ,
1961+ } ,
1962+ ) ;
1963+
1964+ const passThrough = new Stream . PassThrough ( streamOptions ) ;
1965+
1966+ passThrough . on ( 'data' , chunk => {
1967+ if ( isStatic ) {
1968+ chunks1 . static . push ( chunk ) ;
1969+ } else {
1970+ chunks1 . dynamic . push ( chunk ) ;
1971+ }
1972+ } ) ;
1973+ passThrough . on ( 'end' , resolve ) ;
1974+
1975+ stream . pipe ( passThrough ) ;
1976+ } ) ;
1977+ setTimeout ( ( ) => {
1978+ isStatic = false ;
1979+ resolveDynamic ( ) ;
1980+ } ) ;
1981+ } ) ;
1982+
1983+ //===============================================
1984+ // 2. Decode the stream from the previous step and render it again.
1985+ // This should preserve existing debug info.
1986+
1987+ const serverConsumerManifest = {
1988+ moduleMap : null ,
1989+ moduleLoading : null ,
1990+ } ;
1991+
1992+ const { chunks : chunks2 , staticEndTime : reencodeStaticEndTime } =
1993+ await reencodeFlightStream (
1994+ chunks1 . static ,
1995+ chunks1 . dynamic ,
1996+ startTime ,
1997+ serverConsumerManifest ,
1998+ ) ;
1999+
2000+ //===============================================
2001+ // 3. SSR the stream from the previous step and abort it after the static stage
2002+ // (which should trigger `onError` for each "hole" that hasn't resolved yet)
2003+
2004+ function ClientRoot ( { response} ) {
2005+ return use ( response ) ;
2006+ }
2007+
2008+ let ssrStream ;
2009+ const errorLocations : Array < {
2010+ componentStack : string ,
2011+ ownerStack : string | null ,
2012+ } > = [ ] ;
2013+
2014+ await new Promise ( async ( resolve , reject ) => {
2015+ const renderController = new AbortController ( ) ;
2016+
2017+ const serverStream = createReadableWithLateRelease (
2018+ chunks2 . static ,
2019+ chunks2 . dynamic ,
2020+ renderController . signal ,
2021+ ) ;
2022+
2023+ const decodedPromise = ReactServerDOMClient . createFromNodeStream (
2024+ serverStream ,
2025+ serverConsumerManifest ,
2026+ {
2027+ endTime : reencodeStaticEndTime ,
2028+ } ,
2029+ ) ;
2030+
2031+ setTimeout ( ( ) => {
2032+ ssrStream = ReactDOMServer . renderToPipeableStream (
2033+ React . createElement ( ClientRoot , { response : decodedPromise } ) ,
2034+ {
2035+ onError ( err , errorInfo ) {
2036+ const componentStack = errorInfo . componentStack ;
2037+ const ownerStack = React . captureOwnerStack
2038+ ? React . captureOwnerStack ( )
2039+ : null ;
2040+ errorLocations . push ( { componentStack, ownerStack} ) ;
2041+ return null ;
2042+ } ,
2043+ } ,
2044+ ) ;
2045+
2046+ renderController . signal . addEventListener (
2047+ 'abort' ,
2048+ ( ) => {
2049+ const { reason} = renderController . signal ;
2050+ ssrStream . abort ( reason ) ;
2051+ } ,
2052+ {
2053+ once : true ,
2054+ } ,
2055+ ) ;
2056+ } ) ;
2057+
2058+ setTimeout ( ( ) => {
2059+ renderController . abort ( new Error ( 'ssr-abort' ) ) ;
2060+ resolve ( ) ;
2061+ } ) ;
2062+ } ) ;
2063+
2064+ const result = await readResult ( ssrStream ) ;
2065+
2066+ expect ( errorLocations ) . toHaveLength ( 2 ) ;
2067+ expect ( normalizeCodeLocInfo ( errorLocations [ 0 ] . componentStack ) ) . toBe (
2068+ '\n' +
2069+ ' in Dynamic1 (at **)\n' +
2070+ ' in section\n' +
2071+ ' in Suspense\n' +
2072+ ' in body\n' +
2073+ ' in html\n' +
2074+ ' in App (at **)\n' +
2075+ ' in ClientRoot (at **)' ,
2076+ ) ;
2077+ expect ( normalizeCodeLocInfo ( errorLocations [ 0 ] . ownerStack ) ) . toBe (
2078+ '\n' + ' in Dynamic1 (at **)\n' + ' in App (at **)' ,
2079+ ) ;
2080+
2081+ expect ( normalizeCodeLocInfo ( errorLocations [ 1 ] . componentStack ) ) . toBe (
2082+ '\n' +
2083+ ' in Dynamic2 (at **)\n' +
2084+ ' in section\n' +
2085+ ' in Suspense\n' +
2086+ ' in body\n' +
2087+ ' in html\n' +
2088+ ' in App (at **)\n' +
2089+ ' in ClientRoot (at **)' ,
2090+ ) ;
2091+ expect ( normalizeCodeLocInfo ( errorLocations [ 1 ] . ownerStack ) ) . toBe (
2092+ '\n' + ' in Dynamic2 (at **)\n' + ' in App (at **)' ,
2093+ ) ;
2094+
2095+ expect ( result ) . toContain (
2096+ 'Switched to client rendering because the server rendering aborted due to:\n\n' +
2097+ 'ssr-abort' ,
2098+ ) ;
2099+ } ) ;
2100+
18832101 // @gate __DEV__
18842102 it ( 'can preserve debug info for promises when decoding and re-encoding a stream' , async ( ) => {
18852103 function ClientComponent ( { promise} ) {
0 commit comments