agent-ecosystem/scripts/ci/verify-radix-presence-patch.mjs
2026-06-05 18:47:31 +03:00

136 lines
4.9 KiB
JavaScript

import { createRequire } from 'node:module';
import { readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
const require = createRequire(import.meta.url);
const filesToCheck = ['dist/index.js', 'dist/index.mjs'];
const patchChecks = [
{
packageName: '@radix-ui/react-presence',
requiredMarkers: ['nodeCleanupGenerationRef', 'syncNode(null)'],
forbiddenSnippets: ['setNode(node2);'],
},
{
packageName: '@radix-ui/react-focus-scope',
resolverFromPackage: '@radix-ui/react-dialog',
requiredMarkers: ['containerCleanupGenerationRef', 'syncContainer(null)'],
forbiddenSnippets: ['(node) => setContainer(node)'],
},
{
packageName: '@radix-ui/react-dismissable-layer',
resolverFromPackage: '@radix-ui/react-dialog',
requiredMarkers: ['nodeCleanupGenerationRef', 'syncNode(null)'],
forbiddenSnippets: ['(node2) => setNode(node2)'],
},
{
packageName: '@radix-ui/react-select',
requiredMarkers: [
'useGuardedNodeSetter',
'setContentRef',
'setItemTextNodeRef',
'textValueRef',
'nextTextValue',
],
forbiddenSnippets: [
'(node) => setContent(node)',
'(node) => setItemTextNode(node)',
'forwardedRef,\n (node) => contentContext.itemRefCallback?.(node, value, disabled)',
'itemContext.onItemTextChange,\n (node) => contentContext.itemTextRefCallback?.(node, itemContext.value, itemContext.disabled)',
'setTextValue((prevTextValue) => prevTextValue || (node?.textContent ?? "").trim());',
'onTriggerChange: setTrigger,',
'onValueNodeChange: setValueNode,',
'onViewportChange: setViewport,',
'ref: setContentWrapper,',
'setSelectedItem(node);',
'setSelectedItemText(node);',
],
},
{
packageName: '@radix-ui/react-slot',
requiredMarkers: ['composedRef', 'React.useMemo'],
forbiddenSnippets: [
'props2.ref = forwardedRef ? (0, import_react_compose_refs.composeRefs)(forwardedRef, childrenRef) : childrenRef;',
'props2.ref = forwardedRef ? composeRefs(forwardedRef, childrenRef) : childrenRef;',
],
},
{
packageName: '@radix-ui/react-slot',
resolverFromPackage: '@radix-ui/react-select',
requiredMarkers: ['composedRef', 'React.useMemo'],
forbiddenSnippets: [
'props2.ref = forwardedRef ? (0, import_react_compose_refs.composeRefs)(forwardedRef, childrenRef) : childrenRef;',
'props2.ref = forwardedRef ? composeRefs(forwardedRef, childrenRef) : childrenRef;',
],
},
{
packageName: '@radix-ui/react-popper',
resolverFromPackage: '@radix-ui/react-select',
requiredMarkers: ['useGuardedNodeSetter', 'setContentRef'],
forbiddenSnippets: ['(node) => setContent(node)'],
},
{
packageName: '@radix-ui/react-tooltip',
requiredMarkers: ['useGuardedNodeSetter', 'setTriggerRef'],
forbiddenSnippets: ['onTriggerChange: setTrigger,'],
},
{
packageName: '@radix-ui/react-menu',
resolverFromPackage: '@radix-ui/react-dropdown-menu',
requiredMarkers: ['useGuardedNodeSetter', 'setContentRef', 'setTriggerRef'],
forbiddenSnippets: ['onContentChange: setContent,', 'onTriggerChange: setTrigger,'],
},
{
packageName: '@radix-ui/react-checkbox',
requiredMarkers: ['useGuardedNodeSetter', 'setControlRef', 'setBubbleInputRef'],
forbiddenSnippets: [
'useComposedRefs(forwardedRef, setControl)',
'useComposedRefs(forwardedRef, setBubbleInput)',
'useComposedRefs)(forwardedRef, setControl)',
'useComposedRefs)(forwardedRef, setBubbleInput)',
],
},
];
function resolvePackageRoot({ packageName, resolverFromPackage }) {
const packageRequire = resolverFromPackage
? createRequire(require.resolve(resolverFromPackage))
: require;
const entrypointPath = packageRequire.resolve(packageName);
return dirname(dirname(entrypointPath));
}
const missing = [];
for (const check of patchChecks) {
const packageRoot = resolvePackageRoot(check);
for (const relativePath of filesToCheck) {
const filePath = join(packageRoot, relativePath);
const source = readFileSync(filePath, 'utf8');
const missingMarkers = check.requiredMarkers.filter((marker) => !source.includes(marker));
if (missingMarkers.length > 0) {
missing.push(`${check.packageName}/${relativePath}: ${missingMarkers.join(', ')}`);
}
const forbiddenSnippets = check.forbiddenSnippets ?? [];
const presentForbiddenSnippets = forbiddenSnippets.filter((snippet) => source.includes(snippet));
if (presentForbiddenSnippets.length > 0) {
missing.push(
`${check.packageName}/${relativePath}: forbidden snippets still present: ${presentForbiddenSnippets.join(', ')}`
);
}
}
}
if (missing.length > 0) {
console.error(
[
'Radix is installed without one or more local React 19 ref-cleanup patches.',
'Run `pnpm install --force` before building production artifacts.',
'',
...missing,
].join('\n')
);
process.exit(1);
}