101
return (
102
' in ' +
103
name +
104
- (/\d/.test(m)
104
+ (/:\d+:\d+/.test(m)
105
? preserveLocation
106
? ' ' + location.replace(__filename, relativeFilename)
107
: ' (at **)'
1589
expect(ownerStack).toBeNull();
1590
}
1591
});
1592
+
1593
+ function createReadableWithLateRelease(initialChunks, lateChunks, signal) {
1594
+ // Create a new Readable and push all initial chunks immediately.
1595
+ const readable = new Stream.Readable({...streamOptions, read() {}});
1596
+ for (let i = 0; i < initialChunks.length; i++) {
1597
+ readable.push(initialChunks[i]);
1598
+ }
1599
+
1600
+ // When prerendering is aborted, push all dynamic chunks. They won't be
1601
+ // considered for rendering, but they include debug info we want to use.
1602
+ signal.addEventListener(
1603
+ 'abort',
1604
+ () => {
1605
+ for (let i = 0; i < lateChunks.length; i++) {
1606
+ readable.push(lateChunks[i]);
1607
+ }
1608
+ setImmediate(() => {
1609
+ readable.push(null);
1610
+ });
1611
+ },
1612
+ {once: true},
1613
+ );
1614
+
1615
+ return readable;
1616
+ }
1617
+
1618
+ async function reencodeFlightStream(
1619
+ staticChunks,
1620
+ dynamicChunks,
1621
+ startTime,
1622
+ serverConsumerManifest,
1623
+ ) {
1624
+ let staticEndTime = -1;
1625
+ const chunks = {
1626
+ static: [],
1627
+ dynamic: [],
1628
+ };
1629
+ await new Promise(async resolve => {
1630
+ const renderStageController = new AbortController();
1631
+
1632
+ const serverStream = createReadableWithLateRelease(
1633
+ staticChunks,
1634
+ dynamicChunks,
1635
+ renderStageController.signal,
1636
+ );
1637
+ const decoded = await ReactServerDOMClient.createFromNodeStream(
1638
+ serverStream,
1639
+ serverConsumerManifest,
1640
+ {
1641
+ // We're re-encoding the whole stream, so we don't want to filter out any debug info.
1642
+ endTime: undefined,
1643
+ },
1644
+ );
1645
+
1646
+ setTimeout(async () => {
1647
+ const stream = ReactServerDOMServer.renderToPipeableStream(
1648
+ decoded,
1649
+ webpackMap,
1650
+ {
1651
+ filterStackFrame,
1652
+ // Pass in the original render's startTime to avoid omitting its IO info.
1653
+ startTime,
1654
+ },
1655
+ );
1656
+
1657
+ const passThrough = new Stream.PassThrough(streamOptions);
1658
+
1659
+ passThrough.on('data', chunk => {
1660
+ if (!renderStageController.signal.aborted) {
1661
+ chunks.static.push(chunk);
1662
+ } else {
1663
+ chunks.dynamic.push(chunk);
1664
+ }
1665
+ });
1666
+ passThrough.on('end', resolve);
1667
+
1668
+ stream.pipe(passThrough);
1669
+ });
1670
+
1671
+ setTimeout(() => {
1672
+ staticEndTime = performance.now() + performance.timeOrigin;
1673
+ renderStageController.abort();
1674
+ });
1675
+ });
1676
+
1677
+ return {chunks, staticEndTime};
1678
+ }
1679
+
1680
+ // @gate __DEV__
1681
+ it('can preserve old IO info when decoding and re-encoding a stream with options.startTime', async () => {
1682
+ let resolveDynamicData;
1683
+
1684
+ function getDynamicData() {
1685
+ return new Promise(resolve => {
1686
+ resolveDynamicData = resolve;
1687
+ });
1688
+ }
1689
+
1690
+ async function Dynamic() {
1691
+ const data = await getDynamicData();
1692
+ return ReactServer.createElement('p', null, data);
1693
+ }
1694
+
1695
+ function App() {
1696
+ return ReactServer.createElement(
1697
+ 'html',
1698
+ null,
1699
+ ReactServer.createElement(
1700
+ 'body',
1701
+ null,
1702
+ ReactServer.createElement(
1703
+ ReactServer.Suspense,
1704
+ {fallback: 'Loading...'},
1705
+ // TODO: having a wrapper <section> here seems load-bearing.
1706
+ // ReactServer.createElement(ReactServer.createElement(Dynamic)),
1707
+ ReactServer.createElement(
1708
+ 'section',
1709
+ null,
1710
+ ReactServer.createElement(Dynamic),
1711
+ ),
1712
+ ),
1713
+ ),
1714
+ );
1715
+ }
1716
+
1717
+ const resolveDynamic = () => {
1718
+ resolveDynamicData('Hi Janka');
1719
+ };
1720
+
1721
+ // 1. Render <App />, dividing the output into static and dynamic content.
1722
+
1723
+ let startTime = -1;
1724
+
1725
+ let isStatic = true;
1726
+ const chunks1 = {
1727
+ static: [],
1728
+ dynamic: [],
1729
+ };
1730
+
1731
+ await new Promise(resolve => {
1732
+ setTimeout(async () => {
1733
+ startTime = performance.now() + performance.timeOrigin;
1734
+
1735
+ const stream = ReactServerDOMServer.renderToPipeableStream(
1736
+ ReactServer.createElement(App),
1737
+ webpackMap,
1738
+ {
1739
+ filterStackFrame,
1740
+ startTime,
1741
+ environmentName() {
1742
+ return isStatic ? 'Prerender' : 'Server';
1743
+ },
1744
+ },
1745
+ );
1746
+
1747
+ const passThrough = new Stream.PassThrough(streamOptions);
1748
+
1749
+ passThrough.on('data', chunk => {
1750
+ if (isStatic) {
1751
+ chunks1.static.push(chunk);
1752
+ } else {
1753
+ chunks1.dynamic.push(chunk);
1754
+ }
1755
+ });
1756
+ passThrough.on('end', resolve);
1757
+
1758
+ stream.pipe(passThrough);
1759
+ });
1760
+ setTimeout(() => {
1761
+ isStatic = false;
1762
+ resolveDynamic();
1763
+ });
1764
+ });
1765
+
1766
+ //===============================================
1767
+ // 2. Decode the stream from the previous step and render it again.
1768
+ // This should preserve existing debug info.
1769
+
1770
+ const serverConsumerManifest = {
1771
+ moduleMap: null,
1772
+ moduleLoading: null,
1773
+ };
1774
+
1775
+ const {chunks: chunks2, staticEndTime: reencodeStaticEndTime} =
1776
+ await reencodeFlightStream(
1777
+ chunks1.static,
1778
+ chunks1.dynamic,
1779
+ // This is load-bearing. If we don't pass a startTime, IO info
1780
+ // from the initial render will be skipped (because it finished in the past)
1781
+ // and we won't get the precise location of the blocking await in the owner stack.
1782
+ startTime,
1783
+ serverConsumerManifest,
1784
+ );
1785
+
1786
+ //===============================================
1787
+ // 3. SSR the stream from the previous step and abort it after the static stage
1788
+ // (which should trigger `onError` for each "hole" that hasn't resolved yet)
1789
+
1790
+ function ClientRoot({response}) {
1791
+ return use(response);
1792
+ }
1793
+
1794
+ let ssrStream;
1795
+ let ownerStack;
1796
+ let componentStack;
1797
+
1798
+ await new Promise(async (resolve, reject) => {
1799
+ const renderController = new AbortController();
1800
+
1801
+ const serverStream = createReadableWithLateRelease(
1802
+ chunks2.static,
1803
+ chunks2.dynamic,
1804
+ renderController.signal,
1805
+ );
1806
+
1807
+ const decodedPromise = ReactServerDOMClient.createFromNodeStream(
1808
+ serverStream,
1809
+ serverConsumerManifest,
1810
+ {
1811
+ endTime: reencodeStaticEndTime,
1812
+ },
1813
+ );
1814
+
1815
+ setTimeout(() => {
1816
+ ssrStream = ReactDOMServer.renderToPipeableStream(
1817
+ React.createElement(ClientRoot, {
1818
+ response: decodedPromise,
1819
+ }),
1820
+ {
1821
+ onError(err, errorInfo) {
1822
+ componentStack = errorInfo.componentStack;
1823
+ ownerStack = React.captureOwnerStack
1824
+ ? React.captureOwnerStack()
1825
+ : null;
1826
+ return null;
1827
+ },
1828
+ },
1829
+ );
1830
+
1831
+ renderController.signal.addEventListener(
1832
+ 'abort',
1833
+ () => {
1834
+ const {reason} = renderController.signal;
1835
+ ssrStream.abort(reason);
1836
+ },
1837
+ {
1838
+ once: true,
1839
+ },
1840
+ );
1841
+ });
1842
+
1843
+ setTimeout(() => {
1844
+ renderController.abort(new Error('ssr-abort'));
1845
+ resolve();
1846
+ });
1847
+ });
1848
+
1849
+ const result = await readResult(ssrStream);
1850
+
1851
+ expect(normalizeCodeLocInfo(componentStack)).toBe(
1852
+ '\n' +
1853
+ // TODO:
1854
+ // when we reencode a stream, the component stack doesn't have server frames for the dynamic content
1855
+ // (which is what causes the dynamic hole here)
1856
+ // because Flight delays forwarding debug info for lazies until they resolve.
1857
+ // (the owner stack is filled in `pushHaltedAwaitOnComponentStack`, so it works fine)
1858
+ //
1859
+ // ' in Dynamic (at **)\n'
1860
+ ' in section\n' +
1861
+ ' in Suspense\n' +
1862
+ ' in body\n' +
1863
+ ' in html\n' +
1864
+ ' in App (at **)\n' +
1865
+ ' in ClientRoot (at **)',
1866
+ );
1867
+ expect(normalizeCodeLocInfo(ownerStack)).toBe(
1868
+ '\n' +
1869
+ gate(flags =>
1870
+ flags.enableAsyncDebugInfo
1871
+ ? ' in Dynamic (at **)\n'
1872
+ : ' in section\n',
1873
+ ) +
1874
+ ' in App (at **)',
1875
+ );
1876
+
1877
+ expect(result).toContain(
1878
+ 'Switched to client rendering because the server rendering aborted due to:\n\n' +
1879
+ 'ssr-abort',
1880
+ );
1881
+ });
1882
});
1883
1884
it('warns with a tailored message if eval is not available in dev', async () => {