Skip to content

Commit 0375bee

Browse files
committed
tests
1 parent 9854d6c commit 0375bee

File tree

1 file changed

+328
-11
lines changed

1 file changed

+328
-11
lines changed

packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMNode-test.js

Lines changed: 328 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -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(/^[ ]+at (.*?) \((.*?)\)$/) ??
119+
line.match(/^[ ]+in (.*?) \(at (.*?)\)$/);
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

Comments
 (0)