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