Files
paseo/scripts/postinstall-patches.mjs
T
Mohamed Boudra 3b67e20d89 fix(app): clean up native animation and layout lifecycles (#7914)
Worklets 0.10.4 and Reanimated 4.5.5 while keeping the stale-view guard from #7907; backport the Enriched Markdown settled-row measurement fix (upstream #924); release each bottom sheet's Dimensions subscription on unmount.

(cherry picked from commit e277656cbd)
2026-10-26 11:07:42 +01:00

138 lines
5.4 KiB
JavaScript

import { copyFileSync, existsSync, mkdirSync, readdirSync, rmSync } from "node:fs";
import { spawnSync } from "node:child_process";
import { join, relative } from "node:path";
// In CI we often install a single workspace (e.g. server/relay/website). Only apply patches
// when the patched dependency is actually present.
// `cwd` is where patch-package must run from. Packages that npm does not hoist to the
// workspace root live in their workspace's own node_modules, and patch-package resolves
// the patch's node_modules/... paths relative to its working directory.
const patchedPackages = [
// Backport for expo-modules-core 57.0.23: https://github.com/expo/expo/pull/49634.
// Remove when the installed Expo SDK includes the Activity recreation fix.
{
nodeModulesPath: "node_modules/expo-modules-core",
patchPrefix: "expo-modules-core+",
},
// Backport https://github.com/software-mansion/enriched-markdown/pull/924: preserve settled
// Android measurements across parent clones. Remove those hunks when upgrading past
// 1.2.0-nightly-20261019-926ef06f0; reconcile the other local patch hunks separately.
{
nodeModulesPath: "packages/app/node_modules/react-native-enriched-markdown",
patchPrefix: "react-native-enriched-markdown+",
cwd: "packages/app",
},
// Remove after react-native-unistyles ships
// https://github.com/jpudysz/react-native-unistyles/pull/1203.
{
nodeModulesPath: "node_modules/react-native-unistyles",
patchPrefix: "react-native-unistyles+",
},
// Backport of https://github.com/software-mansion/react-native-reanimated/pull/10435
// (Reanimated 4.7.1): skip synchronous Android prop updates for unmounted view tags
// instead of throwing and printing a full stack trace on the UI thread per tag per event.
// Remove when the installed Reanimated is >= 4.7.1.
{
nodeModulesPath: "node_modules/react-native-reanimated",
patchPrefix: "react-native-reanimated+",
},
{
nodeModulesPath: "node_modules/react-native-draggable-flatlist",
patchPrefix: "react-native-draggable-flatlist+",
},
{
nodeModulesPath: "node_modules/react-native-gesture-handler",
patchPrefix: "react-native-gesture-handler+",
},
// Exports BottomSheetModalInternalProvider so a subtree can host its own sheets on the shared
// modal stack (see packages/app/src/components/ui/isolated-bottom-sheet-modal/host-scope.tsx), and
// keeps a dismissing modal's status through its final onChange(-1), so a modal with
// enableDismissOnClose=false that unmounts while presented still leaves the portal.
// Dimensions subscription cleanup: https://github.com/gorhom/react-native-bottom-sheet/issues/2734
// and https://github.com/gorhom/react-native-bottom-sheet/pull/2751. Remove the useAnimatedLayout
// hunks once an installed release includes the cleanup; retain the other local hunks.
{
nodeModulesPath: "node_modules/@gorhom/bottom-sheet",
patchPrefix: "@gorhom+bottom-sheet+",
},
{
nodeModulesPath: "packages/app/node_modules/react-native-svg",
patchPrefix: "react-native-svg+",
cwd: "packages/app",
},
{
nodeModulesPath: "node_modules/@mattermost/react-native-paste-input",
patchPrefix: "@mattermost+react-native-paste-input+",
},
{
nodeModulesPath: "packages/server/node_modules/@opencode-ai/sdk",
patchPrefix: "@opencode-ai+sdk+",
cwd: "packages/server",
},
];
const installedPackages = patchedPackages.filter(({ nodeModulesPath }) =>
existsSync(nodeModulesPath),
);
if (!existsSync("patches") || installedPackages.length === 0) {
process.exit(0);
}
const patchFiles = readdirSync("patches").filter((file) => file.endsWith(".patch"));
const isWindows = process.platform === "win32";
const cmd = isWindows ? "patch-package.cmd" : "patch-package";
let failed = false;
for (const [index, { nodeModulesPath, patchPrefix, cwd = "." }] of installedPackages.entries()) {
const files = patchFiles.filter((file) => file.startsWith(patchPrefix));
if (files.length === 0) {
continue;
}
const outcome = applyPatches(cwd, files, index);
if (outcome === "applied") {
continue;
}
failed = true;
if (outcome === "unavailable") {
continue;
}
// patch-package cannot apply a patch that changed after an earlier version of it was
// applied. Without the package, the next install fetches it clean and applies the patch.
rmSync(nodeModulesPath, { recursive: true, force: true });
console.error(
`postinstall-patches: removed ${nodeModulesPath} because its patch did not apply. ` +
"Run npm install again to reinstall it with the current patch. " +
"If it fails again on the clean package, the patch is out of date and must be regenerated.",
);
}
process.exit(failed ? 1 : 0);
function applyPatches(cwd, files, index) {
const tempPatchDir = join(".tmp", `postinstall-patches-${process.pid}-${index}`);
mkdirSync(tempPatchDir, { recursive: true });
for (const patchFile of files) {
copyFileSync(join("patches", patchFile), join(tempPatchDir, patchFile));
}
let result;
try {
result = spawnSync(cmd, ["--patch-dir", relative(cwd, tempPatchDir), "--error-on-fail"], {
cwd,
shell: isWindows,
stdio: "inherit",
windowsHide: true,
});
} finally {
rmSync(tempPatchDir, { recursive: true, force: true });
}
if (result.error) {
console.error("postinstall-patches: patch-package failed to spawn:", result.error.message);
return "unavailable";
}
return result.status === 0 ? "applied" : "rejected";
}