From a4fceb9ca5e303ea7f676f2193675019eacedeba Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 25 May 2026 18:50:33 -0700 Subject: [PATCH 01/21] Patch victory-native with explicitSize + headless props Mirrors the upstream PR (https://github.com/FormidableLabs/victory-native-xl/pull/657) into the installed package so the Bun renderer can drive CartesianChart headlessly. Adds pixelmatch / pngjs for the upcoming reference-PNG test and marks @shopify/react-native-skia as a trusted dependency for Bun. Co-authored-by: Cursor --- package-lock.json | 42 +++- package.json | 6 + patches/victory-native+41.20.2.details.md | 27 +++ patches/victory-native+41.20.2.patch | 242 ++++++++++++++++++++++ 4 files changed, 315 insertions(+), 2 deletions(-) create mode 100644 patches/victory-native+41.20.2.details.md create mode 100644 patches/victory-native+41.20.2.patch diff --git a/package-lock.json b/package-lock.json index 6f0b2818ea43..8ca680b1e01a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -206,6 +206,7 @@ "@types/mime-db": "^1.43.5", "@types/node": "^20.11.5", "@types/pako": "^2.0.3", + "@types/pngjs": "^6.0.5", "@types/pusher-js": "^5.1.0", "@types/react": "^19.2.0", "@types/react-collapse": "^5.0.1", @@ -269,6 +270,8 @@ "openai": "^6.16.0", "patch-package": "^8.1.0-canary.1", "peggy": "^4.0.3", + "pixelmatch": "^7.2.0", + "pngjs": "^7.0.0", "portfinder": "^1.0.34", "prettier": "3.7.4", "react-native-clean-project": "^4.0.0-alpha4.0", @@ -17079,6 +17082,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/pngjs": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/@types/pngjs/-/pngjs-6.0.5.tgz", + "integrity": "sha512-0k5eKfrA83JOZPppLtS2C7OUtyNAl2wKNxfyYl9Q5g9lPkgBl/9hNyAu6HuEH2J4XmIv2znEpkDd0SaZVxW6iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/prop-types": { "version": "15.7.14", "license": "MIT" @@ -33385,6 +33398,19 @@ "node": ">= 6" } }, + "node_modules/pixelmatch": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-7.2.0.tgz", + "integrity": "sha512-xhcb4yHu9sM/G7foGzoLtXYcC0zHEaOXXjRKhGup0fw78Nf2Tkiapv4EQyMzrbcmQPsllAI7DbFY2UT7PlI9Pg==", + "dev": true, + "license": "ISC", + "dependencies": { + "pngjs": "^7.0.0" + }, + "bin": { + "pixelmatch": "bin/pixelmatch" + } + }, "node_modules/pkg-dir": { "version": "4.2.0", "dev": true, @@ -33545,10 +33571,13 @@ } }, "node_modules/pngjs": { - "version": "5.0.0", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz", + "integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==", + "dev": true, "license": "MIT", "engines": { - "node": ">=10.13.0" + "node": ">=14.19.0" } }, "node_modules/portfinder": { @@ -33915,6 +33944,15 @@ "node": ">=10.13.0" } }, + "node_modules/qrcode/node_modules/pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/qs": { "version": "6.13.0", "devOptional": true, diff --git a/package.json b/package.json index d4f88fb7c0cb..6c92f76d749d 100644 --- a/package.json +++ b/package.json @@ -273,6 +273,7 @@ "@types/mime-db": "^1.43.5", "@types/node": "^20.11.5", "@types/pako": "^2.0.3", + "@types/pngjs": "^6.0.5", "@types/pusher-js": "^5.1.0", "@types/react": "^19.2.0", "@types/react-collapse": "^5.0.1", @@ -336,6 +337,8 @@ "openai": "^6.16.0", "patch-package": "^8.1.0-canary.1", "peggy": "^4.0.3", + "pixelmatch": "^7.2.0", + "pngjs": "^7.0.0", "portfinder": "^1.0.34", "prettier": "3.7.4", "react-native-clean-project": "^4.0.0-alpha4.0", @@ -421,6 +424,9 @@ "expo": "55.0.25" } }, + "trustedDependencies": [ + "@shopify/react-native-skia" + ], "expo": { "autolinking": { "exclude": [ diff --git a/patches/victory-native+41.20.2.details.md b/patches/victory-native+41.20.2.details.md new file mode 100644 index 000000000000..947ee5539f60 --- /dev/null +++ b/patches/victory-native+41.20.2.details.md @@ -0,0 +1,27 @@ +# victory-native patch: explicitSize + headless props + +Mirrors upstream PR: + +## Why + +The `server/victory-chart-renderer` Bun CLI renders Expensify chart XML to PNG using +the same `CartesianChart` users see in-app, but it runs without React Native — there is no +host view to fire `onLayout`, and the existing `` wrapper expects to be mounted +inside one. This patch adds two opt-in props that let the CLI drive the chart with a +fixed canvas size and emit only the Skia subtree (so we can paint it onto an offscreen +surface created via `LoadSkiaWeb`): + +- `explicitSize?: { width: number; height: number }` — seeds the chart's size state + immediately, skipping the `onLayout` round-trip. +- `headless?: boolean` — when paired with `explicitSize`, returns a Skia-only `` + containing the chart contents instead of wrapping them in `` and a gesture + handler. The chart still renders identically inside React Native when neither prop is + passed, so the in-app code path is unchanged. + +The patch lives in both `src/` (consumed by Metro via the package's `react-native` +field) and `dist/` (consumed by Bun, which falls through to the `import` condition and +loads compiled JS / `.d.ts`). + +## Remove when + +Upstream PR is merged and we upgrade `victory-native` to a release containing it. diff --git a/patches/victory-native+41.20.2.patch b/patches/victory-native+41.20.2.patch new file mode 100644 index 000000000000..8b916df23059 --- /dev/null +++ b/patches/victory-native+41.20.2.patch @@ -0,0 +1,242 @@ +diff --git a/node_modules/victory-native/dist/cartesian/CartesianChart.d.ts b/node_modules/victory-native/dist/cartesian/CartesianChart.d.ts +index 5b718aa..28a2dd8 100644 +--- a/node_modules/victory-native/dist/cartesian/CartesianChart.d.ts ++++ b/node_modules/victory-native/dist/cartesian/CartesianChart.d.ts +@@ -14,6 +14,10 @@ export type CartesianChartRef = { + canvas: CanvasRef | null; + actions: CartesianActionsHandle; + }; ++export type CartesianChartExplicitSize = { ++ width: number; ++ height: number; ++}; + type CartesianChartProps, XK extends keyof InputFields, YK extends keyof NumericalFields> = { + data: RawData[]; + xKey: XK; +@@ -62,6 +66,16 @@ type CartesianChartProps, XK extends key + x: InputFields[XK]; + y: Record; + }> | undefined>>; ++ /** ++ * When provided, initializes chart dimensions without waiting for React Native ++ * `onLayout`. Required when using `headless`. ++ */ ++ explicitSize?: CartesianChartExplicitSize; ++ /** ++ * When `true` (with `explicitSize`), renders a Skia-only subtree suitable for ++ * headless renderers that cannot mount React Native views. ++ */ ++ headless?: boolean; + }; + export declare function CartesianChart, XK extends keyof InputFields, YK extends keyof NumericalFields>({ transformState, children, ref, ...rest }: CartesianChartProps): React.JSX.Element; + export {}; +diff --git a/node_modules/victory-native/dist/cartesian/CartesianChart.js b/node_modules/victory-native/dist/cartesian/CartesianChart.js +index a3a842c..11f1772 100644 +--- a/node_modules/victory-native/dist/cartesian/CartesianChart.js ++++ b/node_modules/victory-native/dist/cartesian/CartesianChart.js +@@ -71,18 +71,22 @@ function CartesianChart(_a) { + ); + } + exports.CartesianChart = CartesianChart; +-function CartesianChartContent({ data, xKey, yKeys, padding, domainPadding, children, renderOutside = () => null, axisOptions, domain, chartPressState, chartPressConfig, gestureHandlerConfig, onChartBoundsChange, onScaleChange, gestureLongPressDelay = 100, xAxis, yAxis, frame, transformState, transformConfig, customGestures, actionsRef, viewport, ref, }) { ++function CartesianChartContent({ data, xKey, yKeys, padding, domainPadding, children, renderOutside = () => null, axisOptions, domain, chartPressState, chartPressConfig, gestureHandlerConfig, onChartBoundsChange, onScaleChange, gestureLongPressDelay = 100, xAxis, yAxis, frame, transformState, transformConfig, customGestures, actionsRef, viewport, ref, explicitSize, headless, }) { + var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l; +- const [size, setSize] = React.useState({ width: 0, height: 0 }); ++ const [size, setSize] = React.useState(explicitSize !== null && explicitSize !== void 0 ? explicitSize : { width: 0, height: 0 }); + const chartBoundsRef = React.useRef(undefined); + const xScaleRef = React.useRef(undefined); + const yScaleRef = React.useRef(undefined); + const canvasRef = React.useRef(null); +- const [hasMeasuredLayoutSize, setHasMeasuredLayoutSize] = React.useState(false); ++ const [hasMeasuredLayoutSize, setHasMeasuredLayoutSize] = React.useState(Boolean(explicitSize)); ++ const isHeadless = Boolean(headless && explicitSize); + const onLayout = React.useCallback(({ nativeEvent: { layout } }) => { ++ if (explicitSize) { ++ return; ++ } + setHasMeasuredLayoutSize(true); + setSize(layout); +- }, []); ++ }, [explicitSize]); + const normalizedAxisProps = (0, useBuildChartAxis_1.useBuildChartAxis)({ + xAxis, + yAxis, +@@ -458,8 +462,7 @@ function CartesianChartContent({ data, xKey, yKeys, padding, domainPadding, chil + : null; + const XAxisComponents = hasMeasuredLayoutSize && (axisOptions || xAxis) ? () : null; + const FrameComponent = hasMeasuredLayoutSize && (axisOptions || frame) ? () : null; +- // Body of the chart. +- const body = ( ++ const chartContent = (<> + {YAxisComponents} + {XAxisComponents} + {FrameComponent} +@@ -471,6 +474,9 @@ function CartesianChartContent({ data, xKey, yKeys, padding, domainPadding, chil + + + {hasMeasuredLayoutSize && (renderOutside === null || renderOutside === void 0 ? void 0 : renderOutside(renderArg))} ++ ); ++ const body = isHeadless ? ({chartContent}) : ( ++ {chartContent} + ); + let composed = customGestures !== null && customGestures !== void 0 ? customGestures : react_native_gesture_handler_1.Gesture.Race(); + if (transformState) { +@@ -486,8 +492,11 @@ function CartesianChartContent({ data, xKey, yKeys, padding, domainPadding, chil + if (chartPressState) { + composed = react_native_gesture_handler_1.Gesture.Race(composed, panGesture); + } ++ if (isHeadless) { ++ return body; ++ } + return ( +- ++ + {body} + = { + actions: CartesianActionsHandle; + }; + ++export type CartesianChartExplicitSize = { ++ width: number; ++ height: number; ++}; ++ + type CartesianChartProps< + RawData extends Record, + XK extends keyof InputFields, +@@ -130,6 +135,16 @@ type CartesianChartProps< + | undefined + > + >; ++ /** ++ * When provided, initializes chart dimensions without waiting for React Native ++ * `onLayout`. Required when using `headless`. ++ */ ++ explicitSize?: CartesianChartExplicitSize; ++ /** ++ * When `true` (with `explicitSize`), renders a Skia-only subtree suitable for ++ * headless renderers that cannot mount React Native views. ++ */ ++ headless?: boolean; + }; + + export function CartesianChart< +@@ -180,8 +195,12 @@ function CartesianChartContent< + actionsRef, + viewport, + ref, ++ explicitSize, ++ headless, + }: CartesianChartProps) { +- const [size, setSize] = React.useState({ width: 0, height: 0 }); ++ const [size, setSize] = React.useState( ++ explicitSize ?? { width: 0, height: 0 }, ++ ); + const chartBoundsRef = React.useRef(undefined); + const xScaleRef = React.useRef< + ScaleLogarithmic | ScaleLinear | undefined +@@ -191,14 +210,20 @@ function CartesianChartContent< + undefined, + ); + const canvasRef = React.useRef(null); +- const [hasMeasuredLayoutSize, setHasMeasuredLayoutSize] = +- React.useState(false); ++ const [hasMeasuredLayoutSize, setHasMeasuredLayoutSize] = React.useState( ++ Boolean(explicitSize), ++ ); ++ const isHeadless = Boolean(headless && explicitSize); + const onLayout = React.useCallback( + ({ nativeEvent: { layout } }: LayoutChangeEvent) => { ++ if (explicitSize) { ++ return; ++ } ++ + setHasMeasuredLayoutSize(true); + setSize(layout); + }, +- [], ++ [explicitSize], + ); + const normalizedAxisProps = useBuildChartAxis({ + xAxis, +@@ -707,9 +732,8 @@ function CartesianChartContent< + /> + ) : null; + +- // Body of the chart. +- const body = ( +- ++ const chartContent = ( ++ <> + {YAxisComponents} + {XAxisComponents} + {FrameComponent} +@@ -721,6 +745,14 @@ function CartesianChartContent< + + + {hasMeasuredLayoutSize && renderOutside?.(renderArg)} ++ ++ ); ++ ++ const body = isHeadless ? ( ++ {chartContent} ++ ) : ( ++ ++ {chartContent} + + ); + +@@ -748,9 +780,21 @@ function CartesianChartContent< + composed = Gesture.Race(composed, panGesture); + } + ++ if (isHeadless) { ++ return body; ++ } ++ + return ( + +- ++ + {body} + Date: Mon, 25 May 2026 19:21:43 -0700 Subject: [PATCH 02/21] Render a hardcoded CartesianChart from the Bun CLI Replaces the PR 2 hello-world entry with a real headless `CartesianChart` that draws two bars to a Skia offscreen surface and writes the result as a PNG. To get `victory-native` and `@shopify/react-native-skia` to load under Bun, the React Native peers are stubbed via tsconfig path mappings dropped into both installed packages by patch-package; Metro ignores those tsconfigs so the in-app build is untouched. Co-authored-by: Cursor --- package.json | 6 +- ...hopify+react-native-skia+2.4.14.details.md | 21 ++++ .../@shopify+react-native-skia+2.4.14.patch | 25 +++++ patches/victory-native+41.20.2.details.md | 8 ++ patches/victory-native+41.20.2.patch | 22 ++++ server/tsconfig.json | 23 +++- server/victory-chart-renderer/src/cli.ts | 60 ++++++++++- .../stubs/react-native-gesture-handler.ts | 100 ++++++++++++++++++ .../stubs/react-native-reanimated.js | 66 ++++++++++++ .../stubs/react-native-subpath.ts | 12 +++ .../stubs/react-native.ts | 65 ++++++++++++ 11 files changed, 401 insertions(+), 7 deletions(-) create mode 100644 patches/@shopify+react-native-skia+2.4.14.details.md create mode 100644 patches/@shopify+react-native-skia+2.4.14.patch create mode 100644 server/victory-chart-renderer/stubs/react-native-gesture-handler.ts create mode 100644 server/victory-chart-renderer/stubs/react-native-reanimated.js create mode 100644 server/victory-chart-renderer/stubs/react-native-subpath.ts create mode 100644 server/victory-chart-renderer/stubs/react-native.ts diff --git a/package.json b/package.json index 6c92f76d749d..09b5eec2a725 100644 --- a/package.json +++ b/package.json @@ -72,9 +72,9 @@ "web:dist": "concurrently \"npm:web-proxy\" \"http-server ./dist --cors --port 8080 -P http://localhost:9000\"", "octokit": "cd scripts && npx ts-node -i -e \"$(cat ./octokit.ts)\"", "compress-svg": "npx ts-node scripts/compressSvg.ts --dir assets/images && npx ts-node scripts/compressSvg.ts --dir docs/assets/images", - "server:vcr:dev": "bun run server/victory-chart-renderer/src/cli.ts", - "server:vcr:build:linux": "bun build --compile --target=bun-linux-x64 server/victory-chart-renderer/src/cli.ts --outfile server/victory-chart-renderer/dist/victory-chart-renderer-linux-x64", - "server:vcr:build:macos": "bun build --compile --target=bun-darwin-arm64 server/victory-chart-renderer/src/cli.ts --outfile server/victory-chart-renderer/dist/victory-chart-renderer-darwin-arm64" + "server:vcr:dev": "bun --conditions=react-native run server/victory-chart-renderer/src/cli.ts", + "server:vcr:build:linux": "bun build --conditions=react-native --compile --target=bun-linux-x64 server/victory-chart-renderer/src/cli.ts --outfile server/victory-chart-renderer/dist/victory-chart-renderer-linux-x64", + "server:vcr:build:macos": "bun build --conditions=react-native --compile --target=bun-darwin-arm64 server/victory-chart-renderer/src/cli.ts --outfile server/victory-chart-renderer/dist/victory-chart-renderer-darwin-arm64" }, "dependencies": { "@babel/runtime": "^7.25.0", diff --git a/patches/@shopify+react-native-skia+2.4.14.details.md b/patches/@shopify+react-native-skia+2.4.14.details.md new file mode 100644 index 000000000000..e8a4656a94f2 --- /dev/null +++ b/patches/@shopify+react-native-skia+2.4.14.details.md @@ -0,0 +1,21 @@ +# @shopify/react-native-skia patch: local tsconfig for Bun headless renderer + +## Why + +The `server/victory-chart-renderer` Bun CLI renders Skia content without React +Native. Bun (and any other non-RN consumer) loads `@shopify/react-native-skia`'s +root entry, which transitively imports `react-native`, +`react-native-reanimated`, and `react-native/Libraries/...` subpaths. None of +those packages run in Bun. + +This patch drops a minimal `tsconfig.json` inside the installed package so that +when Bun resolves imports from files in `node_modules/@shopify/react-native-skia/...` +it walks up to this `tsconfig.json` and remaps the React Native dependencies to +the stubs in `server/victory-chart-renderer/stubs/`. Metro doesn't consult +`tsconfig.json` paths, so the in-app build is unaffected. + +## Remove when + +We replace the Bun renderer with a different mechanism, or the Skia package +ships a `bun` export condition (or equivalent) that lets us load a stripped +entry without React Native dependencies. diff --git a/patches/@shopify+react-native-skia+2.4.14.patch b/patches/@shopify+react-native-skia+2.4.14.patch new file mode 100644 index 000000000000..0b5f8bc62178 --- /dev/null +++ b/patches/@shopify+react-native-skia+2.4.14.patch @@ -0,0 +1,25 @@ +diff --git a/node_modules/@shopify/react-native-skia/tsconfig.json b/node_modules/@shopify/react-native-skia/tsconfig.json +new file mode 100644 +index 0000000..50f2975 +--- /dev/null ++++ b/node_modules/@shopify/react-native-skia/tsconfig.json +@@ -0,0 +1,19 @@ ++{ ++ "compilerOptions": { ++ "baseUrl": ".", ++ "paths": { ++ "react-native": [ ++ "../../../server/victory-chart-renderer/stubs/react-native.ts" ++ ], ++ "react-native/*": [ ++ "../../../server/victory-chart-renderer/stubs/react-native-subpath.ts" ++ ], ++ "react-native-reanimated": [ ++ "../../../server/victory-chart-renderer/stubs/react-native-reanimated.js" ++ ], ++ "react-native-gesture-handler": [ ++ "../../../server/victory-chart-renderer/stubs/react-native-gesture-handler.ts" ++ ] ++ } ++ } ++} diff --git a/patches/victory-native+41.20.2.details.md b/patches/victory-native+41.20.2.details.md index 947ee5539f60..3ae2126211fc 100644 --- a/patches/victory-native+41.20.2.details.md +++ b/patches/victory-native+41.20.2.details.md @@ -22,6 +22,14 @@ The patch lives in both `src/` (consumed by Metro via the package's `react-nativ field) and `dist/` (consumed by Bun, which falls through to the `import` condition and loads compiled JS / `.d.ts`). +A small `tsconfig.json` is also dropped into the installed package. Bun's +resolver walks up to the closest `tsconfig.json` when resolving bare specifiers +in `node_modules/victory-native/...`, so this file remaps the chart's React +Native dependencies (`react-native`, `react-native-reanimated`, +`react-native-gesture-handler`) to the stubs in +`server/victory-chart-renderer/stubs/`. Metro doesn't honor `tsconfig.json` +paths, so the in-app build is unaffected. + ## Remove when Upstream PR is merged and we upgrade `victory-native` to a release containing it. diff --git a/patches/victory-native+41.20.2.patch b/patches/victory-native+41.20.2.patch index 8b916df23059..5007104b1649 100644 --- a/patches/victory-native+41.20.2.patch +++ b/patches/victory-native+41.20.2.patch @@ -240,3 +240,25 @@ index 3065639..e291087 100644 type CartesianChartRef, } from "./cartesian/CartesianChart"; +diff --git a/node_modules/victory-native/tsconfig.json b/node_modules/victory-native/tsconfig.json +new file mode 100644 +index 0000000..0fff5ee +--- /dev/null ++++ b/node_modules/victory-native/tsconfig.json +@@ -0,0 +1,16 @@ ++{ ++ "compilerOptions": { ++ "baseUrl": ".", ++ "paths": { ++ "react-native": [ ++ "../../server/victory-chart-renderer/stubs/react-native.ts" ++ ], ++ "react-native-reanimated": [ ++ "../../server/victory-chart-renderer/stubs/react-native-reanimated.js" ++ ], ++ "react-native-gesture-handler": [ ++ "../../server/victory-chart-renderer/stubs/react-native-gesture-handler.ts" ++ ] ++ } ++ } ++} diff --git a/server/tsconfig.json b/server/tsconfig.json index 0c0135bb370d..f739bda96471 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -1,7 +1,26 @@ { "extends": "../tsconfig.json", "compilerOptions": { - "types": ["@types/bun"] + "types": ["@types/bun"], + "paths": { + "react-native": ["./victory-chart-renderer/stubs/react-native.ts"], + "react-native-reanimated": ["./victory-chart-renderer/stubs/react-native-reanimated.js"], + "react-native-gesture-handler": ["./victory-chart-renderer/stubs/react-native-gesture-handler.ts"], + "@assets/*": ["../assets/*"], + "@components/*": ["../src/components/*"], + "@github/*": ["../.github/*"], + "@hooks/*": ["../src/hooks/*"], + "@libs/*": ["../src/libs/*"], + "@navigation/*": ["../src/libs/Navigation/*"], + "@pages/*": ["../src/pages/*"], + "@prompts/*": ["../prompts/*"], + "@scripts/*": ["../scripts/*", "../.github/scripts/*"], + "@styles/*": ["../src/styles/*"], + "@src/*": ["../src/*"], + "@userActions/*": ["../src/libs/actions/*"], + "@selectors/*": ["../src/selectors/*"], + "tests/*": ["../tests/*"] + } }, - "include": ["victory-chart-renderer/src", "victory-chart-renderer/__tests__"] + "include": ["victory-chart-renderer/src", "victory-chart-renderer/__tests__", "victory-chart-renderer/stubs"] } diff --git a/server/victory-chart-renderer/src/cli.ts b/server/victory-chart-renderer/src/cli.ts index 59adda34eb26..196e9a18b6b1 100644 --- a/server/victory-chart-renderer/src/cli.ts +++ b/server/victory-chart-renderer/src/cli.ts @@ -1,12 +1,68 @@ +/* eslint-disable no-restricted-imports */ +import {LoadSkiaWeb} from '@shopify/react-native-skia/lib/module/web/LoadSkiaWeb'; +import {JsiSkApi} from '@shopify/react-native-skia/lib/module/skia/web'; +import {drawOffscreen, makeOffscreenSurface} from '@shopify/react-native-skia/lib/module/headless'; import CLI from '@scripts/utils/CLI'; const cli = new CLI({ positionalArgs: [ { name: 'outPath', - description: 'Output file path for the scaffold message', + description: 'Path to write the rendered PNG to', }, ], }); -await Bun.write(cli.positionalArgs.outPath, 'victory-chart-renderer scaffolded\n'); +await LoadSkiaWeb(); + +// The `Skia` named export from `@shopify/react-native-skia` is captured at +// module load time from `globalThis.SkiaApi` (the native bridge on iOS/Android). +// Off-device we have to seed it ourselves before anything imports `victory-native`, +// which calls `Skia.Path.Make()` from inside `useBarPath`. +(globalThis as unknown as {SkiaApi: ReturnType}).SkiaApi = JsiSkApi( + (globalThis as unknown as {CanvasKit: unknown}).CanvasKit, +); + +const [React, victoryNative] = await Promise.all([import('react'), import('victory-native')]); +const {createElement} = React; +const {Bar, CartesianChart} = victoryNative; + +const width = 400; +const height = 250; + +const data: Array<{x: number; y: number}> = [ + {x: 1, y: 60}, + {x: 2, y: 90}, +]; + +const chartElement = createElement( + CartesianChart<(typeof data)[number], 'x', 'y'>, + { + data, + xKey: 'x', + yKeys: ['y'], + domain: {x: [0, 3], y: [0, 100]}, + domainPadding: {left: 40, right: 40, top: 20, bottom: 20}, + padding: 20, + explicitSize: {width, height}, + headless: true, + xAxis: {lineColor: '#111827', lineWidth: 2}, + yAxis: [{yKeys: ['y'], lineColor: '#111827', lineWidth: 2}], + }, + ({points, chartBounds}) => + createElement(Bar, { + points: points.y, + chartBounds, + color: '#4f46e5', + innerPadding: 0.45, + }), +); + +using surface = makeOffscreenSurface(width, height); +using image = await drawOffscreen(surface, chartElement); +const pngBytes = image.encodeToBytes(); +if (!pngBytes) { + throw new Error('Skia failed to encode the rendered chart to PNG bytes'); +} + +await Bun.write(cli.positionalArgs.outPath, pngBytes); diff --git a/server/victory-chart-renderer/stubs/react-native-gesture-handler.ts b/server/victory-chart-renderer/stubs/react-native-gesture-handler.ts new file mode 100644 index 000000000000..99e666b1cc57 --- /dev/null +++ b/server/victory-chart-renderer/stubs/react-native-gesture-handler.ts @@ -0,0 +1,100 @@ +// Minimal `react-native-gesture-handler` stub for the headless Bun renderer. +// +// The chart code builds gesture configs (`Gesture.Pan()...`) and composes +// them (`Gesture.Race`, `Gesture.Simultaneous`) during render even when no +// gesture is ever attached. We expose a chainable builder so those calls +// succeed; the headless branch of `CartesianChart` early-returns before any +// gesture handlers would actually be attached. + +import type {FunctionComponent, PropsWithChildren} from 'react'; + +class GestureBuilder { + onTouchesDown(): this { + return this; + } + + onStart(): this { + return this; + } + + onTouchesMove(): this { + return this; + } + + onTouchesUp(): this { + return this; + } + + onEnd(): this { + return this; + } + + onFinalize(): this { + return this; + } + + onChange(): this { + return this; + } + + onUpdate(): this { + return this; + } + + activateAfterLongPress(): this { + return this; + } + + activeOffsetX(): this { + return this; + } + + activeOffsetY(): this { + return this; + } + + failOffsetX(): this { + return this; + } + + failOffsetY(): this { + return this; + } + + minDistance(): this { + return this; + } + + enabled(): this { + return this; + } + + runOnJS(): this { + return this; + } +} + +const buildGesture = (): GestureBuilder => new GestureBuilder(); + +export const Gesture = { + Pan: buildGesture, + Pinch: buildGesture, + Rotation: buildGesture, + Tap: buildGesture, + LongPress: buildGesture, + Manual: buildGesture, + Race: (..._gestures: unknown[]) => buildGesture(), + Simultaneous: (..._gestures: unknown[]) => buildGesture(), + Exclusive: (..._gestures: unknown[]) => buildGesture(), +}; + +const noopComponent: FunctionComponent> = () => null; + +export const GestureHandlerRootView = noopComponent; +export const GestureDetector = noopComponent; + +export type ComposedGesture = GestureBuilder; +export type GestureType = GestureBuilder; +export type PanGesture = GestureBuilder; +export type PinchGesture = GestureBuilder; +export type TouchData = {id: number; x: number; y: number; absoluteX: number; absoluteY: number}; diff --git a/server/victory-chart-renderer/stubs/react-native-reanimated.js b/server/victory-chart-renderer/stubs/react-native-reanimated.js new file mode 100644 index 000000000000..316b6f770c80 --- /dev/null +++ b/server/victory-chart-renderer/stubs/react-native-reanimated.js @@ -0,0 +1,66 @@ +// Minimal `react-native-reanimated` stub for the headless Bun renderer. +// +// `victory-native`'s `CartesianChart` calls `useSharedValue` while rendering, +// and `@shopify/react-native-skia`'s `ReanimatedProxy` reads arbitrary +// properties off the module at load (`Rea.createWorkletRuntime`, +// `Rea.runOnRuntime`, etc.). Both must keep working without a real Reanimated +// runtime mounted. +// +// To handle both shapes uniformly, the module exports a Proxy. ES-style named +// imports (`import { useSharedValue } from ...`) read the proxy's own +// properties; default imports (`import Rea from ...`) get the same proxy, +// which falls back to a no-op function for any unknown property. +// +// This file is CommonJS so that Bun returns the proxy directly from +// `require("react-native-reanimated")`. +'use strict'; + +const noopFn = () => undefined; + +const noopComponent = () => null; + +const sharedValue = (initialValue) => ({value: initialValue}); + +const exports_ = { + __esModule: true, + default: undefined, + useSharedValue: sharedValue, + useDerivedValue: (updater) => ({value: updater()}), + useAnimatedStyle: (factory) => factory(), + useAnimatedReaction: noopFn, + makeMutable: sharedValue, + isSharedValue: (value) => typeof value === 'object' && value !== null && 'value' in value, + runOnJS: (fn) => fn, + runOnUI: (fn) => fn, + withTiming: (value) => value, + withSpring: (value) => value, + withDecay: noopFn, + withDelay: (_delay, animation) => animation, + withRepeat: (animation) => animation, + cancelAnimation: noopFn, + Easing: { + linear: () => 0, + ease: () => 0, + bezier: () => () => 0, + inOut: (fn) => fn, + }, + View: noopComponent, + Image: noopComponent, + Text: noopComponent, + ScrollView: noopComponent, +}; + +const proxy = new Proxy(exports_, { + get(target, prop) { + if (prop in target) { + return target[prop]; + } + return noopFn; + }, +}); + +// Match Reanimated's runtime shape: `import Rea from "react-native-reanimated"` +// returns the same proxy as `import * as Rea`. +exports_.default = proxy; + +module.exports = proxy; diff --git a/server/victory-chart-renderer/stubs/react-native-subpath.ts b/server/victory-chart-renderer/stubs/react-native-subpath.ts new file mode 100644 index 000000000000..2628d084bcea --- /dev/null +++ b/server/victory-chart-renderer/stubs/react-native-subpath.ts @@ -0,0 +1,12 @@ +// Catch-all stub for `react-native/Libraries/...` style deep imports that +// `@shopify/react-native-skia` emits at module load (e.g. its native picture +// view spec or asset registry helpers). None of those code paths execute in +// the Bun headless renderer, but their import statements still run, so this +// stub provides a no-op default plus a few of the most common named exports. + +const noopFunction = (..._args: unknown[]) => null; + +export default noopFunction; +export const codegenNativeComponent = noopFunction; +export const registerAsset = (asset: unknown) => asset; +export const getAssetByID = () => null; diff --git a/server/victory-chart-renderer/stubs/react-native.ts b/server/victory-chart-renderer/stubs/react-native.ts new file mode 100644 index 000000000000..9da326cc8c50 --- /dev/null +++ b/server/victory-chart-renderer/stubs/react-native.ts @@ -0,0 +1,65 @@ +// Minimal `react-native` stub for the headless Bun renderer. +// +// `react-native`'s real entry pulls in Flow-typed sources, native module +// bindings, and Metro-resolved platform extensions that Bun cannot parse. +// Our headless `CartesianChart` path never mounts a real RN view tree, so we +// only need enough surface area for `victory-native` and `@shopify/react-native-skia` +// modules to load without throwing while we walk the headless branch. +// +// Anything that escapes into the render tree (e.g. ``) renders to `null` +// so it cannot affect the Skia output. + +import type {FunctionComponent, PropsWithChildren} from 'react'; + +const noopComponent: FunctionComponent> = () => null; + +export type LayoutChangeEvent = {nativeEvent: {layout: {x: number; y: number; width: number; height: number}}}; +export type ViewStyle = Record; +export type ImageStyle = Record; +export type TextStyle = Record; +export type StyleProp = T | T[] | null | undefined | false; +export type TransformsStyle = Record; + +export const View = noopComponent; +export const Image = Object.assign(noopComponent, { + resolveAssetSource: (source: unknown) => { + if (source && typeof source === 'object' && 'uri' in (source as Record)) { + return source as {uri: string}; + } + return {uri: String(source)}; + }, +}); +export const Text = noopComponent; + +export const Platform = { + OS: 'web' as const, + Version: 0, + select: (specifics: {default?: T; web?: T}) => specifics.web ?? specifics.default, +}; + +export const PixelRatio = { + get: () => 1, + getFontScale: () => 1, + getPixelSizeForLayoutSize: (size: number) => size, + roundToNearestPixel: (size: number) => size, +}; + +export const StyleSheet = { + create: (styles: T): T => styles, + flatten: (style: T): T => style, + compose: (a: T, _b: T): T => a, + hairlineWidth: 1, + absoluteFill: {}, + absoluteFillObject: {}, +}; + +export const findNodeHandle = () => null; + +// `@shopify/react-native-skia/lib/module/specs/NativeSkiaModule` does +// `TurboModuleRegistry.getEnforcing("RNSkiaModule")` at module load. The +// `Platform.OS === "web"` short-circuit in NativeSetup then ignores the +// missing module, but we still need the named export to exist. +export const TurboModuleRegistry = { + get: (_name: string): null => null, + getEnforcing: (_name: string): null => null, +}; From 390a69ef98f0004a0898bb00950310d14f62bbf9 Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 25 May 2026 19:35:53 -0700 Subject: [PATCH 03/21] Wire up reference-PNG test, JSX entry, and lint config - Switch the Bun CLI to a `.tsx` JSX entry so the chart tree types cleanly through `createElement` without `react/no-children-prop` workarounds. - Replace the PR 2 hello-world smoke test with a real reference-PNG check that decodes the rendered PNG with `pngjs`, compares it to a checked-in `__tests__/__golden__/smoke.png` via `pixelmatch`, and supports `UPDATE_GOLDEN=1 bun test` to regenerate the golden intentionally. - Extend the server ESLint overrides to cover `.tsx` so the subproject's Bun-aware tsconfig wins for the new entry point. Co-authored-by: Cursor --- config/eslint/eslint.config.mjs | 2 +- package.json | 6 +- .../__tests__/__golden__/smoke.png | Bin 0 -> 1951 bytes .../__tests__/cli.test.ts | 54 +++++++++++++--- .../src/{cli.ts => cli.tsx} | 61 +++++++++--------- .../stubs/react-native-gesture-handler.ts | 26 ++++---- .../stubs/react-native-reanimated.js | 9 ++- .../stubs/react-native-subpath.ts | 10 +-- .../stubs/react-native.ts | 44 +++++++------ 9 files changed, 127 insertions(+), 85 deletions(-) create mode 100644 server/victory-chart-renderer/__tests__/__golden__/smoke.png rename server/victory-chart-renderer/src/{cli.ts => cli.tsx} (52%) diff --git a/config/eslint/eslint.config.mjs b/config/eslint/eslint.config.mjs index 329caf5f6170..0c39109f1954 100644 --- a/config/eslint/eslint.config.mjs +++ b/config/eslint/eslint.config.mjs @@ -736,7 +736,7 @@ const config = defineConfig([ }, { - files: ['server/**/*.ts'], + files: ['server/**/*.ts', 'server/**/*.tsx'], languageOptions: { parserOptions: { project: path.resolve(projectRoot, 'server/tsconfig.json'), diff --git a/package.json b/package.json index 09b5eec2a725..7ffc5121477d 100644 --- a/package.json +++ b/package.json @@ -72,9 +72,9 @@ "web:dist": "concurrently \"npm:web-proxy\" \"http-server ./dist --cors --port 8080 -P http://localhost:9000\"", "octokit": "cd scripts && npx ts-node -i -e \"$(cat ./octokit.ts)\"", "compress-svg": "npx ts-node scripts/compressSvg.ts --dir assets/images && npx ts-node scripts/compressSvg.ts --dir docs/assets/images", - "server:vcr:dev": "bun --conditions=react-native run server/victory-chart-renderer/src/cli.ts", - "server:vcr:build:linux": "bun build --conditions=react-native --compile --target=bun-linux-x64 server/victory-chart-renderer/src/cli.ts --outfile server/victory-chart-renderer/dist/victory-chart-renderer-linux-x64", - "server:vcr:build:macos": "bun build --conditions=react-native --compile --target=bun-darwin-arm64 server/victory-chart-renderer/src/cli.ts --outfile server/victory-chart-renderer/dist/victory-chart-renderer-darwin-arm64" + "server:vcr:dev": "bun --conditions=react-native run server/victory-chart-renderer/src/cli.tsx", + "server:vcr:build:linux": "bun build --conditions=react-native --compile --target=bun-linux-x64 server/victory-chart-renderer/src/cli.tsx --outfile server/victory-chart-renderer/dist/victory-chart-renderer-linux-x64", + "server:vcr:build:macos": "bun build --conditions=react-native --compile --target=bun-darwin-arm64 server/victory-chart-renderer/src/cli.tsx --outfile server/victory-chart-renderer/dist/victory-chart-renderer-darwin-arm64" }, "dependencies": { "@babel/runtime": "^7.25.0", diff --git a/server/victory-chart-renderer/__tests__/__golden__/smoke.png b/server/victory-chart-renderer/__tests__/__golden__/smoke.png new file mode 100644 index 0000000000000000000000000000000000000000..39916a7162d5dfffe98ded4f80e17b54165f2d87 GIT binary patch literal 1951 zcmeAS@N?(olHy`uVBq!ia0y~yV4MJC|KeZ+lKsmCU4Rs0age(c!@6@aFM%AEVkgfK z4j`!ENak)|-@V@(tzEUNa`Rrc z_tWmBMunDs&a39Vr?dC;mQ}Bw{rx62M{}?D^fWJhh69s59T+?W1sH-@SQCbM5KJA$(z#ylfz_5gagF%yt$)T%( zVS*AtuFs=d1)q)S^P(bW%$(WFy!q>=?DcS=r@LnZ&z!IQ{2_b&o`+H* zA5PlekGyjys<-;P**w|f8@^c06%?rY`X{`;{Q4Oi(td`|ueG`T&md8M z|A#Oam!7=8nNM%s{~u8`_uLP49TI{R8vax6{kQ(UD(~LA>SM}lwj9qFzn7e~Hve9Z z*L#NQqjSHmUO#P*c(40mzjS|~U!JQOJT45A>);gLvU6)x73&RSvu$~EJ353}!z&&! z71c2rmu(UfwD0Ti7W~-WRPD3&=D|b$N{>8V+9v4Vu($Y8=u#6=)xYkG+=O-VGs_mQ znr)ox@Gq*UKl9rs?(X}d0zc;e-03V|JM*3EJ{NW(k_Aa=2)|-T3gZT)u%q89R(+5Q z&VKuD>-#D97{7Jh&YtcUURu27%Ab!J6{}8v{wg=2!@{yr@^)0kuZlfCzRq`(o^3y2acbM_3J*Y@3=qp z-eT>&>?xo`TYb#0PsjHA;h@coa@*=Z-07Y4{m8nXLb60=5By#uN_L9ledblWEN_1A znVXYS`>ST$|or7fA5`aq3K?v|!;SndgNU~dg3Ln@WAeWh=(h8zn8R*jQ)ST6BP7i>3?6XR(;?3 z_~M4kH$nbA=L_^NFpWzpYdFx}V zR@(je8}yjb?)kS{OD~7smC-h>w~?Cn3c0xKnR|v$4Cvs1l^+@A7pMQt%E?Fv1hex!Qo^qE2Fw_ksV ztCpRyXZx+{hd(=ZL{!ZLW%!R75pSl4UOk)i`(xTk+hviV=EraMUyp4zTD9w^ { - const tempDir = mkdtempSync(join(tmpdir(), 'vcr-cli-test-')); - const outPath = join(tempDir, 'out.txt'); +const EXPECTED_WIDTH = 400; +const EXPECTED_HEIGHT = 250; + +// Set `UPDATE_GOLDEN=1` while running the test to refresh the reference PNG. +// Intentionally separate from the comparison flow so a regression can't quietly +// rewrite the golden. +const UPDATE_GOLDEN = process.env.UPDATE_GOLDEN === '1'; + +test('CLI renders a chart whose PNG matches the golden reference', () => { + const tempDir = mkdtempSync(join(tmpdir(), 'vcr-smoke-')); + const actualPath = join(tempDir, 'smoke-actual.png'); try { - const result = spawnSync('bun', ['run', cliPath, outPath], { + const result = spawnSync('bun', ['--conditions=react-native', 'run', cliPath, actualPath], { cwd: projectRoot, encoding: 'utf8', }); expect(result.status).toBe(0); - expect(readFileSync(outPath, 'utf8')).toBe('victory-chart-renderer scaffolded\n'); + + if (UPDATE_GOLDEN) { + copyFileSync(actualPath, goldenPath); + } + + const actual = PNG.sync.read(readFileSync(actualPath)); + const golden = PNG.sync.read(readFileSync(goldenPath)); + + expect(actual.width).toBe(EXPECTED_WIDTH); + expect(actual.height).toBe(EXPECTED_HEIGHT); + expect(golden.width).toBe(EXPECTED_WIDTH); + expect(golden.height).toBe(EXPECTED_HEIGHT); + + const diff = new PNG({width: EXPECTED_WIDTH, height: EXPECTED_HEIGHT}); + const mismatchedPixels = pixelmatch(actual.data, golden.data, diff.data, EXPECTED_WIDTH, EXPECTED_HEIGHT, { + threshold: 0.1, + }); + + // Allow a tiny number of antialiasing-driven pixel differences without making the test brittle. + const maxAllowedMismatch = Math.floor(EXPECTED_WIDTH * EXPECTED_HEIGHT * 0.001); + expect(mismatchedPixels).toBeLessThanOrEqual(maxAllowedMismatch); } finally { rmSync(tempDir, {recursive: true, force: true}); } }); + +// Sanity-check that the golden lives where we expect; this keeps an +// accidentally-deleted reference from looking like a passing test. +test('golden reference PNG exists on disk', () => { + expect(() => readFileSync(goldenPath)).not.toThrow(); +}); diff --git a/server/victory-chart-renderer/src/cli.ts b/server/victory-chart-renderer/src/cli.tsx similarity index 52% rename from server/victory-chart-renderer/src/cli.ts rename to server/victory-chart-renderer/src/cli.tsx index 196e9a18b6b1..7ab39b846f39 100644 --- a/server/victory-chart-renderer/src/cli.ts +++ b/server/victory-chart-renderer/src/cli.tsx @@ -1,7 +1,6 @@ -/* eslint-disable no-restricted-imports */ -import {LoadSkiaWeb} from '@shopify/react-native-skia/lib/module/web/LoadSkiaWeb'; -import {JsiSkApi} from '@shopify/react-native-skia/lib/module/skia/web'; import {drawOffscreen, makeOffscreenSurface} from '@shopify/react-native-skia/lib/module/headless'; +import {JsiSkApi} from '@shopify/react-native-skia/lib/module/skia/web'; +import {LoadSkiaWeb} from '@shopify/react-native-skia/lib/module/web/LoadSkiaWeb'; import CLI from '@scripts/utils/CLI'; const cli = new CLI({ @@ -19,43 +18,43 @@ await LoadSkiaWeb(); // module load time from `globalThis.SkiaApi` (the native bridge on iOS/Android). // Off-device we have to seed it ourselves before anything imports `victory-native`, // which calls `Skia.Path.Make()` from inside `useBarPath`. -(globalThis as unknown as {SkiaApi: ReturnType}).SkiaApi = JsiSkApi( - (globalThis as unknown as {CanvasKit: unknown}).CanvasKit, -); +const globalScope = globalThis as unknown as {CanvasKit: Parameters[0]; SkiaApi: ReturnType}; +globalScope.SkiaApi = JsiSkApi(globalScope.CanvasKit); + +const {Bar, CartesianChart} = await import('victory-native'); -const [React, victoryNative] = await Promise.all([import('react'), import('victory-native')]); -const {createElement} = React; -const {Bar, CartesianChart} = victoryNative; +type ChartDatum = {x: number; y: number}; const width = 400; const height = 250; -const data: Array<{x: number; y: number}> = [ +const data: ChartDatum[] = [ {x: 1, y: 60}, {x: 2, y: 90}, ]; -const chartElement = createElement( - CartesianChart<(typeof data)[number], 'x', 'y'>, - { - data, - xKey: 'x', - yKeys: ['y'], - domain: {x: [0, 3], y: [0, 100]}, - domainPadding: {left: 40, right: 40, top: 20, bottom: 20}, - padding: 20, - explicitSize: {width, height}, - headless: true, - xAxis: {lineColor: '#111827', lineWidth: 2}, - yAxis: [{yKeys: ['y'], lineColor: '#111827', lineWidth: 2}], - }, - ({points, chartBounds}) => - createElement(Bar, { - points: points.y, - chartBounds, - color: '#4f46e5', - innerPadding: 0.45, - }), +const chartElement = ( + + data={data} + xKey="x" + yKeys={['y']} + domain={{x: [0, 3], y: [0, 100]}} + domainPadding={{left: 40, right: 40, top: 20, bottom: 20}} + padding={20} + explicitSize={{width, height}} + headless + xAxis={{lineColor: '#111827', lineWidth: 2}} + yAxis={[{yKeys: ['y'], lineColor: '#111827', lineWidth: 2}]} + > + {({points, chartBounds}) => ( + + )} + ); using surface = makeOffscreenSurface(width, height); diff --git a/server/victory-chart-renderer/stubs/react-native-gesture-handler.ts b/server/victory-chart-renderer/stubs/react-native-gesture-handler.ts index 99e666b1cc57..fc6d63e1893f 100644 --- a/server/victory-chart-renderer/stubs/react-native-gesture-handler.ts +++ b/server/victory-chart-renderer/stubs/react-native-gesture-handler.ts @@ -5,7 +5,6 @@ // gesture is ever attached. We expose a chainable builder so those calls // succeed; the headless branch of `CartesianChart` early-returns before any // gesture handlers would actually be attached. - import type {FunctionComponent, PropsWithChildren} from 'react'; class GestureBuilder { @@ -76,25 +75,28 @@ class GestureBuilder { const buildGesture = (): GestureBuilder => new GestureBuilder(); -export const Gesture = { +const Gesture = { Pan: buildGesture, Pinch: buildGesture, Rotation: buildGesture, Tap: buildGesture, LongPress: buildGesture, Manual: buildGesture, - Race: (..._gestures: unknown[]) => buildGesture(), - Simultaneous: (..._gestures: unknown[]) => buildGesture(), - Exclusive: (..._gestures: unknown[]) => buildGesture(), + Race: () => buildGesture(), + Simultaneous: () => buildGesture(), + Exclusive: () => buildGesture(), }; const noopComponent: FunctionComponent> = () => null; -export const GestureHandlerRootView = noopComponent; -export const GestureDetector = noopComponent; +const GestureHandlerRootView = noopComponent; +const GestureDetector = noopComponent; + +type ComposedGesture = GestureBuilder; +type GestureType = GestureBuilder; +type PanGesture = GestureBuilder; +type PinchGesture = GestureBuilder; +type TouchData = {id: number; x: number; y: number; absoluteX: number; absoluteY: number}; -export type ComposedGesture = GestureBuilder; -export type GestureType = GestureBuilder; -export type PanGesture = GestureBuilder; -export type PinchGesture = GestureBuilder; -export type TouchData = {id: number; x: number; y: number; absoluteX: number; absoluteY: number}; +export type {ComposedGesture, GestureType, PanGesture, PinchGesture, TouchData}; +export {Gesture, GestureHandlerRootView, GestureDetector}; diff --git a/server/victory-chart-renderer/stubs/react-native-reanimated.js b/server/victory-chart-renderer/stubs/react-native-reanimated.js index 316b6f770c80..3ee122c66df2 100644 --- a/server/victory-chart-renderer/stubs/react-native-reanimated.js +++ b/server/victory-chart-renderer/stubs/react-native-reanimated.js @@ -13,7 +13,6 @@ // // This file is CommonJS so that Bun returns the proxy directly from // `require("react-native-reanimated")`. -'use strict'; const noopFn = () => undefined; @@ -21,7 +20,7 @@ const noopComponent = () => null; const sharedValue = (initialValue) => ({value: initialValue}); -const exports_ = { +const namedExports = { __esModule: true, default: undefined, useSharedValue: sharedValue, @@ -35,7 +34,7 @@ const exports_ = { withTiming: (value) => value, withSpring: (value) => value, withDecay: noopFn, - withDelay: (_delay, animation) => animation, + withDelay: (delay, animation) => animation, withRepeat: (animation) => animation, cancelAnimation: noopFn, Easing: { @@ -50,7 +49,7 @@ const exports_ = { ScrollView: noopComponent, }; -const proxy = new Proxy(exports_, { +const proxy = new Proxy(namedExports, { get(target, prop) { if (prop in target) { return target[prop]; @@ -61,6 +60,6 @@ const proxy = new Proxy(exports_, { // Match Reanimated's runtime shape: `import Rea from "react-native-reanimated"` // returns the same proxy as `import * as Rea`. -exports_.default = proxy; +namedExports.default = proxy; module.exports = proxy; diff --git a/server/victory-chart-renderer/stubs/react-native-subpath.ts b/server/victory-chart-renderer/stubs/react-native-subpath.ts index 2628d084bcea..0eda28658b0f 100644 --- a/server/victory-chart-renderer/stubs/react-native-subpath.ts +++ b/server/victory-chart-renderer/stubs/react-native-subpath.ts @@ -4,9 +4,11 @@ // the Bun headless renderer, but their import statements still run, so this // stub provides a no-op default plus a few of the most common named exports. -const noopFunction = (..._args: unknown[]) => null; +const noopFunction = () => null; +const codegenNativeComponent = noopFunction; +const registerAsset = (asset: unknown) => asset; +const getAssetByID = () => null; + +export {codegenNativeComponent, registerAsset, getAssetByID}; export default noopFunction; -export const codegenNativeComponent = noopFunction; -export const registerAsset = (asset: unknown) => asset; -export const getAssetByID = () => null; diff --git a/server/victory-chart-renderer/stubs/react-native.ts b/server/victory-chart-renderer/stubs/react-native.ts index 9da326cc8c50..12840f238608 100644 --- a/server/victory-chart-renderer/stubs/react-native.ts +++ b/server/victory-chart-renderer/stubs/react-native.ts @@ -8,20 +8,19 @@ // // Anything that escapes into the render tree (e.g. ``) renders to `null` // so it cannot affect the Skia output. - import type {FunctionComponent, PropsWithChildren} from 'react'; const noopComponent: FunctionComponent> = () => null; -export type LayoutChangeEvent = {nativeEvent: {layout: {x: number; y: number; width: number; height: number}}}; -export type ViewStyle = Record; -export type ImageStyle = Record; -export type TextStyle = Record; -export type StyleProp = T | T[] | null | undefined | false; -export type TransformsStyle = Record; +type LayoutChangeEvent = {nativeEvent: {layout: {x: number; y: number; width: number; height: number}}}; +type ViewStyle = Record; +type ImageStyle = Record; +type TextStyle = Record; +type StyleProp = T | T[] | null | undefined | false; +type TransformsStyle = Record; -export const View = noopComponent; -export const Image = Object.assign(noopComponent, { +const View = noopComponent; +const Image = Object.assign(noopComponent, { resolveAssetSource: (source: unknown) => { if (source && typeof source === 'object' && 'uri' in (source as Record)) { return source as {uri: string}; @@ -29,37 +28,40 @@ export const Image = Object.assign(noopComponent, { return {uri: String(source)}; }, }); -export const Text = noopComponent; +const Text = noopComponent; -export const Platform = { +const Platform = { OS: 'web' as const, Version: 0, - select: (specifics: {default?: T; web?: T}) => specifics.web ?? specifics.default, + select: (specifics: {default?: T; web?: T}) => specifics.web ?? specifics.default, }; -export const PixelRatio = { +const PixelRatio = { get: () => 1, getFontScale: () => 1, getPixelSizeForLayoutSize: (size: number) => size, roundToNearestPixel: (size: number) => size, }; -export const StyleSheet = { - create: (styles: T): T => styles, - flatten: (style: T): T => style, - compose: (a: T, _b: T): T => a, +const StyleSheet = { + create: (styles: T): T => styles, + flatten: (style: T): T => style, + compose: (a: T): T => a, hairlineWidth: 1, absoluteFill: {}, absoluteFillObject: {}, }; -export const findNodeHandle = () => null; +const findNodeHandle = () => null; // `@shopify/react-native-skia/lib/module/specs/NativeSkiaModule` does // `TurboModuleRegistry.getEnforcing("RNSkiaModule")` at module load. The // `Platform.OS === "web"` short-circuit in NativeSetup then ignores the // missing module, but we still need the named export to exist. -export const TurboModuleRegistry = { - get: (_name: string): null => null, - getEnforcing: (_name: string): null => null, +const TurboModuleRegistry = { + get: (): null => null, + getEnforcing: (): null => null, }; + +export type {LayoutChangeEvent, ViewStyle, ImageStyle, TextStyle, StyleProp, TransformsStyle}; +export {View, Image, Text, Platform, PixelRatio, StyleSheet, findNodeHandle, TurboModuleRegistry}; From 7abe629bc294846647502dd75b480c120f9b80a9 Mon Sep 17 00:00:00 2001 From: rory Date: Thu, 28 May 2026 09:23:44 -0700 Subject: [PATCH 04/21] Move VCR to npm workspace with stub packages Replace patch-package tsconfig stubs with @expensify/victory-chart-renderer workspace: file: RN stub packages, root server:vcr:* delegates via -w, and Bun bundler plugin for dev/compile because victory-native hoists to root. Co-authored-by: Cursor --- config/eslint/eslint.config.mjs | 2 +- package-lock.json | 59 +++++++++++++++++-- package.json | 21 +++---- ...hopify+react-native-skia+2.4.14.details.md | 21 ------- .../@shopify+react-native-skia+2.4.14.patch | 25 -------- patches/victory-native/details.md | 17 ------ ...victory-native+41.21.0+002+bun-stubs.patch | 22 ------- server/tsconfig.json | 26 -------- server/victory-chart-renderer/.gitignore | 3 + server/victory-chart-renderer/README.md | 21 +++++-- server/victory-chart-renderer/package.json | 39 ++++++++++++ .../victory-chart-renderer/scripts/build.ts | 30 ++++++++++ server/victory-chart-renderer/scripts/dev.ts | 41 +++++++++++++ .../scripts/rnStubPlugin.ts | 30 ++++++++++ .../index.ts} | 0 .../react-native-gesture-handler/package.json | 7 +++ .../index.js} | 0 .../react-native-reanimated/package.json | 6 ++ .../index.ts} | 0 .../stubs/react-native/package.json | 11 ++++ .../subpath.ts} | 0 .../victory-chart-renderer/tests/cli.test.ts | 8 +-- server/victory-chart-renderer/tsconfig.json | 10 ++++ 23 files changed, 259 insertions(+), 140 deletions(-) delete mode 100644 patches/@shopify+react-native-skia+2.4.14.details.md delete mode 100644 patches/@shopify+react-native-skia+2.4.14.patch delete mode 100644 patches/victory-native/victory-native+41.21.0+002+bun-stubs.patch delete mode 100644 server/tsconfig.json create mode 100644 server/victory-chart-renderer/.gitignore create mode 100644 server/victory-chart-renderer/package.json create mode 100644 server/victory-chart-renderer/scripts/build.ts create mode 100644 server/victory-chart-renderer/scripts/dev.ts create mode 100644 server/victory-chart-renderer/scripts/rnStubPlugin.ts rename server/victory-chart-renderer/stubs/{react-native-gesture-handler.ts => react-native-gesture-handler/index.ts} (100%) create mode 100644 server/victory-chart-renderer/stubs/react-native-gesture-handler/package.json rename server/victory-chart-renderer/stubs/{react-native-reanimated.js => react-native-reanimated/index.js} (100%) create mode 100644 server/victory-chart-renderer/stubs/react-native-reanimated/package.json rename server/victory-chart-renderer/stubs/{react-native.ts => react-native/index.ts} (100%) create mode 100644 server/victory-chart-renderer/stubs/react-native/package.json rename server/victory-chart-renderer/stubs/{react-native-subpath.ts => react-native/subpath.ts} (100%) create mode 100644 server/victory-chart-renderer/tsconfig.json diff --git a/config/eslint/eslint.config.mjs b/config/eslint/eslint.config.mjs index b8edb82f57a8..b7ca91c8f671 100644 --- a/config/eslint/eslint.config.mjs +++ b/config/eslint/eslint.config.mjs @@ -739,7 +739,7 @@ const config = defineConfig([ files: ['server/**/*.ts', 'server/**/*.tsx'], languageOptions: { parserOptions: { - project: path.resolve(projectRoot, 'server/tsconfig.json'), + project: path.resolve(projectRoot, 'server/victory-chart-renderer/tsconfig.json'), }, }, }, diff --git a/package-lock.json b/package-lock.json index 34384f0566f7..ddba731701ef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,9 @@ "version": "9.3.84-0", "hasInstallScript": true, "license": "MIT", + "workspaces": [ + "server/victory-chart-renderer" + ], "dependencies": { "@babel/runtime": "^7.25.0", "@dnd-kit/core": "^6.3.1", @@ -206,7 +209,6 @@ "@types/mime-db": "^1.43.5", "@types/node": "^20.11.5", "@types/pako": "^2.0.3", - "@types/pngjs": "^6.0.5", "@types/pusher-js": "^5.1.0", "@types/react": "^19.2.0", "@types/react-collapse": "^5.0.1", @@ -270,8 +272,6 @@ "openai": "^6.16.0", "patch-package": "^8.1.0-canary.1", "peggy": "^4.0.3", - "pixelmatch": "^7.2.0", - "pngjs": "^7.0.0", "portfinder": "^1.0.34", "prettier": "3.7.4", "react-native-clean-project": "^4.0.0-alpha4.0", @@ -5670,6 +5670,10 @@ "react-native": "*" } }, + "node_modules/@expensify/victory-chart-renderer": { + "resolved": "server/victory-chart-renderer", + "link": true + }, "node_modules/@expo/code-signing-certificates": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/@expo/code-signing-certificates/-/code-signing-certificates-0.0.6.tgz", @@ -22803,9 +22807,9 @@ }, "node_modules/eslint-config-airbnb-typescript/node_modules/eslint-config-airbnb-base": { "name": "eslint-config-expensify", - "version": "2.0.89", - "resolved": "https://registry.npmjs.org/eslint-config-expensify/-/eslint-config-expensify-2.0.89.tgz", - "integrity": "sha512-fI57deNANpTwvq8LMKet7M/D1zGAnzzmsOsQ9m55Psun6MRK9lj1hhFZhV0Q1GGfC8la6adAoGJ7aQz96aWShg==", + "version": "2.0.91", + "resolved": "https://registry.npmjs.org/eslint-config-expensify/-/eslint-config-expensify-2.0.91.tgz", + "integrity": "sha512-sdVa7o7Cl9JIV2UiENQTue9nlt8sc45B7SyiKdtDizajY7tMK402PEa9ngPIkRt/0xzKOFC+jtPy8pMXg6OSeg==", "dev": true, "license": "ISC", "dependencies": { @@ -22822,6 +22826,7 @@ "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-rulesdir": "^0.2.2", + "globals": "^15.14.0", "lodash": "^4.17.21", "underscore": "^1.13.6" } @@ -40584,6 +40589,48 @@ "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } + }, + "server/victory-chart-renderer": { + "name": "@expensify/victory-chart-renderer", + "version": "0.0.0", + "dependencies": { + "@shopify/react-native-skia": "^2.4.14", + "react": "19.2.0", + "react-native": "file:./stubs/react-native", + "react-native-gesture-handler": "file:./stubs/react-native-gesture-handler", + "react-native-reanimated": "file:./stubs/react-native-reanimated", + "victory-native": "^41.21.0" + }, + "devDependencies": { + "@types/bun": "1.3.14", + "@types/pngjs": "^6.0.5", + "pixelmatch": "^7.2.0", + "pngjs": "^7.0.0" + }, + "engines": { + "bun": "1.3.14" + } + }, + "server/victory-chart-renderer/node_modules/react-native": { + "resolved": "server/victory-chart-renderer/stubs/react-native", + "link": true + }, + "server/victory-chart-renderer/node_modules/react-native-gesture-handler": { + "resolved": "server/victory-chart-renderer/stubs/react-native-gesture-handler", + "link": true + }, + "server/victory-chart-renderer/node_modules/react-native-reanimated": { + "resolved": "server/victory-chart-renderer/stubs/react-native-reanimated", + "link": true + }, + "server/victory-chart-renderer/stubs/react-native": { + "version": "0.0.0-stub" + }, + "server/victory-chart-renderer/stubs/react-native-gesture-handler": { + "version": "0.0.0-stub" + }, + "server/victory-chart-renderer/stubs/react-native-reanimated": { + "version": "0.0.0-stub" } } } diff --git a/package.json b/package.json index 6204a241895a..e706b9b89973 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", "license": "MIT", "private": true, + "workspaces": ["server/victory-chart-renderer"], "scripts": { "i-standalone": "STANDALONE_NEW_DOT=true npm i", "install-standalone": "STANDALONE_NEW_DOT=true npm install", @@ -43,8 +44,8 @@ "test:verbose": "TZ=utc NODE_OPTIONS=\"--experimental-vm-modules --max_old_space_size=4096\" JEST_VERBOSE=true jest", "test:debug": "TZ=utc NODE_OPTIONS='--inspect-brk --experimental-vm-modules' jest --runInBand", "perf-test": "NODE_OPTIONS=--experimental-vm-modules npx reassure", - "typecheck": "NODE_OPTIONS=--max_old_space_size=8192 tsc && tsc -p server/tsconfig.json", - "typecheck-tsgo": "tsgo --noEmit --incremental --tsBuildInfoFile tsconfig.tsgo.tsbuildinfo && tsgo --noEmit -p server/tsconfig.json --incremental --tsBuildInfoFile server/tsconfig.tsgo.tsbuildinfo", + "typecheck": "NODE_OPTIONS=--max_old_space_size=8192 tsc && tsc -p server/victory-chart-renderer/tsconfig.json", + "typecheck-tsgo": "tsgo --noEmit --incremental --tsBuildInfoFile tsconfig.tsgo.tsbuildinfo && tsgo --noEmit -p server/victory-chart-renderer/tsconfig.json --incremental --tsBuildInfoFile server/victory-chart-renderer/tsconfig.tsgo.tsbuildinfo", "lint": "./scripts/lint.sh", "lint-changed": "./scripts/lintChanged.sh", "lint-watch": "onchange '**/*.{js,jsx,ts,tsx,mjs,cjs}' -- ./scripts/lint.sh {{changed}}", @@ -72,11 +73,11 @@ "web:dist": "concurrently \"npm:web-proxy\" \"http-server ./dist --cors --port 8080 -P http://localhost:9000\"", "octokit": "cd scripts && npx ts-node -i -e \"$(cat ./octokit.ts)\"", "compress-svg": "npx ts-node scripts/compressSvg.ts --dir assets/images && npx ts-node scripts/compressSvg.ts --dir docs/assets/images", - "server:vcr:dev": "bun --conditions=react-native run server/victory-chart-renderer/src/cli.tsx", - "server:vcr:test": "bun test server/victory-chart-renderer/tests", - "server:vcr:build:linux": "bun build --conditions=react-native --compile --target=bun-linux-x64 server/victory-chart-renderer/src/cli.tsx --outfile server/victory-chart-renderer/dist/victory-chart-renderer-linux-x64", - "server:vcr:build:linux-arm": "bun build --conditions=react-native --compile --target=bun-linux-arm64 server/victory-chart-renderer/src/cli.tsx --outfile server/victory-chart-renderer/dist/victory-chart-renderer-linux-arm64", - "server:vcr:build:macos": "bun build --conditions=react-native --compile --target=bun-darwin-arm64 server/victory-chart-renderer/src/cli.tsx --outfile server/victory-chart-renderer/dist/victory-chart-renderer-darwin-arm64" + "server:vcr:dev": "npm run dev -w @expensify/victory-chart-renderer", + "server:vcr:test": "npm run test -w @expensify/victory-chart-renderer", + "server:vcr:build:linux": "npm run build:linux -w @expensify/victory-chart-renderer", + "server:vcr:build:linux-arm": "npm run build:linux-arm -w @expensify/victory-chart-renderer", + "server:vcr:build:macos": "npm run build:macos -w @expensify/victory-chart-renderer" }, "dependencies": { "@babel/runtime": "^7.25.0", @@ -275,7 +276,6 @@ "@types/mime-db": "^1.43.5", "@types/node": "^20.11.5", "@types/pako": "^2.0.3", - "@types/pngjs": "^6.0.5", "@types/pusher-js": "^5.1.0", "@types/react": "^19.2.0", "@types/react-collapse": "^5.0.1", @@ -339,8 +339,6 @@ "openai": "^6.16.0", "patch-package": "^8.1.0-canary.1", "peggy": "^4.0.3", - "pixelmatch": "^7.2.0", - "pngjs": "^7.0.0", "portfinder": "^1.0.34", "prettier": "3.7.4", "react-native-clean-project": "^4.0.0-alpha4.0", @@ -426,9 +424,6 @@ "expo": "55.0.25" } }, - "trustedDependencies": [ - "@shopify/react-native-skia" - ], "expo": { "autolinking": { "exclude": [ diff --git a/patches/@shopify+react-native-skia+2.4.14.details.md b/patches/@shopify+react-native-skia+2.4.14.details.md deleted file mode 100644 index e8a4656a94f2..000000000000 --- a/patches/@shopify+react-native-skia+2.4.14.details.md +++ /dev/null @@ -1,21 +0,0 @@ -# @shopify/react-native-skia patch: local tsconfig for Bun headless renderer - -## Why - -The `server/victory-chart-renderer` Bun CLI renders Skia content without React -Native. Bun (and any other non-RN consumer) loads `@shopify/react-native-skia`'s -root entry, which transitively imports `react-native`, -`react-native-reanimated`, and `react-native/Libraries/...` subpaths. None of -those packages run in Bun. - -This patch drops a minimal `tsconfig.json` inside the installed package so that -when Bun resolves imports from files in `node_modules/@shopify/react-native-skia/...` -it walks up to this `tsconfig.json` and remaps the React Native dependencies to -the stubs in `server/victory-chart-renderer/stubs/`. Metro doesn't consult -`tsconfig.json` paths, so the in-app build is unaffected. - -## Remove when - -We replace the Bun renderer with a different mechanism, or the Skia package -ships a `bun` export condition (or equivalent) that lets us load a stripped -entry without React Native dependencies. diff --git a/patches/@shopify+react-native-skia+2.4.14.patch b/patches/@shopify+react-native-skia+2.4.14.patch deleted file mode 100644 index 0b5f8bc62178..000000000000 --- a/patches/@shopify+react-native-skia+2.4.14.patch +++ /dev/null @@ -1,25 +0,0 @@ -diff --git a/node_modules/@shopify/react-native-skia/tsconfig.json b/node_modules/@shopify/react-native-skia/tsconfig.json -new file mode 100644 -index 0000000..50f2975 ---- /dev/null -+++ b/node_modules/@shopify/react-native-skia/tsconfig.json -@@ -0,0 +1,19 @@ -+{ -+ "compilerOptions": { -+ "baseUrl": ".", -+ "paths": { -+ "react-native": [ -+ "../../../server/victory-chart-renderer/stubs/react-native.ts" -+ ], -+ "react-native/*": [ -+ "../../../server/victory-chart-renderer/stubs/react-native-subpath.ts" -+ ], -+ "react-native-reanimated": [ -+ "../../../server/victory-chart-renderer/stubs/react-native-reanimated.js" -+ ], -+ "react-native-gesture-handler": [ -+ "../../../server/victory-chart-renderer/stubs/react-native-gesture-handler.ts" -+ ] -+ } -+ } -+} diff --git a/patches/victory-native/details.md b/patches/victory-native/details.md index 7aeb2d68c71f..a2a6fcc031f6 100644 --- a/patches/victory-native/details.md +++ b/patches/victory-native/details.md @@ -1,22 +1,5 @@ # `victory-native` patches -### [victory-native+41.21.0+002+bun-stubs.patch](victory-native+41.21.0+002+bun-stubs.patch) - -- Reason: - - ``` - Drops a tsconfig.json into the installed package so Bun's resolver can remap - react-native, react-native-reanimated, and react-native-gesture-handler to the - stubs in server/victory-chart-renderer/stubs/ when loading victory-native for the - headless chart renderer CLI. Metro ignores tsconfig paths, so the in-app build is - unaffected. - ``` - -- Upstream PR/issue: N/A — Bun-specific wiring for the victory-chart-renderer CLI. -- E/App issue: https://github.com/Expensify/Expensify/issues/91528 -- PR introducing patch: https://github.com/Expensify/App/pull/91672 -- Remove when: We replace the Bun renderer or victory-native ships a bun export condition. - ### [victory-native+41.21.0+001+horizontal-bars.patch](victory-native+41.21.0+001+horizontal-bars.patch) - Reason: diff --git a/patches/victory-native/victory-native+41.21.0+002+bun-stubs.patch b/patches/victory-native/victory-native+41.21.0+002+bun-stubs.patch deleted file mode 100644 index 569a1477a4d2..000000000000 --- a/patches/victory-native/victory-native+41.21.0+002+bun-stubs.patch +++ /dev/null @@ -1,22 +0,0 @@ -diff --git a/node_modules/victory-native/tsconfig.json b/node_modules/victory-native/tsconfig.json -new file mode 100644 -index 0000000..0fff5ee ---- /dev/null -+++ b/node_modules/victory-native/tsconfig.json -@@ -0,0 +1,16 @@ -+{ -+ "compilerOptions": { -+ "baseUrl": ".", -+ "paths": { -+ "react-native": [ -+ "../../server/victory-chart-renderer/stubs/react-native.ts" -+ ], -+ "react-native-reanimated": [ -+ "../../server/victory-chart-renderer/stubs/react-native-reanimated.js" -+ ], -+ "react-native-gesture-handler": [ -+ "../../server/victory-chart-renderer/stubs/react-native-gesture-handler.ts" -+ ] -+ } -+ } -+} diff --git a/server/tsconfig.json b/server/tsconfig.json deleted file mode 100644 index d4e1bf66228b..000000000000 --- a/server/tsconfig.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "extends": "../tsconfig.json", - "compilerOptions": { - "types": ["@types/bun"], - "paths": { - "react-native": ["./victory-chart-renderer/stubs/react-native.ts"], - "react-native-reanimated": ["./victory-chart-renderer/stubs/react-native-reanimated.js"], - "react-native-gesture-handler": ["./victory-chart-renderer/stubs/react-native-gesture-handler.ts"], - "@assets/*": ["../assets/*"], - "@components/*": ["../src/components/*"], - "@github/*": ["../.github/*"], - "@hooks/*": ["../src/hooks/*"], - "@libs/*": ["../src/libs/*"], - "@navigation/*": ["../src/libs/Navigation/*"], - "@pages/*": ["../src/pages/*"], - "@prompts/*": ["../prompts/*"], - "@scripts/*": ["../scripts/*", "../.github/scripts/*"], - "@styles/*": ["../src/styles/*"], - "@src/*": ["../src/*"], - "@userActions/*": ["../src/libs/actions/*"], - "@selectors/*": ["../src/selectors/*"], - "tests/*": ["../tests/*"] - } - }, - "include": ["victory-chart-renderer/src", "victory-chart-renderer/tests", "victory-chart-renderer/stubs"] -} diff --git a/server/victory-chart-renderer/.gitignore b/server/victory-chart-renderer/.gitignore new file mode 100644 index 000000000000..5547cab15cef --- /dev/null +++ b/server/victory-chart-renderer/.gitignore @@ -0,0 +1,3 @@ +.dev/ +dist/ +bun.lock diff --git a/server/victory-chart-renderer/README.md b/server/victory-chart-renderer/README.md index 001455cf30a6..edeabf445785 100644 --- a/server/victory-chart-renderer/README.md +++ b/server/victory-chart-renderer/README.md @@ -1,10 +1,14 @@ # Victory Chart Renderer -Standalone Bun CLI that will render Expensify chart XML to PNG using the same chart code as the App. +Standalone Bun CLI (`@expensify/victory-chart-renderer`) that renders Expensify charts to PNG using the same `victory-native` and Skia code paths as the App. + +This package is an [npm workspace](https://docs.npmjs.com/cli/using-npm/workspaces) child of the App root. React Native peer dependencies are declared as local `file:` stub packages under `stubs/` (with `overrides` + `installConfig.hoistingLimits` so they stay in this workspace). At runtime, `scripts/dev.ts` and `scripts/build.ts` bundle the CLI with a Bun plugin that redirects `react-native`, `react-native-reanimated`, `react-native-gesture-handler`, and `react-native/*` imports to those stubs, because hoisted `victory-native` at the repo root would otherwise resolve the real native modules. The in-app Metro build is unaffected. ## Project layout - `src/` — CLI source +- `stubs/` — `file:` stub packages (`react-native`, `react-native-reanimated`, `react-native-gesture-handler`) used only in this workspace +- `scripts/` — Bun bundler entrypoints (`dev.ts`, `build.ts`, `rnStubPlugin.ts`) - `tests/` — Bun integration tests (`bun:test`) - `dist/` — compiled binaries (gitignored) @@ -16,6 +20,12 @@ From the App repository root: npm run server:vcr:dev /tmp/out.png ``` +Or from this directory: + +```bash +npm run dev /tmp/out.png +``` + ## Tests From the App repository root: @@ -27,20 +37,23 @@ npm run server:vcr:test Or from this directory: ```bash -bun test +npm test ``` To refresh the reference PNG after an intentional rendering change: ```bash -UPDATE_GOLDEN=1 bun test +UPDATE_GOLDEN=1 npm test ``` ## Compiled binaries +From the App repository root: + ```bash npm run server:vcr:build:linux +npm run server:vcr:build:linux-arm npm run server:vcr:build:macos ``` -Binaries are written to `server/victory-chart-renderer/dist/` (gitignored). +Binaries are written to `dist/` (gitignored). diff --git a/server/victory-chart-renderer/package.json b/server/victory-chart-renderer/package.json new file mode 100644 index 000000000000..730bbf121095 --- /dev/null +++ b/server/victory-chart-renderer/package.json @@ -0,0 +1,39 @@ +{ + "name": "@expensify/victory-chart-renderer", + "version": "0.0.0", + "private": true, + "description": "Headless Bun CLI that renders Expensify chart XML to PNG using victory-native and Skia.", + "scripts": { + "dev": "bun run scripts/dev.ts", + "test": "bun test", + "build:linux": "bun run scripts/build.ts bun-linux-x64 dist/victory-chart-renderer-linux-x64", + "build:linux-arm": "bun run scripts/build.ts bun-linux-arm64 dist/victory-chart-renderer-linux-arm64", + "build:macos": "bun run scripts/build.ts bun-darwin-arm64 dist/victory-chart-renderer-darwin-arm64" + }, + "dependencies": { + "@shopify/react-native-skia": "^2.4.14", + "react": "19.2.0", + "react-native": "file:./stubs/react-native", + "react-native-gesture-handler": "file:./stubs/react-native-gesture-handler", + "react-native-reanimated": "file:./stubs/react-native-reanimated", + "victory-native": "^41.21.0" + }, + "devDependencies": { + "@types/bun": "1.3.14", + "@types/pngjs": "^6.0.5", + "pixelmatch": "^7.2.0", + "pngjs": "^7.0.0" + }, + "overrides": { + "react-native": "file:./stubs/react-native", + "react-native-reanimated": "file:./stubs/react-native-reanimated", + "react-native-gesture-handler": "file:./stubs/react-native-gesture-handler" + }, + "installConfig": { + "hoistingLimits": "workspaces" + }, + "trustedDependencies": ["@shopify/react-native-skia"], + "engines": { + "bun": "1.3.14" + } +} diff --git a/server/victory-chart-renderer/scripts/build.ts b/server/victory-chart-renderer/scripts/build.ts new file mode 100644 index 000000000000..764382ad6d65 --- /dev/null +++ b/server/victory-chart-renderer/scripts/build.ts @@ -0,0 +1,30 @@ +import {join, resolve} from 'node:path'; +import {createRnStubPlugin} from './rnStubPlugin'; + +const packageRoot = resolve(import.meta.dir, '..'); +const stubRoot = resolve(packageRoot, 'stubs'); + +const target = process.argv[2]; +const outfile = process.argv[3]; + +if (!target || !outfile) { + throw new Error('Usage: bun run scripts/build.ts '); +} + +const buildResult = await Bun.build({ + entrypoints: [join(packageRoot, 'src/cli.tsx')], + compile: { + target, + outfile, + }, + packages: 'bundle', + conditions: ['react-native'], + plugins: [createRnStubPlugin(stubRoot)], +}); + +if (!buildResult.success) { + for (const log of buildResult.logs) { + console.error(log); + } + throw new Error(`Failed to compile victory-chart-renderer for ${target}`); +} diff --git a/server/victory-chart-renderer/scripts/dev.ts b/server/victory-chart-renderer/scripts/dev.ts new file mode 100644 index 000000000000..39d5af1ea930 --- /dev/null +++ b/server/victory-chart-renderer/scripts/dev.ts @@ -0,0 +1,41 @@ +import {spawnSync} from 'node:child_process'; +import {mkdirSync} from 'node:fs'; +import {join, resolve} from 'node:path'; +import {createRnStubPlugin} from './rnStubPlugin'; + +const packageRoot = resolve(import.meta.dir, '..'); +const repoRoot = resolve(packageRoot, '../..'); +const stubRoot = resolve(packageRoot, 'stubs'); +const outFile = resolve(packageRoot, '.dev/cli.js'); + +mkdirSync(join(packageRoot, '.dev'), {recursive: true}); + +const buildResult = await Bun.build({ + entrypoints: [resolve(packageRoot, 'src/cli.tsx')], + outfile: outFile, + target: 'bun', + packages: 'bundle', + conditions: ['react-native'], + plugins: [createRnStubPlugin(stubRoot)], +}); + +if (!buildResult.success) { + for (const log of buildResult.logs) { + console.error(log); + } + throw new Error('Failed to bundle victory-chart-renderer CLI'); +} + +const [bundle] = buildResult.outputs; +if (!bundle) { + throw new Error('Bundled CLI output is missing'); +} + +await Bun.write(outFile, bundle); + +const runResult = spawnSync(process.execPath, [outFile, ...process.argv.slice(2)], { + cwd: repoRoot, + stdio: 'inherit', +}); + +process.exit(runResult.status ?? 1); diff --git a/server/victory-chart-renderer/scripts/rnStubPlugin.ts b/server/victory-chart-renderer/scripts/rnStubPlugin.ts new file mode 100644 index 000000000000..f122cb30c61c --- /dev/null +++ b/server/victory-chart-renderer/scripts/rnStubPlugin.ts @@ -0,0 +1,30 @@ +import type {BunPlugin} from 'bun'; +import {resolve} from 'node:path'; + +export function createRnStubPlugin(stubRoot: string): BunPlugin { + const reactNativeStub = resolve(stubRoot, 'react-native/index.ts'); + const reactNativeSubpathStub = resolve(stubRoot, 'react-native/subpath.ts'); + const reanimatedStub = resolve(stubRoot, 'react-native-reanimated/index.js'); + const gestureHandlerStub = resolve(stubRoot, 'react-native-gesture-handler/index.ts'); + + return { + name: 'victory-chart-renderer-rn-stubs', + setup(build) { + build.onResolve({filter: /^react-native$/}, () => ({ + path: reactNativeStub, + })); + + build.onResolve({filter: /^react-native\/.+/}, () => ({ + path: reactNativeSubpathStub, + })); + + build.onResolve({filter: /^react-native-reanimated$/}, () => ({ + path: reanimatedStub, + })); + + build.onResolve({filter: /^react-native-gesture-handler$/}, () => ({ + path: gestureHandlerStub, + })); + }, + }; +} diff --git a/server/victory-chart-renderer/stubs/react-native-gesture-handler.ts b/server/victory-chart-renderer/stubs/react-native-gesture-handler/index.ts similarity index 100% rename from server/victory-chart-renderer/stubs/react-native-gesture-handler.ts rename to server/victory-chart-renderer/stubs/react-native-gesture-handler/index.ts diff --git a/server/victory-chart-renderer/stubs/react-native-gesture-handler/package.json b/server/victory-chart-renderer/stubs/react-native-gesture-handler/package.json new file mode 100644 index 000000000000..84bc99ed96dc --- /dev/null +++ b/server/victory-chart-renderer/stubs/react-native-gesture-handler/package.json @@ -0,0 +1,7 @@ +{ + "name": "react-native-gesture-handler", + "version": "0.0.0-stub", + "private": true, + "type": "module", + "main": "./index.ts" +} diff --git a/server/victory-chart-renderer/stubs/react-native-reanimated.js b/server/victory-chart-renderer/stubs/react-native-reanimated/index.js similarity index 100% rename from server/victory-chart-renderer/stubs/react-native-reanimated.js rename to server/victory-chart-renderer/stubs/react-native-reanimated/index.js diff --git a/server/victory-chart-renderer/stubs/react-native-reanimated/package.json b/server/victory-chart-renderer/stubs/react-native-reanimated/package.json new file mode 100644 index 000000000000..107271909f92 --- /dev/null +++ b/server/victory-chart-renderer/stubs/react-native-reanimated/package.json @@ -0,0 +1,6 @@ +{ + "name": "react-native-reanimated", + "version": "0.0.0-stub", + "private": true, + "main": "./index.js" +} diff --git a/server/victory-chart-renderer/stubs/react-native.ts b/server/victory-chart-renderer/stubs/react-native/index.ts similarity index 100% rename from server/victory-chart-renderer/stubs/react-native.ts rename to server/victory-chart-renderer/stubs/react-native/index.ts diff --git a/server/victory-chart-renderer/stubs/react-native/package.json b/server/victory-chart-renderer/stubs/react-native/package.json new file mode 100644 index 000000000000..5851456cbebb --- /dev/null +++ b/server/victory-chart-renderer/stubs/react-native/package.json @@ -0,0 +1,11 @@ +{ + "name": "react-native", + "version": "0.0.0-stub", + "private": true, + "type": "module", + "main": "./index.ts", + "exports": { + ".": "./index.ts", + "./*": "./subpath.ts" + } +} diff --git a/server/victory-chart-renderer/stubs/react-native-subpath.ts b/server/victory-chart-renderer/stubs/react-native/subpath.ts similarity index 100% rename from server/victory-chart-renderer/stubs/react-native-subpath.ts rename to server/victory-chart-renderer/stubs/react-native/subpath.ts diff --git a/server/victory-chart-renderer/tests/cli.test.ts b/server/victory-chart-renderer/tests/cli.test.ts index c157c1cc659e..64a48763b1f6 100644 --- a/server/victory-chart-renderer/tests/cli.test.ts +++ b/server/victory-chart-renderer/tests/cli.test.ts @@ -6,9 +6,7 @@ import {join} from 'node:path'; import pixelmatch from 'pixelmatch'; import {PNG} from 'pngjs'; -const cliDir = join(import.meta.dir, '..'); -const projectRoot = join(cliDir, '../..'); -const cliPath = join(cliDir, 'src/cli.tsx'); +const repoRoot = join(import.meta.dir, '../../..'); const goldenPath = join(import.meta.dir, '__golden__/smoke.png'); const EXPECTED_WIDTH = 400; @@ -24,8 +22,8 @@ test('CLI renders a chart whose PNG matches the golden reference', () => { const actualPath = join(tempDir, 'smoke-actual.png'); try { - const result = spawnSync('bun', ['--conditions=react-native', 'run', cliPath, actualPath], { - cwd: projectRoot, + const result = spawnSync('npm', ['run', 'dev', '-w', '@expensify/victory-chart-renderer', '--', actualPath], { + cwd: repoRoot, encoding: 'utf8', }); diff --git a/server/victory-chart-renderer/tsconfig.json b/server/victory-chart-renderer/tsconfig.json new file mode 100644 index 000000000000..365469e3528d --- /dev/null +++ b/server/victory-chart-renderer/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "types": ["@types/bun"], + "paths": { + "@scripts/*": ["../../scripts/*", "../../.github/scripts/*"] + } + }, + "include": ["src", "tests", "stubs", "scripts"] +} From 3580f416bf38369aaa12bb491202e0dd2757bcd0 Mon Sep 17 00:00:00 2001 From: rory Date: Fri, 29 May 2026 06:50:15 -0700 Subject: [PATCH 05/21] Move VCR stubs to server/ and harden headless types Relocate stub packages and shared server/tsconfig so the workspace references them via file:../stubs. Convert reanimated stub to TypeScript, add @src paths for CLI typechecking, and fix VCR script lint/types. Co-authored-by: Cursor --- config/eslint/eslint.config.mjs | 2 +- package-lock.json | 30 +++++--- package.json | 4 +- .../react-native-gesture-handler/index.ts | 0 .../react-native-gesture-handler/package.json | 0 server/stubs/react-native-reanimated/index.ts | 69 +++++++++++++++++++ .../react-native-reanimated/package.json | 0 .../stubs/react-native/index.ts | 0 .../stubs/react-native/package.json | 0 .../stubs/react-native/subpath.ts | 0 server/tsconfig.json | 11 +++ server/victory-chart-renderer/README.md | 4 +- server/victory-chart-renderer/package.json | 12 ++-- .../victory-chart-renderer/scripts/build.ts | 10 +-- server/victory-chart-renderer/scripts/dev.ts | 5 +- .../scripts/rnStubPlugin.ts | 4 +- .../stubs/react-native-reanimated/index.js | 65 ----------------- server/victory-chart-renderer/tsconfig.json | 10 --- 18 files changed, 121 insertions(+), 105 deletions(-) rename server/{victory-chart-renderer => }/stubs/react-native-gesture-handler/index.ts (100%) rename server/{victory-chart-renderer => }/stubs/react-native-gesture-handler/package.json (100%) create mode 100644 server/stubs/react-native-reanimated/index.ts rename server/{victory-chart-renderer => }/stubs/react-native-reanimated/package.json (100%) rename server/{victory-chart-renderer => }/stubs/react-native/index.ts (100%) rename server/{victory-chart-renderer => }/stubs/react-native/package.json (100%) rename server/{victory-chart-renderer => }/stubs/react-native/subpath.ts (100%) create mode 100644 server/tsconfig.json delete mode 100644 server/victory-chart-renderer/stubs/react-native-reanimated/index.js delete mode 100644 server/victory-chart-renderer/tsconfig.json diff --git a/config/eslint/eslint.config.mjs b/config/eslint/eslint.config.mjs index b7ca91c8f671..b8edb82f57a8 100644 --- a/config/eslint/eslint.config.mjs +++ b/config/eslint/eslint.config.mjs @@ -739,7 +739,7 @@ const config = defineConfig([ files: ['server/**/*.ts', 'server/**/*.tsx'], languageOptions: { parserOptions: { - project: path.resolve(projectRoot, 'server/victory-chart-renderer/tsconfig.json'), + project: path.resolve(projectRoot, 'server/tsconfig.json'), }, }, }, diff --git a/package-lock.json b/package-lock.json index ddba731701ef..9ee47b7b89d8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -40590,15 +40590,24 @@ "zod": "^3.25.0 || ^4.0.0" } }, + "server/stubs/react-native": { + "version": "0.0.0-stub" + }, + "server/stubs/react-native-gesture-handler": { + "version": "0.0.0-stub" + }, + "server/stubs/react-native-reanimated": { + "version": "0.0.0-stub" + }, "server/victory-chart-renderer": { "name": "@expensify/victory-chart-renderer", "version": "0.0.0", "dependencies": { "@shopify/react-native-skia": "^2.4.14", "react": "19.2.0", - "react-native": "file:./stubs/react-native", - "react-native-gesture-handler": "file:./stubs/react-native-gesture-handler", - "react-native-reanimated": "file:./stubs/react-native-reanimated", + "react-native": "file:../stubs/react-native", + "react-native-gesture-handler": "file:../stubs/react-native-gesture-handler", + "react-native-reanimated": "file:../stubs/react-native-reanimated", "victory-native": "^41.21.0" }, "devDependencies": { @@ -40612,25 +40621,28 @@ } }, "server/victory-chart-renderer/node_modules/react-native": { - "resolved": "server/victory-chart-renderer/stubs/react-native", + "resolved": "server/stubs/react-native", "link": true }, "server/victory-chart-renderer/node_modules/react-native-gesture-handler": { - "resolved": "server/victory-chart-renderer/stubs/react-native-gesture-handler", + "resolved": "server/stubs/react-native-gesture-handler", "link": true }, "server/victory-chart-renderer/node_modules/react-native-reanimated": { - "resolved": "server/victory-chart-renderer/stubs/react-native-reanimated", + "resolved": "server/stubs/react-native-reanimated", "link": true }, "server/victory-chart-renderer/stubs/react-native": { - "version": "0.0.0-stub" + "version": "0.0.0-stub", + "extraneous": true }, "server/victory-chart-renderer/stubs/react-native-gesture-handler": { - "version": "0.0.0-stub" + "version": "0.0.0-stub", + "extraneous": true }, "server/victory-chart-renderer/stubs/react-native-reanimated": { - "version": "0.0.0-stub" + "version": "0.0.0-stub", + "extraneous": true } } } diff --git a/package.json b/package.json index e706b9b89973..d9b8f9d81091 100644 --- a/package.json +++ b/package.json @@ -44,8 +44,8 @@ "test:verbose": "TZ=utc NODE_OPTIONS=\"--experimental-vm-modules --max_old_space_size=4096\" JEST_VERBOSE=true jest", "test:debug": "TZ=utc NODE_OPTIONS='--inspect-brk --experimental-vm-modules' jest --runInBand", "perf-test": "NODE_OPTIONS=--experimental-vm-modules npx reassure", - "typecheck": "NODE_OPTIONS=--max_old_space_size=8192 tsc && tsc -p server/victory-chart-renderer/tsconfig.json", - "typecheck-tsgo": "tsgo --noEmit --incremental --tsBuildInfoFile tsconfig.tsgo.tsbuildinfo && tsgo --noEmit -p server/victory-chart-renderer/tsconfig.json --incremental --tsBuildInfoFile server/victory-chart-renderer/tsconfig.tsgo.tsbuildinfo", + "typecheck": "NODE_OPTIONS=--max_old_space_size=8192 tsc && tsc -p server/tsconfig.json", + "typecheck-tsgo": "tsgo --noEmit --incremental --tsBuildInfoFile tsconfig.tsgo.tsbuildinfo && tsgo --noEmit -p server/tsconfig.json --incremental --tsBuildInfoFile server/tsconfig.tsgo.tsbuildinfo", "lint": "./scripts/lint.sh", "lint-changed": "./scripts/lintChanged.sh", "lint-watch": "onchange '**/*.{js,jsx,ts,tsx,mjs,cjs}' -- ./scripts/lint.sh {{changed}}", diff --git a/server/victory-chart-renderer/stubs/react-native-gesture-handler/index.ts b/server/stubs/react-native-gesture-handler/index.ts similarity index 100% rename from server/victory-chart-renderer/stubs/react-native-gesture-handler/index.ts rename to server/stubs/react-native-gesture-handler/index.ts diff --git a/server/victory-chart-renderer/stubs/react-native-gesture-handler/package.json b/server/stubs/react-native-gesture-handler/package.json similarity index 100% rename from server/victory-chart-renderer/stubs/react-native-gesture-handler/package.json rename to server/stubs/react-native-gesture-handler/package.json diff --git a/server/stubs/react-native-reanimated/index.ts b/server/stubs/react-native-reanimated/index.ts new file mode 100644 index 000000000000..b09e78d7e909 --- /dev/null +++ b/server/stubs/react-native-reanimated/index.ts @@ -0,0 +1,69 @@ +// Minimal `react-native-reanimated` stub for the headless Bun renderer. +// +// `victory-native`'s `CartesianChart` calls `useSharedValue` while rendering, +// and `@shopify/react-native-skia`'s `ReanimatedProxy` reads arbitrary +// properties off the module at load (`Rea.createWorkletRuntime`, +// `Rea.runOnRuntime`, etc.). Both must keep working without a real Reanimated +// runtime mounted. +// +// To handle both shapes uniformly, the module exports a Proxy. ES-style named +// imports (`import { useSharedValue } from ...`) read the proxy's own +// properties; default imports (`import Rea from ...`) get the same proxy, +// which falls back to a no-op function for any unknown property. +// +// `export =` keeps CommonJS semantics so `require("react-native-reanimated")` +// returns the proxy directly from Bun. +import type {FunctionComponent} from 'react'; + +type SharedValue = {value: T}; +type NoopFn = () => void; + +const noopFn: NoopFn = () => undefined; + +const noopComponent: FunctionComponent = () => null; + +const sharedValue = (initialValue: T): SharedValue => ({value: initialValue}); + +const namedExports = { + __esModule: true as const, + default: undefined as unknown, + useSharedValue: sharedValue, + useDerivedValue: (updater: () => T): SharedValue => ({value: updater()}), + useAnimatedStyle: (factory: () => T): T => factory(), + useAnimatedReaction: noopFn, + makeMutable: sharedValue, + isSharedValue: (value: unknown): value is SharedValue => typeof value === 'object' && value !== null && 'value' in value, + runOnJS: (fn: T): T => fn, + runOnUI: (fn: T): T => fn, + withTiming: (value: T): T => value, + withSpring: (value: T): T => value, + withDecay: noopFn, + withDelay: (_delay: number, animation: T): T => animation, + withRepeat: (animation: T): T => animation, + cancelAnimation: noopFn, + Easing: { + linear: () => 0, + ease: () => 0, + bezier: () => () => 0, + inOut: (fn: () => number) => fn, + }, + View: noopComponent, + Image: noopComponent, + Text: noopComponent, + ScrollView: noopComponent, +}; + +type NamedExports = typeof namedExports; + +const proxy = new Proxy(namedExports, { + get(target, prop: string | symbol) { + if (typeof prop === 'string' && prop in target) { + return target[prop as keyof NamedExports]; + } + return noopFn; + }, +}) as NamedExports & Record; + +namedExports.default = proxy; + +export = proxy; diff --git a/server/victory-chart-renderer/stubs/react-native-reanimated/package.json b/server/stubs/react-native-reanimated/package.json similarity index 100% rename from server/victory-chart-renderer/stubs/react-native-reanimated/package.json rename to server/stubs/react-native-reanimated/package.json diff --git a/server/victory-chart-renderer/stubs/react-native/index.ts b/server/stubs/react-native/index.ts similarity index 100% rename from server/victory-chart-renderer/stubs/react-native/index.ts rename to server/stubs/react-native/index.ts diff --git a/server/victory-chart-renderer/stubs/react-native/package.json b/server/stubs/react-native/package.json similarity index 100% rename from server/victory-chart-renderer/stubs/react-native/package.json rename to server/stubs/react-native/package.json diff --git a/server/victory-chart-renderer/stubs/react-native/subpath.ts b/server/stubs/react-native/subpath.ts similarity index 100% rename from server/victory-chart-renderer/stubs/react-native/subpath.ts rename to server/stubs/react-native/subpath.ts diff --git a/server/tsconfig.json b/server/tsconfig.json new file mode 100644 index 000000000000..efcd93854907 --- /dev/null +++ b/server/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "types": ["@types/bun"], + "paths": { + "@scripts/*": ["../scripts/*", "../.github/scripts/*"], + "@src/*": ["../src/*"] + } + }, + "include": ["stubs", "victory-chart-renderer/src", "victory-chart-renderer/tests", "victory-chart-renderer/scripts"] +} diff --git a/server/victory-chart-renderer/README.md b/server/victory-chart-renderer/README.md index edeabf445785..540836a11ed0 100644 --- a/server/victory-chart-renderer/README.md +++ b/server/victory-chart-renderer/README.md @@ -2,12 +2,12 @@ Standalone Bun CLI (`@expensify/victory-chart-renderer`) that renders Expensify charts to PNG using the same `victory-native` and Skia code paths as the App. -This package is an [npm workspace](https://docs.npmjs.com/cli/using-npm/workspaces) child of the App root. React Native peer dependencies are declared as local `file:` stub packages under `stubs/` (with `overrides` + `installConfig.hoistingLimits` so they stay in this workspace). At runtime, `scripts/dev.ts` and `scripts/build.ts` bundle the CLI with a Bun plugin that redirects `react-native`, `react-native-reanimated`, `react-native-gesture-handler`, and `react-native/*` imports to those stubs, because hoisted `victory-native` at the repo root would otherwise resolve the real native modules. The in-app Metro build is unaffected. +This package is an [npm workspace](https://docs.npmjs.com/cli/using-npm/workspaces) child of the App root. React Native peer dependencies are declared as local `file:` stub packages under [`../stubs/`](../stubs/) (with `overrides` + `installConfig.hoistingLimits` so they stay in this workspace). At runtime, `scripts/dev.ts` and `scripts/build.ts` bundle the CLI with a Bun plugin that redirects `react-native`, `react-native-reanimated`, `react-native-gesture-handler`, and `react-native/*` imports to those stubs, because hoisted `victory-native` at the repo root would otherwise resolve the real native modules. The in-app Metro build is unaffected. ## Project layout - `src/` — CLI source -- `stubs/` — `file:` stub packages (`react-native`, `react-native-reanimated`, `react-native-gesture-handler`) used only in this workspace +- `../stubs/` — shared `file:` stub packages (`react-native`, `react-native-reanimated`, `react-native-gesture-handler`) for server-side Bun tooling - `scripts/` — Bun bundler entrypoints (`dev.ts`, `build.ts`, `rnStubPlugin.ts`) - `tests/` — Bun integration tests (`bun:test`) - `dist/` — compiled binaries (gitignored) diff --git a/server/victory-chart-renderer/package.json b/server/victory-chart-renderer/package.json index 730bbf121095..f8d66c933b7e 100644 --- a/server/victory-chart-renderer/package.json +++ b/server/victory-chart-renderer/package.json @@ -13,9 +13,9 @@ "dependencies": { "@shopify/react-native-skia": "^2.4.14", "react": "19.2.0", - "react-native": "file:./stubs/react-native", - "react-native-gesture-handler": "file:./stubs/react-native-gesture-handler", - "react-native-reanimated": "file:./stubs/react-native-reanimated", + "react-native": "file:../stubs/react-native", + "react-native-gesture-handler": "file:../stubs/react-native-gesture-handler", + "react-native-reanimated": "file:../stubs/react-native-reanimated", "victory-native": "^41.21.0" }, "devDependencies": { @@ -25,9 +25,9 @@ "pngjs": "^7.0.0" }, "overrides": { - "react-native": "file:./stubs/react-native", - "react-native-reanimated": "file:./stubs/react-native-reanimated", - "react-native-gesture-handler": "file:./stubs/react-native-gesture-handler" + "react-native": "file:../stubs/react-native", + "react-native-reanimated": "file:../stubs/react-native-reanimated", + "react-native-gesture-handler": "file:../stubs/react-native-gesture-handler" }, "installConfig": { "hoistingLimits": "workspaces" diff --git a/server/victory-chart-renderer/scripts/build.ts b/server/victory-chart-renderer/scripts/build.ts index 764382ad6d65..969baa4fb1d7 100644 --- a/server/victory-chart-renderer/scripts/build.ts +++ b/server/victory-chart-renderer/scripts/build.ts @@ -1,11 +1,11 @@ import {join, resolve} from 'node:path'; -import {createRnStubPlugin} from './rnStubPlugin'; +import createRnStubPlugin from './rnStubPlugin'; const packageRoot = resolve(import.meta.dir, '..'); -const stubRoot = resolve(packageRoot, 'stubs'); +const stubRoot = resolve(packageRoot, '../stubs'); -const target = process.argv[2]; -const outfile = process.argv[3]; +const target = process.argv.at(2); +const outfile = process.argv.at(3); if (!target || !outfile) { throw new Error('Usage: bun run scripts/build.ts '); @@ -14,7 +14,7 @@ if (!target || !outfile) { const buildResult = await Bun.build({ entrypoints: [join(packageRoot, 'src/cli.tsx')], compile: { - target, + target: target as Bun.Build.CompileTarget, outfile, }, packages: 'bundle', diff --git a/server/victory-chart-renderer/scripts/dev.ts b/server/victory-chart-renderer/scripts/dev.ts index 39d5af1ea930..5bb1ec9cd5b8 100644 --- a/server/victory-chart-renderer/scripts/dev.ts +++ b/server/victory-chart-renderer/scripts/dev.ts @@ -1,18 +1,17 @@ import {spawnSync} from 'node:child_process'; import {mkdirSync} from 'node:fs'; import {join, resolve} from 'node:path'; -import {createRnStubPlugin} from './rnStubPlugin'; +import createRnStubPlugin from './rnStubPlugin'; const packageRoot = resolve(import.meta.dir, '..'); const repoRoot = resolve(packageRoot, '../..'); -const stubRoot = resolve(packageRoot, 'stubs'); +const stubRoot = resolve(packageRoot, '../stubs'); const outFile = resolve(packageRoot, '.dev/cli.js'); mkdirSync(join(packageRoot, '.dev'), {recursive: true}); const buildResult = await Bun.build({ entrypoints: [resolve(packageRoot, 'src/cli.tsx')], - outfile: outFile, target: 'bun', packages: 'bundle', conditions: ['react-native'], diff --git a/server/victory-chart-renderer/scripts/rnStubPlugin.ts b/server/victory-chart-renderer/scripts/rnStubPlugin.ts index f122cb30c61c..764f1fbae279 100644 --- a/server/victory-chart-renderer/scripts/rnStubPlugin.ts +++ b/server/victory-chart-renderer/scripts/rnStubPlugin.ts @@ -1,10 +1,10 @@ import type {BunPlugin} from 'bun'; import {resolve} from 'node:path'; -export function createRnStubPlugin(stubRoot: string): BunPlugin { +export default function createRnStubPlugin(stubRoot: string): BunPlugin { const reactNativeStub = resolve(stubRoot, 'react-native/index.ts'); const reactNativeSubpathStub = resolve(stubRoot, 'react-native/subpath.ts'); - const reanimatedStub = resolve(stubRoot, 'react-native-reanimated/index.js'); + const reanimatedStub = resolve(stubRoot, 'react-native-reanimated/index.ts'); const gestureHandlerStub = resolve(stubRoot, 'react-native-gesture-handler/index.ts'); return { diff --git a/server/victory-chart-renderer/stubs/react-native-reanimated/index.js b/server/victory-chart-renderer/stubs/react-native-reanimated/index.js deleted file mode 100644 index 3ee122c66df2..000000000000 --- a/server/victory-chart-renderer/stubs/react-native-reanimated/index.js +++ /dev/null @@ -1,65 +0,0 @@ -// Minimal `react-native-reanimated` stub for the headless Bun renderer. -// -// `victory-native`'s `CartesianChart` calls `useSharedValue` while rendering, -// and `@shopify/react-native-skia`'s `ReanimatedProxy` reads arbitrary -// properties off the module at load (`Rea.createWorkletRuntime`, -// `Rea.runOnRuntime`, etc.). Both must keep working without a real Reanimated -// runtime mounted. -// -// To handle both shapes uniformly, the module exports a Proxy. ES-style named -// imports (`import { useSharedValue } from ...`) read the proxy's own -// properties; default imports (`import Rea from ...`) get the same proxy, -// which falls back to a no-op function for any unknown property. -// -// This file is CommonJS so that Bun returns the proxy directly from -// `require("react-native-reanimated")`. - -const noopFn = () => undefined; - -const noopComponent = () => null; - -const sharedValue = (initialValue) => ({value: initialValue}); - -const namedExports = { - __esModule: true, - default: undefined, - useSharedValue: sharedValue, - useDerivedValue: (updater) => ({value: updater()}), - useAnimatedStyle: (factory) => factory(), - useAnimatedReaction: noopFn, - makeMutable: sharedValue, - isSharedValue: (value) => typeof value === 'object' && value !== null && 'value' in value, - runOnJS: (fn) => fn, - runOnUI: (fn) => fn, - withTiming: (value) => value, - withSpring: (value) => value, - withDecay: noopFn, - withDelay: (delay, animation) => animation, - withRepeat: (animation) => animation, - cancelAnimation: noopFn, - Easing: { - linear: () => 0, - ease: () => 0, - bezier: () => () => 0, - inOut: (fn) => fn, - }, - View: noopComponent, - Image: noopComponent, - Text: noopComponent, - ScrollView: noopComponent, -}; - -const proxy = new Proxy(namedExports, { - get(target, prop) { - if (prop in target) { - return target[prop]; - } - return noopFn; - }, -}); - -// Match Reanimated's runtime shape: `import Rea from "react-native-reanimated"` -// returns the same proxy as `import * as Rea`. -namedExports.default = proxy; - -module.exports = proxy; diff --git a/server/victory-chart-renderer/tsconfig.json b/server/victory-chart-renderer/tsconfig.json deleted file mode 100644 index 365469e3528d..000000000000 --- a/server/victory-chart-renderer/tsconfig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "types": ["@types/bun"], - "paths": { - "@scripts/*": ["../../scripts/*", "../../.github/scripts/*"] - } - }, - "include": ["src", "tests", "stubs", "scripts"] -} From d49c72472d3bf5a91a6e161d9bbc3ad01812ff95 Mon Sep 17 00:00:00 2001 From: rory Date: Fri, 29 May 2026 06:50:24 -0700 Subject: [PATCH 06/21] Harden headless RN stub types and lint Add explicit return types, convert gesture builder methods to fields, and point reanimated package main at index.ts. Co-authored-by: Cursor --- .../react-native-gesture-handler/index.ts | 65 +++++-------------- .../react-native-reanimated/package.json | 2 +- server/stubs/react-native/index.ts | 11 ++-- server/stubs/react-native/subpath.ts | 4 +- 4 files changed, 27 insertions(+), 55 deletions(-) diff --git a/server/stubs/react-native-gesture-handler/index.ts b/server/stubs/react-native-gesture-handler/index.ts index fc6d63e1893f..41f8b986bba3 100644 --- a/server/stubs/react-native-gesture-handler/index.ts +++ b/server/stubs/react-native-gesture-handler/index.ts @@ -8,69 +8,38 @@ import type {FunctionComponent, PropsWithChildren} from 'react'; class GestureBuilder { - onTouchesDown(): this { - return this; - } + // Chainable no-ops: victory-native builds gesture configs during render even in headless mode. + onTouchesDown = (): this => this; - onStart(): this { - return this; - } + onStart = (): this => this; - onTouchesMove(): this { - return this; - } + onTouchesMove = (): this => this; - onTouchesUp(): this { - return this; - } + onTouchesUp = (): this => this; - onEnd(): this { - return this; - } + onEnd = (): this => this; - onFinalize(): this { - return this; - } + onFinalize = (): this => this; - onChange(): this { - return this; - } + onChange = (): this => this; - onUpdate(): this { - return this; - } + onUpdate = (): this => this; - activateAfterLongPress(): this { - return this; - } + activateAfterLongPress = (): this => this; - activeOffsetX(): this { - return this; - } + activeOffsetX = (): this => this; - activeOffsetY(): this { - return this; - } + activeOffsetY = (): this => this; - failOffsetX(): this { - return this; - } + failOffsetX = (): this => this; - failOffsetY(): this { - return this; - } + failOffsetY = (): this => this; - minDistance(): this { - return this; - } + minDistance = (): this => this; - enabled(): this { - return this; - } + enabled = (): this => this; - runOnJS(): this { - return this; - } + runOnJS = (): this => this; } const buildGesture = (): GestureBuilder => new GestureBuilder(); diff --git a/server/stubs/react-native-reanimated/package.json b/server/stubs/react-native-reanimated/package.json index 107271909f92..49389f3a58b7 100644 --- a/server/stubs/react-native-reanimated/package.json +++ b/server/stubs/react-native-reanimated/package.json @@ -2,5 +2,5 @@ "name": "react-native-reanimated", "version": "0.0.0-stub", "private": true, - "main": "./index.js" + "main": "./index.ts" } diff --git a/server/stubs/react-native/index.ts b/server/stubs/react-native/index.ts index 12840f238608..527faef725c1 100644 --- a/server/stubs/react-native/index.ts +++ b/server/stubs/react-native/index.ts @@ -20,10 +20,13 @@ type StyleProp = T | T[] | null | undefined | false; type TransformsStyle = Record; const View = noopComponent; +type ImageResolvedAssetSource = {uri: string}; + const Image = Object.assign(noopComponent, { - resolveAssetSource: (source: unknown) => { - if (source && typeof source === 'object' && 'uri' in (source as Record)) { - return source as {uri: string}; + resolveAssetSource: (source: unknown): ImageResolvedAssetSource => { + if (source && typeof source === 'object' && 'uri' in source) { + const {uri} = source as {uri: unknown}; + return {uri: String(uri)}; } return {uri: String(source)}; }, @@ -33,7 +36,7 @@ const Text = noopComponent; const Platform = { OS: 'web' as const, Version: 0, - select: (specifics: {default?: T; web?: T}) => specifics.web ?? specifics.default, + select: (specifics: {default?: T; web?: T}): T | undefined => specifics.web ?? specifics.default, }; const PixelRatio = { diff --git a/server/stubs/react-native/subpath.ts b/server/stubs/react-native/subpath.ts index 0eda28658b0f..01a0256a1a5d 100644 --- a/server/stubs/react-native/subpath.ts +++ b/server/stubs/react-native/subpath.ts @@ -7,8 +7,8 @@ const noopFunction = () => null; const codegenNativeComponent = noopFunction; -const registerAsset = (asset: unknown) => asset; -const getAssetByID = () => null; +const registerAsset = (asset: T): T => asset; +const getAssetByID = (): null => null; export {codegenNativeComponent, registerAsset, getAssetByID}; export default noopFunction; From d52b017ec99f346cc04d199f07df4ca6958569b6 Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 1 Jun 2026 08:06:27 -0700 Subject: [PATCH 07/21] Parse VCR build script args with shared CLI utility Use named --target and --outfile flags in build.ts so argument parsing matches other scripts and npm build scripts pass explicit flags. Co-authored-by: Cursor --- server/victory-chart-renderer/package.json | 6 +++--- server/victory-chart-renderer/scripts/build.ts | 17 ++++++++++++----- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/server/victory-chart-renderer/package.json b/server/victory-chart-renderer/package.json index f8d66c933b7e..bd9fa44e353f 100644 --- a/server/victory-chart-renderer/package.json +++ b/server/victory-chart-renderer/package.json @@ -6,9 +6,9 @@ "scripts": { "dev": "bun run scripts/dev.ts", "test": "bun test", - "build:linux": "bun run scripts/build.ts bun-linux-x64 dist/victory-chart-renderer-linux-x64", - "build:linux-arm": "bun run scripts/build.ts bun-linux-arm64 dist/victory-chart-renderer-linux-arm64", - "build:macos": "bun run scripts/build.ts bun-darwin-arm64 dist/victory-chart-renderer-darwin-arm64" + "build:linux": "bun run scripts/build.ts --target bun-linux-x64 --outfile dist/victory-chart-renderer-linux-x64", + "build:linux-arm": "bun run scripts/build.ts --target bun-linux-arm64 --outfile dist/victory-chart-renderer-linux-arm64", + "build:macos": "bun run scripts/build.ts --target bun-darwin-arm64 --outfile dist/victory-chart-renderer-darwin-arm64" }, "dependencies": { "@shopify/react-native-skia": "^2.4.14", diff --git a/server/victory-chart-renderer/scripts/build.ts b/server/victory-chart-renderer/scripts/build.ts index 969baa4fb1d7..bf27b2296a9e 100644 --- a/server/victory-chart-renderer/scripts/build.ts +++ b/server/victory-chart-renderer/scripts/build.ts @@ -1,15 +1,22 @@ import {join, resolve} from 'node:path'; +import CLI from '@scripts/utils/CLI'; import createRnStubPlugin from './rnStubPlugin'; const packageRoot = resolve(import.meta.dir, '..'); const stubRoot = resolve(packageRoot, '../stubs'); -const target = process.argv.at(2); -const outfile = process.argv.at(3); +const cli = new CLI({ + namedArgs: { + target: { + description: 'Bun compile target (e.g. bun-darwin-arm64, bun-linux-x64)', + }, + outfile: { + description: 'Path for the compiled binary output', + }, + }, +}); -if (!target || !outfile) { - throw new Error('Usage: bun run scripts/build.ts '); -} +const {target, outfile} = cli.namedArgs; const buildResult = await Bun.build({ entrypoints: [join(packageRoot, 'src/cli.tsx')], From 2697795c4ee5aca2efa6bb89c26c884d1999fb64 Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 1 Jun 2026 08:06:56 -0700 Subject: [PATCH 08/21] Use named --outPath flag for VCR CLI output Align cli.tsx with build.ts by parsing output path via CLI namedArgs. Update the golden test and README examples to pass --outPath. Co-authored-by: Cursor --- server/victory-chart-renderer/README.md | 4 ++-- server/victory-chart-renderer/src/cli.tsx | 9 ++++----- server/victory-chart-renderer/tests/cli.test.ts | 2 +- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/server/victory-chart-renderer/README.md b/server/victory-chart-renderer/README.md index 540836a11ed0..83390d1057a9 100644 --- a/server/victory-chart-renderer/README.md +++ b/server/victory-chart-renderer/README.md @@ -17,13 +17,13 @@ This package is an [npm workspace](https://docs.npmjs.com/cli/using-npm/workspac From the App repository root: ```bash -npm run server:vcr:dev /tmp/out.png +npm run server:vcr:dev -- --outPath /tmp/out.png ``` Or from this directory: ```bash -npm run dev /tmp/out.png +npm run dev -- --outPath /tmp/out.png ``` ## Tests diff --git a/server/victory-chart-renderer/src/cli.tsx b/server/victory-chart-renderer/src/cli.tsx index 7ab39b846f39..a44ca1718b81 100644 --- a/server/victory-chart-renderer/src/cli.tsx +++ b/server/victory-chart-renderer/src/cli.tsx @@ -4,12 +4,11 @@ import {LoadSkiaWeb} from '@shopify/react-native-skia/lib/module/web/LoadSkiaWeb import CLI from '@scripts/utils/CLI'; const cli = new CLI({ - positionalArgs: [ - { - name: 'outPath', + namedArgs: { + outPath: { description: 'Path to write the rendered PNG to', }, - ], + }, }); await LoadSkiaWeb(); @@ -64,4 +63,4 @@ if (!pngBytes) { throw new Error('Skia failed to encode the rendered chart to PNG bytes'); } -await Bun.write(cli.positionalArgs.outPath, pngBytes); +await Bun.write(cli.namedArgs.outPath, pngBytes); diff --git a/server/victory-chart-renderer/tests/cli.test.ts b/server/victory-chart-renderer/tests/cli.test.ts index 64a48763b1f6..dc1314d69fbd 100644 --- a/server/victory-chart-renderer/tests/cli.test.ts +++ b/server/victory-chart-renderer/tests/cli.test.ts @@ -22,7 +22,7 @@ test('CLI renders a chart whose PNG matches the golden reference', () => { const actualPath = join(tempDir, 'smoke-actual.png'); try { - const result = spawnSync('npm', ['run', 'dev', '-w', '@expensify/victory-chart-renderer', '--', actualPath], { + const result = spawnSync('npm', ['run', 'dev', '-w', '@expensify/victory-chart-renderer', '--', '--outPath', actualPath], { cwd: repoRoot, encoding: 'utf8', }); From 018da43c632208246a000183b841b742e17a947e Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 1 Jun 2026 08:09:39 -0700 Subject: [PATCH 09/21] Validate VCR CLI outPath ends with .png Reject invalid output paths during CLI parsing so mis-typed extensions fail before loading Skia. Co-authored-by: Cursor --- server/victory-chart-renderer/src/cli.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/server/victory-chart-renderer/src/cli.tsx b/server/victory-chart-renderer/src/cli.tsx index a44ca1718b81..774eccbad696 100644 --- a/server/victory-chart-renderer/src/cli.tsx +++ b/server/victory-chart-renderer/src/cli.tsx @@ -7,6 +7,12 @@ const cli = new CLI({ namedArgs: { outPath: { description: 'Path to write the rendered PNG to', + parse: (val) => { + if (!val.toLowerCase().endsWith('.png')) { + throw new Error('outPath must end with .png'); + } + return val; + }, }, }, }); From 33e0a1bea8d60457d170a39f58812bb16989f4ce Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 1 Jun 2026 08:11:09 -0700 Subject: [PATCH 10/21] Use SHOULD_ prefix for bool --- server/victory-chart-renderer/tests/cli.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/victory-chart-renderer/tests/cli.test.ts b/server/victory-chart-renderer/tests/cli.test.ts index dc1314d69fbd..3706557d60b0 100644 --- a/server/victory-chart-renderer/tests/cli.test.ts +++ b/server/victory-chart-renderer/tests/cli.test.ts @@ -15,7 +15,7 @@ const EXPECTED_HEIGHT = 250; // Set `UPDATE_GOLDEN=1` while running the test to refresh the reference PNG. // Intentionally separate from the comparison flow so a regression can't quietly // rewrite the golden. -const UPDATE_GOLDEN = process.env.UPDATE_GOLDEN === '1'; +const SHOULD_UPDATE_GOLDEN = process.env.UPDATE_GOLDEN === '1'; test('CLI renders a chart whose PNG matches the golden reference', () => { const tempDir = mkdtempSync(join(tmpdir(), 'vcr-smoke-')); @@ -29,7 +29,7 @@ test('CLI renders a chart whose PNG matches the golden reference', () => { expect(result.status).toBe(0); - if (UPDATE_GOLDEN) { + if (SHOULD_UPDATE_GOLDEN) { copyFileSync(actualPath, goldenPath); } From c6a49b9f3316256dd3b512a0c83fd9f2280478e6 Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 1 Jun 2026 08:17:44 -0700 Subject: [PATCH 11/21] Consolidate VCR gitignore rules at repo root Move victory-chart-renderer ignore patterns into the App .gitignore and ignore bun.lock repo-wide since npm workspaces own dependency locks. Co-authored-by: Cursor --- .gitignore | 6 +++++- server/victory-chart-renderer/.gitignore | 3 --- 2 files changed, 5 insertions(+), 4 deletions(-) delete mode 100644 server/victory-chart-renderer/.gitignore diff --git a/.gitignore b/.gitignore index 9b179a0dc0d7..60585e4c6e5b 100644 --- a/.gitignore +++ b/.gitignore @@ -174,5 +174,9 @@ agent-device-output/ # cspell cache .cspellcache -# Server CLI build artifacts +# Server CLI build artifacts (victory-chart-renderer, etc.) server/*/dist/ +server/victory-chart-renderer/.dev/ + +# Bun lockfiles (VCR uses npm workspaces + package-lock.json; bun is runtime-only) +bun.lock diff --git a/server/victory-chart-renderer/.gitignore b/server/victory-chart-renderer/.gitignore deleted file mode 100644 index 5547cab15cef..000000000000 --- a/server/victory-chart-renderer/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -.dev/ -dist/ -bun.lock From ed4de0299e7bc72708b5785575a30c5cc8b9273b Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 1 Jun 2026 08:23:25 -0700 Subject: [PATCH 12/21] Document VCR dev, build, and stub plugin scripts Add block comments explaining bundling, compile vs dev flow, and why RN imports are redirected without referencing specific npm invocations. Co-authored-by: Cursor --- server/victory-chart-renderer/scripts/build.ts | 11 +++++++++++ server/victory-chart-renderer/scripts/dev.ts | 13 +++++++++++++ .../victory-chart-renderer/scripts/rnStubPlugin.ts | 11 +++++++++++ 3 files changed, 35 insertions(+) diff --git a/server/victory-chart-renderer/scripts/build.ts b/server/victory-chart-renderer/scripts/build.ts index bf27b2296a9e..69414765e494 100644 --- a/server/victory-chart-renderer/scripts/build.ts +++ b/server/victory-chart-renderer/scripts/build.ts @@ -1,3 +1,14 @@ +/* + * Cross-compiles the victory-chart-renderer CLI into a standalone binary. + * + * Like dev.ts, this bundles src/cli.tsx with rnStubPlugin so hoisted victory-native does + * not pull real react-native into the graph. Unlike dev.ts, Bun.build uses `compile` to + * emit a platform-specific executable (--target, --outfile) rather than a .dev/cli.js + * bundle run under Bun. + * + * Required --target and --outfile select the Bun compile target and output path. The script + * exits after writing the binary; it does not run it. + */ import {join, resolve} from 'node:path'; import CLI from '@scripts/utils/CLI'; import createRnStubPlugin from './rnStubPlugin'; diff --git a/server/victory-chart-renderer/scripts/dev.ts b/server/victory-chart-renderer/scripts/dev.ts index 5bb1ec9cd5b8..1136f47f96a2 100644 --- a/server/victory-chart-renderer/scripts/dev.ts +++ b/server/victory-chart-renderer/scripts/dev.ts @@ -1,3 +1,16 @@ +/* + * Development runner for the victory-chart-renderer CLI. + * + * victory-native is hoisted to the App repo root, so a bare `bun run src/cli.tsx` would + * resolve real react-native native sources that Bun cannot bundle. This script bundles + * src/cli.tsx with rnStubPlugin (redirecting RN imports to server/stubs), writes the + * bundle to .dev/cli.js, then executes it under the current Bun binary. + * + * process.argv entries after this script are forwarded unchanged to the bundled CLI. + * The child process cwd is the App repository root (two levels above this package). + * + * For a standalone executable instead of a Bun-run bundle, use build.ts. + */ import {spawnSync} from 'node:child_process'; import {mkdirSync} from 'node:fs'; import {join, resolve} from 'node:path'; diff --git a/server/victory-chart-renderer/scripts/rnStubPlugin.ts b/server/victory-chart-renderer/scripts/rnStubPlugin.ts index 764f1fbae279..4d0bb064ca51 100644 --- a/server/victory-chart-renderer/scripts/rnStubPlugin.ts +++ b/server/victory-chart-renderer/scripts/rnStubPlugin.ts @@ -1,3 +1,14 @@ +/* + * Bun bundler plugin shared by dev.ts and build.ts. + * + * npm workspaces install victory-native at the App root. Bun would otherwise follow those + * imports into Flow sources, native bindings, and Metro platform files. The headless chart + * path only needs modules to load and export no-op components; Skia draws off-screen. + * + * onResolve hooks map react-native, react-native/* subpaths, react-native-reanimated, and + * react-native-gesture-handler to the file: stub packages under server/stubs/. Stub paths + * must be absolute because Bun resolves plugin targets from the bundler context. + */ import type {BunPlugin} from 'bun'; import {resolve} from 'node:path'; From 3ccbf951c9ec7dd3ab537e661af05e2d35190b4e Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 1 Jun 2026 08:23:41 -0700 Subject: [PATCH 13/21] Move RN stub Bun plugin to server/plugins rnStubPlugin is shared bundler utilities, not a runnable script. VCR dev and build import it from ../../plugins/rnStubPlugin. Co-authored-by: Cursor --- .../scripts => plugins}/rnStubPlugin.ts | 2 +- server/tsconfig.json | 2 +- server/victory-chart-renderer/README.md | 5 +++-- server/victory-chart-renderer/scripts/build.ts | 2 +- server/victory-chart-renderer/scripts/dev.ts | 2 +- 5 files changed, 7 insertions(+), 6 deletions(-) rename server/{victory-chart-renderer/scripts => plugins}/rnStubPlugin.ts (95%) diff --git a/server/victory-chart-renderer/scripts/rnStubPlugin.ts b/server/plugins/rnStubPlugin.ts similarity index 95% rename from server/victory-chart-renderer/scripts/rnStubPlugin.ts rename to server/plugins/rnStubPlugin.ts index 4d0bb064ca51..8c45ef25918c 100644 --- a/server/victory-chart-renderer/scripts/rnStubPlugin.ts +++ b/server/plugins/rnStubPlugin.ts @@ -1,5 +1,5 @@ /* - * Bun bundler plugin shared by dev.ts and build.ts. + * Bun bundler plugin used when bundling the victory-chart-renderer CLI. * * npm workspaces install victory-native at the App root. Bun would otherwise follow those * imports into Flow sources, native bindings, and Metro platform files. The headless chart diff --git a/server/tsconfig.json b/server/tsconfig.json index efcd93854907..a539ff4a0f6c 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -7,5 +7,5 @@ "@src/*": ["../src/*"] } }, - "include": ["stubs", "victory-chart-renderer/src", "victory-chart-renderer/tests", "victory-chart-renderer/scripts"] + "include": ["stubs", "plugins", "victory-chart-renderer/src", "victory-chart-renderer/tests", "victory-chart-renderer/scripts"] } diff --git a/server/victory-chart-renderer/README.md b/server/victory-chart-renderer/README.md index 83390d1057a9..9f29bfb8ab9f 100644 --- a/server/victory-chart-renderer/README.md +++ b/server/victory-chart-renderer/README.md @@ -2,13 +2,14 @@ Standalone Bun CLI (`@expensify/victory-chart-renderer`) that renders Expensify charts to PNG using the same `victory-native` and Skia code paths as the App. -This package is an [npm workspace](https://docs.npmjs.com/cli/using-npm/workspaces) child of the App root. React Native peer dependencies are declared as local `file:` stub packages under [`../stubs/`](../stubs/) (with `overrides` + `installConfig.hoistingLimits` so they stay in this workspace). At runtime, `scripts/dev.ts` and `scripts/build.ts` bundle the CLI with a Bun plugin that redirects `react-native`, `react-native-reanimated`, `react-native-gesture-handler`, and `react-native/*` imports to those stubs, because hoisted `victory-native` at the repo root would otherwise resolve the real native modules. The in-app Metro build is unaffected. +This package is an [npm workspace](https://docs.npmjs.com/cli/using-npm/workspaces) child of the App root. React Native peer dependencies are declared as local `file:` stub packages under [`../stubs/`](../stubs/) (with `overrides` + `installConfig.hoistingLimits` so they stay in this workspace). At runtime, `scripts/dev.ts` and `scripts/build.ts` bundle the CLI with [`../plugins/rnStubPlugin.ts`](../plugins/rnStubPlugin.ts), which redirects `react-native`, `react-native-reanimated`, `react-native-gesture-handler`, and `react-native/*` imports to those stubs, because hoisted `victory-native` at the repo root would otherwise resolve the real native modules. The in-app Metro build is unaffected. ## Project layout - `src/` — CLI source - `../stubs/` — shared `file:` stub packages (`react-native`, `react-native-reanimated`, `react-native-gesture-handler`) for server-side Bun tooling -- `scripts/` — Bun bundler entrypoints (`dev.ts`, `build.ts`, `rnStubPlugin.ts`) +- `../plugins/` — shared Bun bundler plugins (`rnStubPlugin.ts`) +- `scripts/` — dev and release entrypoints (`dev.ts`, `build.ts`) - `tests/` — Bun integration tests (`bun:test`) - `dist/` — compiled binaries (gitignored) diff --git a/server/victory-chart-renderer/scripts/build.ts b/server/victory-chart-renderer/scripts/build.ts index 69414765e494..1698a7cc9d90 100644 --- a/server/victory-chart-renderer/scripts/build.ts +++ b/server/victory-chart-renderer/scripts/build.ts @@ -11,7 +11,7 @@ */ import {join, resolve} from 'node:path'; import CLI from '@scripts/utils/CLI'; -import createRnStubPlugin from './rnStubPlugin'; +import createRnStubPlugin from '../../plugins/rnStubPlugin'; const packageRoot = resolve(import.meta.dir, '..'); const stubRoot = resolve(packageRoot, '../stubs'); diff --git a/server/victory-chart-renderer/scripts/dev.ts b/server/victory-chart-renderer/scripts/dev.ts index 1136f47f96a2..dd4b183e3a8c 100644 --- a/server/victory-chart-renderer/scripts/dev.ts +++ b/server/victory-chart-renderer/scripts/dev.ts @@ -14,7 +14,7 @@ import {spawnSync} from 'node:child_process'; import {mkdirSync} from 'node:fs'; import {join, resolve} from 'node:path'; -import createRnStubPlugin from './rnStubPlugin'; +import createRnStubPlugin from '../../plugins/rnStubPlugin'; const packageRoot = resolve(import.meta.dir, '..'); const repoRoot = resolve(packageRoot, '../..'); From ddd6834a15d830fd7be0494844dfacc0d5450ce8 Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 1 Jun 2026 08:24:43 -0700 Subject: [PATCH 14/21] Remove redundant project layout from VCR README Directory structure is evident from the repo; keep usage-focused sections. Co-authored-by: Cursor --- server/victory-chart-renderer/README.md | 9 --------- 1 file changed, 9 deletions(-) diff --git a/server/victory-chart-renderer/README.md b/server/victory-chart-renderer/README.md index 9f29bfb8ab9f..01f83b77ae58 100644 --- a/server/victory-chart-renderer/README.md +++ b/server/victory-chart-renderer/README.md @@ -4,15 +4,6 @@ Standalone Bun CLI (`@expensify/victory-chart-renderer`) that renders Expensify This package is an [npm workspace](https://docs.npmjs.com/cli/using-npm/workspaces) child of the App root. React Native peer dependencies are declared as local `file:` stub packages under [`../stubs/`](../stubs/) (with `overrides` + `installConfig.hoistingLimits` so they stay in this workspace). At runtime, `scripts/dev.ts` and `scripts/build.ts` bundle the CLI with [`../plugins/rnStubPlugin.ts`](../plugins/rnStubPlugin.ts), which redirects `react-native`, `react-native-reanimated`, `react-native-gesture-handler`, and `react-native/*` imports to those stubs, because hoisted `victory-native` at the repo root would otherwise resolve the real native modules. The in-app Metro build is unaffected. -## Project layout - -- `src/` — CLI source -- `../stubs/` — shared `file:` stub packages (`react-native`, `react-native-reanimated`, `react-native-gesture-handler`) for server-side Bun tooling -- `../plugins/` — shared Bun bundler plugins (`rnStubPlugin.ts`) -- `scripts/` — dev and release entrypoints (`dev.ts`, `build.ts`) -- `tests/` — Bun integration tests (`bun:test`) -- `dist/` — compiled binaries (gitignored) - ## Development From the App repository root: From 64f73e72bdf4ce022457d95da697f0a817dba8b9 Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 1 Jun 2026 08:32:08 -0700 Subject: [PATCH 15/21] Validate VCR build --target and move parser to server/libs Add parseCompileTarget with a satisfies-checked allowlist so build.ts gets a typed CompileTarget without casting. Keep Bun plugins under server/plugins only. Co-authored-by: Cursor --- server/libs/parseCompileTarget.ts | 55 +++++++++++++++++++ server/tsconfig.json | 2 +- .../victory-chart-renderer/scripts/build.ts | 4 +- 3 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 server/libs/parseCompileTarget.ts diff --git a/server/libs/parseCompileTarget.ts b/server/libs/parseCompileTarget.ts new file mode 100644 index 000000000000..7331997be944 --- /dev/null +++ b/server/libs/parseCompileTarget.ts @@ -0,0 +1,55 @@ +/* + * Runtime validation for Bun.build compile --target strings. + * + * CompileTarget is a large template union in @types/bun; this list is checked with + * `satisfies` so additions stay aligned with Bun.Build.CompileTarget at compile time. + */ +const BUN_COMPILE_TARGETS = [ + 'bun-darwin-x64', + 'bun-darwin-x64-baseline', + 'bun-darwin-x64-modern', + 'bun-darwin-arm64', + 'bun-darwin-arm64-baseline', + 'bun-darwin-arm64-modern', + 'bun-darwin-aarch64', + 'bun-darwin-aarch64-baseline', + 'bun-darwin-aarch64-modern', + 'bun-linux-x64', + 'bun-linux-x64-baseline', + 'bun-linux-x64-modern', + 'bun-linux-x64-glibc', + 'bun-linux-x64-musl', + 'bun-linux-arm64', + 'bun-linux-arm64-baseline', + 'bun-linux-arm64-modern', + 'bun-linux-arm64-glibc', + 'bun-linux-arm64-musl', + 'bun-linux-aarch64', + 'bun-linux-aarch64-musl', + 'bun-linux-x64-baseline-glibc', + 'bun-linux-x64-modern-glibc', + 'bun-linux-x64-baseline-musl', + 'bun-linux-x64-modern-musl', + 'bun-linux-arm64-baseline-musl', + 'bun-linux-arm64-modern-musl', + 'bun-windows-x64', + 'bun-windows-x64-baseline', + 'bun-windows-x64-modern', + 'bun-windows-arm64', +] as const satisfies readonly Bun.Build.CompileTarget[]; + +const BUN_COMPILE_TARGET_SET: ReadonlySet = new Set(BUN_COMPILE_TARGETS); + +function isCompileTarget(value: string): value is Bun.Build.CompileTarget { + return BUN_COMPILE_TARGET_SET.has(value); +} + +function parseCompileTarget(value: string): Bun.Build.CompileTarget { + if (isCompileTarget(value)) { + return value; + } + + throw new Error(`Invalid compile target "${value}". Expected one of: ${BUN_COMPILE_TARGETS.join(', ')}`); +} + +export default parseCompileTarget; diff --git a/server/tsconfig.json b/server/tsconfig.json index a539ff4a0f6c..f05fe60144bf 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -7,5 +7,5 @@ "@src/*": ["../src/*"] } }, - "include": ["stubs", "plugins", "victory-chart-renderer/src", "victory-chart-renderer/tests", "victory-chart-renderer/scripts"] + "include": ["stubs", "plugins", "libs", "victory-chart-renderer/src", "victory-chart-renderer/tests", "victory-chart-renderer/scripts"] } diff --git a/server/victory-chart-renderer/scripts/build.ts b/server/victory-chart-renderer/scripts/build.ts index 1698a7cc9d90..c20d9452a176 100644 --- a/server/victory-chart-renderer/scripts/build.ts +++ b/server/victory-chart-renderer/scripts/build.ts @@ -11,6 +11,7 @@ */ import {join, resolve} from 'node:path'; import CLI from '@scripts/utils/CLI'; +import parseCompileTarget from '../../libs/parseCompileTarget'; import createRnStubPlugin from '../../plugins/rnStubPlugin'; const packageRoot = resolve(import.meta.dir, '..'); @@ -20,6 +21,7 @@ const cli = new CLI({ namedArgs: { target: { description: 'Bun compile target (e.g. bun-darwin-arm64, bun-linux-x64)', + parse: parseCompileTarget, }, outfile: { description: 'Path for the compiled binary output', @@ -32,7 +34,7 @@ const {target, outfile} = cli.namedArgs; const buildResult = await Bun.build({ entrypoints: [join(packageRoot, 'src/cli.tsx')], compile: { - target: target as Bun.Build.CompileTarget, + target, outfile, }, packages: 'bundle', From d32b1769ea8985acda92315eb35294c361025048 Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 1 Jun 2026 08:35:58 -0700 Subject: [PATCH 16/21] Fix VCR imports for import/no-relative-packages Export server libs and plugins from the root package and map them in server/tsconfig so ESLint and TypeScript resolve new.expensify paths. Co-authored-by: Cursor --- package.json | 4 ++++ server/tsconfig.json | 4 +++- server/victory-chart-renderer/scripts/build.ts | 4 ++-- server/victory-chart-renderer/scripts/dev.ts | 2 +- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index d9b8f9d81091..917068eb0dd9 100644 --- a/package.json +++ b/package.json @@ -436,6 +436,10 @@ } } }, + "exports": { + "./server/libs/parseCompileTarget": "./server/libs/parseCompileTarget.ts", + "./server/plugins/rnStubPlugin": "./server/plugins/rnStubPlugin.ts" + }, "engines": { "bun": "1.3.14", "node": "20.20.0", diff --git a/server/tsconfig.json b/server/tsconfig.json index f05fe60144bf..1fbbeb9cd5ce 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -4,7 +4,9 @@ "types": ["@types/bun"], "paths": { "@scripts/*": ["../scripts/*", "../.github/scripts/*"], - "@src/*": ["../src/*"] + "@src/*": ["../src/*"], + "new.expensify/server/libs/parseCompileTarget": ["./libs/parseCompileTarget.ts"], + "new.expensify/server/plugins/rnStubPlugin": ["./plugins/rnStubPlugin.ts"] } }, "include": ["stubs", "plugins", "libs", "victory-chart-renderer/src", "victory-chart-renderer/tests", "victory-chart-renderer/scripts"] diff --git a/server/victory-chart-renderer/scripts/build.ts b/server/victory-chart-renderer/scripts/build.ts index c20d9452a176..71821ee29e6d 100644 --- a/server/victory-chart-renderer/scripts/build.ts +++ b/server/victory-chart-renderer/scripts/build.ts @@ -9,10 +9,10 @@ * Required --target and --outfile select the Bun compile target and output path. The script * exits after writing the binary; it does not run it. */ +import parseCompileTarget from 'new.expensify/server/libs/parseCompileTarget'; +import createRnStubPlugin from 'new.expensify/server/plugins/rnStubPlugin'; import {join, resolve} from 'node:path'; import CLI from '@scripts/utils/CLI'; -import parseCompileTarget from '../../libs/parseCompileTarget'; -import createRnStubPlugin from '../../plugins/rnStubPlugin'; const packageRoot = resolve(import.meta.dir, '..'); const stubRoot = resolve(packageRoot, '../stubs'); diff --git a/server/victory-chart-renderer/scripts/dev.ts b/server/victory-chart-renderer/scripts/dev.ts index dd4b183e3a8c..0d02c12e6c0f 100644 --- a/server/victory-chart-renderer/scripts/dev.ts +++ b/server/victory-chart-renderer/scripts/dev.ts @@ -11,10 +11,10 @@ * * For a standalone executable instead of a Bun-run bundle, use build.ts. */ +import createRnStubPlugin from 'new.expensify/server/plugins/rnStubPlugin'; import {spawnSync} from 'node:child_process'; import {mkdirSync} from 'node:fs'; import {join, resolve} from 'node:path'; -import createRnStubPlugin from '../../plugins/rnStubPlugin'; const packageRoot = resolve(import.meta.dir, '..'); const repoRoot = resolve(packageRoot, '../..'); From d5af563b3218f13b68e78c493d38e17e3b542792 Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 1 Jun 2026 08:39:58 -0700 Subject: [PATCH 17/21] Fix is2dArray generic inference for tsgo Pass explicit type parameters so tsgo narrows 1D vs 2D arrays correctly. Co-authored-by: Cursor --- src/components/MapView/utils.ts | 2 +- src/libs/migrations/ConvertGpsPointsTo2DArray.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/MapView/utils.ts b/src/components/MapView/utils.ts index 772e03a30a14..9e1ca810b6e7 100644 --- a/src/components/MapView/utils.ts +++ b/src/components/MapView/utils.ts @@ -3,7 +3,7 @@ import is2dArray from '@libs/is2dArray'; import type {Coordinate} from './MapViewTypes'; function isSingleSegmentRoute(directionCoordinates: Coordinate[] | Coordinate[][]): directionCoordinates is Coordinate[] { - return is2dArray(directionCoordinates); + return is2dArray(directionCoordinates); } function getBounds(waypoints: Coordinate[], directionCoordinates: undefined | Coordinate[]): {southWest: Coordinate; northEast: Coordinate} { diff --git a/src/libs/migrations/ConvertGpsPointsTo2DArray.ts b/src/libs/migrations/ConvertGpsPointsTo2DArray.ts index 2131b8565340..3178fb8d9971 100644 --- a/src/libs/migrations/ConvertGpsPointsTo2DArray.ts +++ b/src/libs/migrations/ConvertGpsPointsTo2DArray.ts @@ -23,7 +23,7 @@ export default function (): Promise { const gpsPoints = gpsDraftDetails?.gpsPoints; // If gpsPoints is already in the correct format or is empty, skip the migration - if (!gpsPoints || is2dArray(gpsPoints)) { + if (!gpsPoints || is2dArray(gpsPoints)) { Log.info('[Migrate Onyx] Skipped ConvertGpsPointsTo2DArray — already correct format or empty'); return resolve(); } From f083b8079d0b898a6ae8bda8ff8823421e3cc149 Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 1 Jun 2026 08:43:17 -0700 Subject: [PATCH 18/21] Use @libs and @plugins paths for server tooling imports Scope server/tsconfig aliases to ./libs and ./plugins instead of new.expensify package subpaths; drop redundant root package exports. Co-authored-by: Cursor --- package.json | 4 ---- server/tsconfig.json | 6 +++--- server/victory-chart-renderer/scripts/build.ts | 4 ++-- server/victory-chart-renderer/scripts/dev.ts | 2 +- 4 files changed, 6 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 917068eb0dd9..d9b8f9d81091 100644 --- a/package.json +++ b/package.json @@ -436,10 +436,6 @@ } } }, - "exports": { - "./server/libs/parseCompileTarget": "./server/libs/parseCompileTarget.ts", - "./server/plugins/rnStubPlugin": "./server/plugins/rnStubPlugin.ts" - }, "engines": { "bun": "1.3.14", "node": "20.20.0", diff --git a/server/tsconfig.json b/server/tsconfig.json index 1fbbeb9cd5ce..913b297e623b 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -3,10 +3,10 @@ "compilerOptions": { "types": ["@types/bun"], "paths": { + "@libs/*": ["./libs/*"], + "@plugins/*": ["./plugins/*"], "@scripts/*": ["../scripts/*", "../.github/scripts/*"], - "@src/*": ["../src/*"], - "new.expensify/server/libs/parseCompileTarget": ["./libs/parseCompileTarget.ts"], - "new.expensify/server/plugins/rnStubPlugin": ["./plugins/rnStubPlugin.ts"] + "@src/*": ["../src/*"] } }, "include": ["stubs", "plugins", "libs", "victory-chart-renderer/src", "victory-chart-renderer/tests", "victory-chart-renderer/scripts"] diff --git a/server/victory-chart-renderer/scripts/build.ts b/server/victory-chart-renderer/scripts/build.ts index 71821ee29e6d..6734f06fcf3c 100644 --- a/server/victory-chart-renderer/scripts/build.ts +++ b/server/victory-chart-renderer/scripts/build.ts @@ -9,9 +9,9 @@ * Required --target and --outfile select the Bun compile target and output path. The script * exits after writing the binary; it does not run it. */ -import parseCompileTarget from 'new.expensify/server/libs/parseCompileTarget'; -import createRnStubPlugin from 'new.expensify/server/plugins/rnStubPlugin'; +import createRnStubPlugin from '@plugins/rnStubPlugin'; import {join, resolve} from 'node:path'; +import parseCompileTarget from '@libs/parseCompileTarget'; import CLI from '@scripts/utils/CLI'; const packageRoot = resolve(import.meta.dir, '..'); diff --git a/server/victory-chart-renderer/scripts/dev.ts b/server/victory-chart-renderer/scripts/dev.ts index 0d02c12e6c0f..c46d65296af2 100644 --- a/server/victory-chart-renderer/scripts/dev.ts +++ b/server/victory-chart-renderer/scripts/dev.ts @@ -11,7 +11,7 @@ * * For a standalone executable instead of a Bun-run bundle, use build.ts. */ -import createRnStubPlugin from 'new.expensify/server/plugins/rnStubPlugin'; +import createRnStubPlugin from '@plugins/rnStubPlugin'; import {spawnSync} from 'node:child_process'; import {mkdirSync} from 'node:fs'; import {join, resolve} from 'node:path'; From 9707df458121c986d806230f4e982964d5f6ce0e Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 1 Jun 2026 08:47:01 -0700 Subject: [PATCH 19/21] Simplify server tsconfig include paths Include victory-chart-renderer as a whole with dist and .dev excluded instead of listing each source subdirectory. Co-authored-by: Cursor --- server/tsconfig.json | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/server/tsconfig.json b/server/tsconfig.json index 913b297e623b..a5292928a2ab 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -9,5 +9,11 @@ "@src/*": ["../src/*"] } }, - "include": ["stubs", "plugins", "libs", "victory-chart-renderer/src", "victory-chart-renderer/tests", "victory-chart-renderer/scripts"] + "include": [ + "libs", + "plugins", + "stubs", + "victory-chart-renderer" + ], + "exclude": ["victory-chart-renderer/dist", "victory-chart-renderer/.dev"] } From edf6f8d64fe038a0072e6a244194741bb5a59c4f Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 1 Jun 2026 08:54:06 -0700 Subject: [PATCH 20/21] Forward CLI args through server:vcr npm scripts Add a trailing -- on each server:vcr:* workspace delegate so flags like --outPath reach the victory-chart-renderer scripts from the root. Co-authored-by: Cursor --- package.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 470cc12fa35c..ab32c54ba956 100644 --- a/package.json +++ b/package.json @@ -73,11 +73,11 @@ "web:dist": "concurrently \"npm:web-proxy\" \"http-server ./dist --cors --port 8080 -P http://localhost:9000\"", "octokit": "cd scripts && npx ts-node -i -e \"$(cat ./octokit.ts)\"", "compress-svg": "npx ts-node scripts/compressSvg.ts --dir assets/images && npx ts-node scripts/compressSvg.ts --dir docs/assets/images", - "server:vcr:dev": "npm run dev -w @expensify/victory-chart-renderer", - "server:vcr:test": "npm run test -w @expensify/victory-chart-renderer", - "server:vcr:build:linux": "npm run build:linux -w @expensify/victory-chart-renderer", - "server:vcr:build:linux-arm": "npm run build:linux-arm -w @expensify/victory-chart-renderer", - "server:vcr:build:macos": "npm run build:macos -w @expensify/victory-chart-renderer" + "server:vcr:dev": "npm run dev -w @expensify/victory-chart-renderer --", + "server:vcr:test": "npm run test -w @expensify/victory-chart-renderer --", + "server:vcr:build:linux": "npm run build:linux -w @expensify/victory-chart-renderer --", + "server:vcr:build:linux-arm": "npm run build:linux-arm -w @expensify/victory-chart-renderer --", + "server:vcr:build:macos": "npm run build:macos -w @expensify/victory-chart-renderer --" }, "dependencies": { "@babel/runtime": "^7.25.0", From 948f24ff452a453891fe3b4ed75e9ede0623d391 Mon Sep 17 00:00:00 2001 From: rory Date: Mon, 1 Jun 2026 09:02:36 -0700 Subject: [PATCH 21/21] Fix prettier --- server/tsconfig.json | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/server/tsconfig.json b/server/tsconfig.json index a5292928a2ab..4e976697134d 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -9,11 +9,6 @@ "@src/*": ["../src/*"] } }, - "include": [ - "libs", - "plugins", - "stubs", - "victory-chart-renderer" - ], + "include": ["libs", "plugins", "stubs", "victory-chart-renderer"], "exclude": ["victory-chart-renderer/dist", "victory-chart-renderer/.dev"] }