mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-21 08:42:39 +00:00
* fix(app): stop a string SVG transform from crashing the app
A root <Svg> is a real React Native view, so react-native-svg rewrites a
*string* `transform` into RN style syntax in `extractTransformSvgView`,
using a PEG parser whose grammar only accepts SVG transform functions.
Upstream lets that parser's SyntaxError escape, so any string it does not
understand takes the whole app down during render:
SyntaxError: Expected transform functions but "n" found.
"found": "n", "location": { "start": { "offset": 0 } }
`none` reproduces that message character for character; so does any other
CSS value (`rotate(-90deg)`, `translate(10px, 0)`). Every sibling
extractor in that file already catches the same error and degrades, so
the asymmetry is the bug: a string that is merely ignored on a <Path>
crashes on an <Svg>.
We feed that path untrusted input. The daemon scans a project for
favicon.svg / icon.svg / logo.svg (packages/server/src/utils/project-icon.ts)
and ships it to the client, where ProjectIconImage hands it to SvgCss.
Running the library's own XML parser over `<svg style="transform:none">`
yields exactly `transform: "none"` on the root, which is why the crash
starts once a host connects and the project icons render, with no
terminal involved.
Note this is *not* reachable from an RN-style array transform: the
extractor returns arrays untouched and only parses strings, so
`style={{ transform: [{ rotate: "-90deg" }] }}` (the context window meter
and the checks ring) was never a factor.
Patch the three builds a consumer can resolve - Metro takes the package's
`react-native` field (src/*.ts), Node and Vitest take main/module - and
register react-native-svg with the postinstall patch runner so the patch
actually applies. The new test pins all three.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EDwfsMWoSFfPm2cFXe5noQ
* test(app): cover parsed root SVG transforms
Exercise the dependency patch with root props produced by the library's XML and CSS parsers, including embedded style rules that can arrive through project icons. Keep dependency-patch documentation focused on the reusable install and resolution constraints.
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
98 lines
3.0 KiB
JavaScript
98 lines
3.0 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 = [
|
|
{
|
|
nodeModulesPath: "node_modules/react-native-markdown-display",
|
|
patchPrefix: "react-native-markdown-display+",
|
|
},
|
|
{
|
|
nodeModulesPath: "node_modules/react-native-draggable-flatlist",
|
|
patchPrefix: "react-native-draggable-flatlist+",
|
|
},
|
|
{
|
|
nodeModulesPath: "node_modules/react-native-gesture-handler",
|
|
patchPrefix: "react-native-gesture-handler+",
|
|
},
|
|
{
|
|
nodeModulesPath: "node_modules/react-native-svg",
|
|
patchPrefix: "react-native-svg+",
|
|
},
|
|
{
|
|
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"));
|
|
|
|
// Group patch files by the directory patch-package must run from.
|
|
const patchFilesByCwd = new Map();
|
|
for (const { patchPrefix, cwd = "." } of installedPackages) {
|
|
const files = patchFiles.filter((file) => file.startsWith(patchPrefix));
|
|
if (files.length === 0) {
|
|
continue;
|
|
}
|
|
const group = patchFilesByCwd.get(cwd) ?? [];
|
|
group.push(...files);
|
|
patchFilesByCwd.set(cwd, group);
|
|
}
|
|
|
|
if (patchFilesByCwd.size === 0) {
|
|
process.exit(0);
|
|
}
|
|
|
|
const isWindows = process.platform === "win32";
|
|
const cmd = isWindows ? "patch-package.cmd" : "patch-package";
|
|
|
|
let groupIndex = 0;
|
|
for (const [cwd, files] of patchFilesByCwd) {
|
|
groupIndex += 1;
|
|
const tempPatchDir = join(".tmp", `postinstall-patches-${process.pid}-${groupIndex}`);
|
|
|
|
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)], {
|
|
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);
|
|
}
|
|
if (result.status !== 0) {
|
|
process.exit(result.status ?? 1);
|
|
}
|
|
}
|
|
|
|
process.exit(0);
|