Skip to content

Commit 19ea0a5

Browse files
committed
tests
1 parent 5db8f7a commit 19ea0a5

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
@@ -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(/^[ ]+at (.*?) \((.*?)\)$/) ??
123+
line.match(/^[ ]+in (.*?) \(at (.*?)\)$/);
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

Comments
 (0)