diff --git a/docusaurus.config.ts b/docusaurus.config.ts index e3c379db9b..d6ed88022c 100644 --- a/docusaurus.config.ts +++ b/docusaurus.config.ts @@ -955,6 +955,8 @@ const config: Config = { }, clientModules: [ require.resolve('./src/clientModules/agentSkillsNavLink.ts'), + require.resolve('./src/clientModules/translationGuard.ts'), + require.resolve('./src/clientModules/chunkLoadRecovery.ts'), ], plugins: [ [ diff --git a/src/clientModules/chunkLoadRecovery.ts b/src/clientModules/chunkLoadRecovery.ts new file mode 100644 index 0000000000..5c6e9edbdd --- /dev/null +++ b/src/clientModules/chunkLoadRecovery.ts @@ -0,0 +1,33 @@ +import { + isChunkLoadError, + reloadOnceForChunkError, + reportCrash, +} from '@site/src/utils/crashReporting'; + +/** + * Recover a first page load that asks for chunks a deploy has removed. + * + * A cached or long-open HTML page from the previous build references chunk + * hashes that no longer exist. Docusaurus already reloads when that happens + * during in-app navigation, but on the first load the route's chunks are + * fetched before hydration, and the failure is an unhandled rejection: the + * page stays as inert server-rendered HTML. Reload instead (see + * reloadOnceForChunkError for the loop guard). When the guard declines, the + * page will not start, so report it. + * + * Only until the app has started: after that, a failed chunk is a link + * preload on hover, and reloading the page the reader is on would be wrong. + */ +let started = false; + +if (typeof window !== 'undefined') { + window.addEventListener('unhandledrejection', (event) => { + if (started || !isChunkLoadError(event.reason)) return; + if (!reloadOnceForChunkError(event.reason)) reportCrash(event.reason, 'first-load'); + }); +} + +// Runs on the first render after hydration too, not only on navigation. +export function onRouteDidUpdate(): void { + started = true; +} diff --git a/src/clientModules/translationGuard.ts b/src/clientModules/translationGuard.ts new file mode 100644 index 0000000000..531cf36a14 --- /dev/null +++ b/src/clientModules/translationGuard.ts @@ -0,0 +1,40 @@ +/** + * Keep React working when the browser translates the page. + * + * Chrome and Edge translation replace text nodes with their own + * wrappers. When React later removes or inserts next to a node it believes it + * owns, the node is no longer where React left it, and the DOM throws + * "Failed to execute 'removeChild' on 'Node'", which takes the whole page + * down. + * + * The guard makes the two operations tolerate a node the translator moved: + * removing a node that is no longer a child does nothing, and inserting before + * one appends instead. Neither path runs unless the DOM would have thrown, and + * both log a warning so a real bug stays visible. This is the workaround from + * facebook/react#11538. + */ +if (typeof window !== 'undefined' && typeof Node === 'function' && Node.prototype) { + const originalRemoveChild = Node.prototype.removeChild; + Node.prototype.removeChild = function removeChild(this: Node, child: T): T { + if (child.parentNode !== this) { + console.warn('Cannot remove a child from a different parent', child, this); + return child; + } + return originalRemoveChild.call(this, child) as T; + }; + + const originalInsertBefore = Node.prototype.insertBefore; + Node.prototype.insertBefore = function insertBefore( + this: Node, + newNode: T, + referenceNode: Node | null, + ): T { + if (referenceNode && referenceNode.parentNode !== this) { + console.warn('Cannot insert before a reference node from a different parent', referenceNode, this); + return originalInsertBefore.call(this, newNode, null) as T; + } + return originalInsertBefore.call(this, newNode, referenceNode) as T; + }; +} + +export {}; diff --git a/src/components/HomePage/Frameworks/Frameworks.tsx b/src/components/HomePage/Frameworks/Frameworks.tsx index d2f12fb73f..64b1056064 100644 --- a/src/components/HomePage/Frameworks/Frameworks.tsx +++ b/src/components/HomePage/Frameworks/Frameworks.tsx @@ -64,7 +64,9 @@ export default function Frameworks({ handleFrameworkClick }: FrameworksProps) {

Select Your Framework to View Supported Products and Features

-
+ {/* Framework names are product names, and the labels change as the + selection does: left to a page translator they can crash React. */} + {frameworkCards.map((item) => { return (
{() => ( -
+ // Framework names are product names, and the labels change as the + // selection does: left to a page translator they can crash React. +
setOpenSelector(!openSelector)} diff --git a/src/components/SkillsCallout/CommandBlock.tsx b/src/components/SkillsCallout/CommandBlock.tsx index 9ec3459818..71303f5973 100644 --- a/src/components/SkillsCallout/CommandBlock.tsx +++ b/src/components/SkillsCallout/CommandBlock.tsx @@ -87,6 +87,9 @@ const CommandBlock: React.FC = ({
{command}