@@ -97,7 +97,7 @@ describe('ReactFlightDOMNode', () => {
9797 return (
9898 ' in ' +
9999 name +
100- ( / \d / . test ( m )
100+ ( / : \d + : \d + / . test ( m )
101101 ? preserveLocation
102102 ? ' ' + location . replace ( __filename , relativeFilename )
103103 : ' (at **)'
@@ -108,6 +108,27 @@ describe('ReactFlightDOMNode', () => {
108108 ) ;
109109 }
110110
111+ /** Apply `filterStackFrame` to a parent or owner stack string. */
112+ function filterCodeLocInfo ( str : string ) {
113+ const result = [ ] ;
114+ // eslint-disable-next-line no-for-of-loops/no-for-of-loops
115+ for ( const line of str . split ( '\n' ) ) {
116+ if ( line ) {
117+ const match =
118+ line . match ( / ^ [ ] + a t ( .* ?) \( ( .* ?) \) $ / ) ??
119+ line . match ( / ^ [ ] + i n ( .* ?) \( a t ( .* ?) \) $ / ) ;
120+ if ( match ) {
121+ const [ , functionName , fileName ] = match ;
122+ if ( ! filterStackFrame ( fileName , functionName ) ) {
123+ continue ;
124+ }
125+ }
126+ }
127+ result . push ( line ) ;
128+ }
129+ return result . join ( '\n' ) ;
130+ }
131+
111132 /**
112133 * Removes all stackframes not pointing into this file
113134 */
@@ -955,10 +976,10 @@ describe('ReactFlightDOMNode', () => {
955976 // The concrete location may change as this test is updated.
956977 // Just make sure they still point at React.use(p2)
957978 ( gate ( flags => flags . enableAsyncDebugInfo )
958- ? '\n at SharedComponent (./ReactFlightDOMNode-test.js:813 :7)'
979+ ? '\n at SharedComponent (./ReactFlightDOMNode-test.js:834 :7)'
959980 : '' ) +
960- '\n at ServerComponent (file://./ReactFlightDOMNode-test.js:835 :26)' +
961- '\n at App (file://./ReactFlightDOMNode-test.js:852 :25)' ,
981+ '\n at ServerComponent (file://./ReactFlightDOMNode-test.js:856 :26)' +
982+ '\n at App (file://./ReactFlightDOMNode-test.js:873 :25)' ,
962983 ) ;
963984 } else {
964985 expect ( ownerStack ) . toBeNull ( ) ;
@@ -1545,12 +1566,12 @@ describe('ReactFlightDOMNode', () => {
15451566 '\n' +
15461567 ' in Dynamic' +
15471568 ( gate ( flags => flags . enableAsyncDebugInfo )
1548- ? ' (file://ReactFlightDOMNode-test.js:1419 :27)\n'
1569+ ? ' (file://ReactFlightDOMNode-test.js:1440 :27)\n'
15491570 : '\n' ) +
15501571 ' in body\n' +
15511572 ' in html\n' +
1552- ' in App (file://ReactFlightDOMNode-test.js:1432 :25)\n' +
1553- ' in ClientRoot (ReactFlightDOMNode-test.js:1507 :16)' ,
1573+ ' in App (file://ReactFlightDOMNode-test.js:1453 :25)\n' +
1574+ ' in ClientRoot (ReactFlightDOMNode-test.js:1528 :16)' ,
15541575 ) ;
15551576 } else {
15561577 expect (
@@ -1559,7 +1580,7 @@ describe('ReactFlightDOMNode', () => {
15591580 '\n' +
15601581 ' in body\n' +
15611582 ' in html\n' +
1562- ' in ClientRoot (ReactFlightDOMNode-test.js:1507 :16)' ,
1583+ ' in ClientRoot (ReactFlightDOMNode-test.js:1528 :16)' ,
15631584 ) ;
15641585 }
15651586
@@ -1569,21 +1590,317 @@ describe('ReactFlightDOMNode', () => {
15691590 normalizeCodeLocInfo ( ownerStack , { preserveLocation : true } ) ,
15701591 ) . toBe (
15711592 '\n' +
1572- ' in Dynamic (file://ReactFlightDOMNode-test.js:1419 :27)\n' +
1573- ' in App (file://ReactFlightDOMNode-test.js:1432 :25)' ,
1593+ ' in Dynamic (file://ReactFlightDOMNode-test.js:1440 :27)\n' +
1594+ ' in App (file://ReactFlightDOMNode-test.js:1453 :25)' ,
15741595 ) ;
15751596 } else {
15761597 expect (
15771598 normalizeCodeLocInfo ( ownerStack , { preserveLocation : true } ) ,
15781599 ) . toBe (
15791600 '' +
15801601 '\n' +
1581- ' in App (file://ReactFlightDOMNode-test.js:1432 :25)' ,
1602+ ' in App (file://ReactFlightDOMNode-test.js:1453 :25)' ,
15821603 ) ;
15831604 }
15841605 } else {
15851606 expect ( ownerStack ) . toBeNull ( ) ;
15861607 }
15871608 } ) ;
1609+
1610+ function createReadableWithLateRelease ( initialChunks , lateChunks , signal ) {
1611+ // Create a new Readable and push all initial chunks immediately.
1612+ const readable = new Stream . Readable ( { ...streamOptions , read ( ) { } } ) ;
1613+ for ( let i = 0 ; i < initialChunks . length ; i ++ ) {
1614+ readable . push ( initialChunks [ i ] ) ;
1615+ }
1616+
1617+ // When prerendering is aborted, push all dynamic chunks. They won't be
1618+ // considered for rendering, but they include debug info we want to use.
1619+ signal . addEventListener (
1620+ 'abort' ,
1621+ ( ) => {
1622+ for ( let i = 0 ; i < lateChunks . length ; i ++ ) {
1623+ readable . push ( lateChunks [ i ] ) ;
1624+ }
1625+ setImmediate ( ( ) => {
1626+ readable . push ( null ) ;
1627+ } ) ;
1628+ } ,
1629+ { once : true } ,
1630+ ) ;
1631+
1632+ return readable ;
1633+ }
1634+
1635+ async function reencodeFlightStream (
1636+ staticChunks ,
1637+ dynamicChunks ,
1638+ startTime ,
1639+ serverConsumerManifest ,
1640+ ) {
1641+ let staticEndTime = - 1 ;
1642+ const chunks = {
1643+ static : [ ] ,
1644+ dynamic : [ ] ,
1645+ } ;
1646+ await new Promise ( async resolve => {
1647+ const renderStageController = new AbortController ( ) ;
1648+
1649+ const serverStream = createReadableWithLateRelease (
1650+ staticChunks ,
1651+ dynamicChunks ,
1652+ renderStageController . signal ,
1653+ ) ;
1654+ const decoded = await ReactServerDOMClient . createFromNodeStream (
1655+ serverStream ,
1656+ serverConsumerManifest ,
1657+ {
1658+ // We're re-encoding the whole stream, so we don't want to filter out any debug info.
1659+ endTime : undefined ,
1660+ } ,
1661+ ) ;
1662+
1663+ setTimeout ( async ( ) => {
1664+ const stream = ReactServerDOMServer . renderToPipeableStream (
1665+ decoded ,
1666+ webpackMap ,
1667+ {
1668+ filterStackFrame,
1669+ // Pass in the original render's startTime to avoid omitting its IO info.
1670+ startTime,
1671+ } ,
1672+ ) ;
1673+
1674+ const passThrough = new Stream . PassThrough ( streamOptions ) ;
1675+
1676+ passThrough . on ( 'data' , chunk => {
1677+ require ( 'fs' ) . writeFileSync (
1678+ process . stdout . fd ,
1679+ Buffer . from ( chunk ) . toString ( 'utf-8' ) ,
1680+ ) ;
1681+ if ( ! renderStageController . signal . aborted ) {
1682+ chunks . static . push ( chunk ) ;
1683+ } else {
1684+ chunks . dynamic . push ( chunk ) ;
1685+ }
1686+ } ) ;
1687+ passThrough . on ( 'end' , resolve ) ;
1688+
1689+ stream . pipe ( passThrough ) ;
1690+ } ) ;
1691+
1692+ setTimeout ( ( ) => {
1693+ staticEndTime = performance . now ( ) + performance . timeOrigin ;
1694+ require ( 'fs' ) . writeFileSync (
1695+ process . stdout . fd ,
1696+ '------------------------------------' + '\n' ,
1697+ ) ;
1698+ renderStageController . abort ( ) ;
1699+ } ) ;
1700+ } ) ;
1701+
1702+ return { chunks, staticEndTime} ;
1703+ }
1704+
1705+ // @gate __DEV__
1706+ it ( 'can preserve old IO info when decoding and re-encoding a stream with options.startTime' , async ( ) => {
1707+ let resolveDynamicData ;
1708+
1709+ function getDynamicData ( ) {
1710+ return new Promise ( resolve => {
1711+ resolveDynamicData = resolve ;
1712+ } ) ;
1713+ }
1714+
1715+ async function Dynamic ( ) {
1716+ const data = await getDynamicData ( ) ;
1717+ return ReactServer . createElement ( 'p' , null , data ) ;
1718+ }
1719+
1720+ function App ( ) {
1721+ return ReactServer . createElement (
1722+ 'html' ,
1723+ null ,
1724+ ReactServer . createElement (
1725+ 'body' ,
1726+ null ,
1727+ ReactServer . createElement (
1728+ ReactServer . Suspense ,
1729+ { fallback : 'Loading...' } ,
1730+ // TODO: having a wrapper <section> here seems load-bearing.
1731+ // ReactServer.createElement(ReactServer.createElement(Dynamic)),
1732+ ReactServer . createElement (
1733+ 'section' ,
1734+ null ,
1735+ ReactServer . createElement ( Dynamic ) ,
1736+ ) ,
1737+ ) ,
1738+ ) ,
1739+ ) ;
1740+ }
1741+
1742+ const resolveDynamic = ( ) => {
1743+ resolveDynamicData ( 'Hi Janka' ) ;
1744+ } ;
1745+
1746+ // 1. Render <App />, dividing the output into static and dynamic content.
1747+
1748+ let startTime = - 1 ;
1749+
1750+ let isStatic = true ;
1751+ const chunks1 = {
1752+ static : [ ] ,
1753+ dynamic : [ ] ,
1754+ } ;
1755+
1756+ await new Promise ( resolve => {
1757+ setTimeout ( async ( ) => {
1758+ startTime = performance . now ( ) + performance . timeOrigin ;
1759+
1760+ const stream = ReactServerDOMServer . renderToPipeableStream (
1761+ ReactServer . createElement ( App ) ,
1762+ webpackMap ,
1763+ {
1764+ filterStackFrame,
1765+ startTime,
1766+ environmentName ( ) {
1767+ return isStatic ? 'Prerender' : 'Server' ;
1768+ } ,
1769+ } ,
1770+ ) ;
1771+
1772+ const passThrough = new Stream . PassThrough ( streamOptions ) ;
1773+
1774+ passThrough . on ( 'data' , chunk => {
1775+ if ( isStatic ) {
1776+ chunks1 . static . push ( chunk ) ;
1777+ } else {
1778+ chunks1 . dynamic . push ( chunk ) ;
1779+ }
1780+ } ) ;
1781+ passThrough . on ( 'end' , resolve ) ;
1782+
1783+ stream . pipe ( passThrough ) ;
1784+ } ) ;
1785+ setTimeout ( ( ) => {
1786+ isStatic = false ;
1787+ resolveDynamic ( ) ;
1788+ } ) ;
1789+ } ) ;
1790+
1791+ //===============================================
1792+ // 2. Decode the stream from the previous step and render it again.
1793+ // This should preserve existing debug info.
1794+
1795+ const serverConsumerManifest = {
1796+ moduleMap : null ,
1797+ moduleLoading : null ,
1798+ } ;
1799+
1800+ const { chunks : chunks2 , staticEndTime : reencodeStaticEndTime } =
1801+ await reencodeFlightStream (
1802+ chunks1 . static ,
1803+ chunks1 . dynamic ,
1804+ // This is load-bearing. If we don't pass a startTime, IO info
1805+ // from the initial render will be skipped (because it finished in the past)
1806+ // and we won't get the precise location of the blocking await in the owner stack.
1807+ startTime ,
1808+ serverConsumerManifest ,
1809+ ) ;
1810+
1811+ //===============================================
1812+ // 3. SSR the stream from the previous step and abort it after the static stage
1813+ // (which should trigger `onError` for each "hole" that hasn't resolved yet)
1814+
1815+ function ClientRoot ( { response} ) {
1816+ return use ( response ) ;
1817+ }
1818+
1819+ let ssrStream ;
1820+ let ownerStack ;
1821+ let componentStack ;
1822+
1823+ await new Promise ( async ( resolve , reject ) => {
1824+ const renderController = new AbortController ( ) ;
1825+
1826+ const serverStream = createReadableWithLateRelease (
1827+ chunks2 . static ,
1828+ chunks2 . dynamic ,
1829+ renderController . signal ,
1830+ ) ;
1831+
1832+ const decodedPromise = ReactServerDOMClient . createFromNodeStream (
1833+ serverStream ,
1834+ serverConsumerManifest ,
1835+ {
1836+ endTime : reencodeStaticEndTime ,
1837+ } ,
1838+ ) ;
1839+
1840+ setTimeout ( ( ) => {
1841+ ssrStream = ReactDOMServer . renderToPipeableStream (
1842+ React . createElement ( ClientRoot , {
1843+ response : decodedPromise ,
1844+ } ) ,
1845+ {
1846+ onError ( err , errorInfo ) {
1847+ componentStack = errorInfo . componentStack ;
1848+ ownerStack = React . captureOwnerStack
1849+ ? React . captureOwnerStack ( )
1850+ : null ;
1851+ return null ;
1852+ } ,
1853+ } ,
1854+ ) ;
1855+
1856+ renderController . signal . addEventListener (
1857+ 'abort' ,
1858+ ( ) => {
1859+ const { reason} = renderController . signal ;
1860+ ssrStream . abort ( reason ) ;
1861+ } ,
1862+ {
1863+ once : true ,
1864+ } ,
1865+ ) ;
1866+ } ) ;
1867+
1868+ setTimeout ( ( ) => {
1869+ renderController . abort ( new Error ( 'ssr-abort' ) ) ;
1870+ resolve ( ) ;
1871+ } ) ;
1872+ } ) ;
1873+
1874+ const result = await readResult ( ssrStream ) ;
1875+
1876+ expect ( normalizeCodeLocInfo ( componentStack ) ) . toBe (
1877+ '\n' +
1878+ // TODO:
1879+ // when we reencode a stream, the component stack doesn't have server frames for the dynamic content
1880+ // (which is what causes the dynamic hole here)
1881+ // because Flight delays forwarding debug info for lazies until they resolve.
1882+ // (the owner stack is filled in `pushHaltedAwaitOnComponentStack`, so it works fine)
1883+ //
1884+ // ' in Dynamic (at **)\n'
1885+ ' in section\n' +
1886+ ' in Suspense\n' +
1887+ ' in body\n' +
1888+ ' in html\n' +
1889+ ' in App (at **)\n' +
1890+ ' in ClientRoot (at **)' ,
1891+ ) ;
1892+ expect ( normalizeCodeLocInfo ( ownerStack ) ) . toBe (
1893+ '\n' +
1894+ gate ( flags =>
1895+ flags . enableAsyncDebugInfo ? ' in Dynamic (at **)\n' : '' ,
1896+ ) +
1897+ ' in App (at **)' ,
1898+ ) ;
1899+
1900+ expect ( result ) . toContain (
1901+ 'Switched to client rendering because the server rendering aborted due to:\n\n' +
1902+ 'ssr-abort' ,
1903+ ) ;
1904+ } ) ;
15881905 } ) ;
15891906} ) ;
0 commit comments