Compare commits

...

11 Commits

Author SHA1 Message Date
Jack Pope
02ef495809 Reset packages we are not releasing to currently published versions 2025-07-24 16:35:10 -04:00
Riccardo Cipolleschi
3f178f55fc [Release] Update build script to properly set React Native's renderers version (#33972) 2025-07-24 16:49:56 +01:00
Jack Pope
87e33ca2b7 Set release versions to 19.1.1 2025-07-16 13:05:36 -04:00
lauren
52cf381c72 [eprh] Bump stable version (#32978)
https://www.npmjs.com/package/eslint-plugin-react-hooks/v/6.0.0 was just
released, so we can bump this now.
2025-07-16 12:55:41 -04:00
lauren
b793948e15 Bump next prerelease version numbers (#32782)
Updates the version numbers in the prerelease channels.
2025-07-16 12:55:24 -04:00
Jack Pope
73e4ba42cd Allow runtime_build_and_test action to trigger manually (#33796) 2025-07-16 12:52:44 -04:00
Ruslan Lesiutin
5a1eb6f61a fix: rename bottom stack frame (#33680)
`react-stack-bottom-frame` -> `react_stack_bottom_frame`.

This survives `@babel/plugin-transform-function-name`, but now frames
will be displayed as `at Object.react_stack_bottom_frame (...)` in V8.
Checks that were relying on exact function name match were updated to
use either `.indexOf()` or `.includes()`

For backwards compatibility, both React DevTools and Flight Client will
look for both options. I am not so sure about the latter and if React
version is locked.
2025-07-16 11:53:56 -04:00
Sebastian Markbåge
01eae200bf [DevTools] Get source location from structured callsites in prepareStackTrace (#33143)
When we get the source location for "View source for this element" we
should be using the enclosing function of the callsite of the child. So
that we don't just point to some random line within the component.

This is similar to the technique in #33136.

This technique is now really better than the fake throw technique, when
available. So I now favor the owner technique. The only problem it's
only available in DEV and only if it has a child that's owned (and not
filtered).

We could implement this same technique for the error that's thrown in
the fake throwing solution. However, we really shouldn't need that at
all because for client components we should be able to call
`inspect(fn)` at least in Chrome which is even better.
2025-07-16 11:11:51 -04:00
Timothy Yung
0e6781a06b Enable the enableEagerAlternateStateNodeCleanup Feature Flag (#33447)
Enables the `enableEagerAlternateStateNodeCleanup` feature flag for all
variants, while maintaining the `__VARIANT__` for the internal React
Native flavor for backtesting reasons.

```
$ yarn test
```
2025-07-15 15:29:02 -04:00
Samuel Susla
2cd3c424ea Add eager alternate.stateNode cleanup (#33161)
This is a fix for a problem where React retains shadow nodes longer than
it needs to. The behaviour is shown in React Native test:
https://github.com/facebook/react-native/blob/main/packages/react-native/src/private/__tests__/utilities/__tests__/ShadowNodeReferenceCounter-itest.js#L169

When React commits a new shadow tree, old shadow nodes are stored inside
`fiber.alternate.stateNode`. This is not cleared up until React clones
the node again. This may be problematic if mutation deletes a subtree,
in that case `fiber.alternate.stateNode` will retain entire subtree
until next update. In case of image nodes, this means retaining entire
images.

So when React goes from revision A: `<View><View /></View>` to revision
B: `<View />`, `fiber.alternate.stateNode` will be pointing to Shadow
Node that represents revision A..

![image](https://github.com/user-attachments/assets/076b677e-d152-4763-8c9d-4f923212b424)

To fix this, this PR adds a new feature flag
`enableEagerAlternateStateNodeCleanup`. When enabled,
`alternate.stateNode` is proactively pointed towards finishedWork's
stateNode, releasing resources sooner.

I have verified this fixes the issue [demonstrated by React Native
tests](https://github.com/facebook/react-native/blob/main/packages/react-native/src/private/__tests__/utilities/__tests__/ShadowNodeReferenceCounter-itest.js#L169).
All existing React tests pass when the flag is enabled.
2025-07-15 15:24:50 -04:00
Jack Pope
a24654e65b Ship enableFabricCompleteRootInCommitPhase (#33064)
This was shipped internally. Cleaning up the flag.
2025-07-15 15:16:15 -04:00
40 changed files with 261 additions and 175 deletions

View File

@@ -6,6 +6,12 @@ on:
pull_request:
paths-ignore:
- compiler/**
workflow_dispatch:
inputs:
commit_sha:
required: false
type: string
default: ''
permissions: {}
@@ -28,7 +34,7 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
- name: Check cache hit
uses: actions/cache/restore@v4
id: node_modules
@@ -69,7 +75,7 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
- name: Check cache hit
uses: actions/cache/restore@v4
id: node_modules
@@ -117,7 +123,7 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
- uses: actions/github-script@v7
id: set-matrix
with:
@@ -136,7 +142,7 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
@@ -166,7 +172,7 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
@@ -198,7 +204,7 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
@@ -254,7 +260,7 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
@@ -294,7 +300,7 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
@@ -389,7 +395,7 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
@@ -430,7 +436,7 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
@@ -458,7 +464,7 @@ jobs:
merge-multiple: true
- name: Display structure of build
run: ls -R build
- run: echo ${{ github.event.pull_request.head.sha || github.sha }} >> build/COMMIT_SHA
- run: echo ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }} >> build/COMMIT_SHA
- name: Scrape warning messages
run: |
mkdir -p ./build/__test_utils__
@@ -483,7 +489,7 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
@@ -523,7 +529,7 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
@@ -560,7 +566,7 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
@@ -601,7 +607,7 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
@@ -675,7 +681,7 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
@@ -732,7 +738,7 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
@@ -777,7 +783,7 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
ref: ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
@@ -822,7 +828,7 @@ jobs:
node ./scripts/print-warnings/print-warnings.js > build/__test_utils__/ReactAllWarnings.js
- name: Display structure of build for PR
run: ls -R build
- run: echo ${{ github.event.pull_request.head.sha || github.sha }} >> build/COMMIT_SHA
- run: echo ${{ github.event.inputs.commit_sha != '' && github.event.inputs.commit_sha || github.event.pull_request.head.sha || github.sha }} >> build/COMMIT_SHA
- run: node ./scripts/tasks/danger
- name: Archive sizebot results
uses: actions/upload-artifact@v4

View File

@@ -18,7 +18,7 @@
//
// 0.0.0-experimental-241c4467e-20200129
const ReactVersion = '19.1.0';
const ReactVersion = '19.1.1';
// The label used by the @canary channel. Represents the upcoming release's
// stability. Most of the time, this will be "canary", but we may temporarily
@@ -34,7 +34,7 @@ const rcNumber = 0;
const stablePackages = {
'eslint-plugin-react-hooks': '5.2.0',
'jest-react': '0.17.0',
'jest-react': '0.16.0',
react: ReactVersion,
'react-art': ReactVersion,
'react-dom': ReactVersion,

View File

@@ -1,7 +1,7 @@
{
"name": "react-art",
"description": "React ART is a JavaScript library for drawing vector graphics using React. It provides declarative and reactive bindings to the ART library. Using the same declarative API you can render the output to either Canvas, SVG or VML (IE8).",
"version": "19.1.0",
"version": "19.1.1",
"main": "index.js",
"repository": {
"type": "git",
@@ -24,10 +24,10 @@
"dependencies": {
"art": "^0.10.1",
"create-react-class": "^15.6.2",
"scheduler": "^0.25.0"
"scheduler": "^0.26.0"
},
"peerDependencies": {
"react": "^19.0.0"
"react": "^19.1.1"
},
"files": [
"LICENSE",
@@ -38,4 +38,4 @@
"Rectangle.js",
"Wedge.js"
]
}
}

View File

@@ -2438,7 +2438,7 @@ function buildFakeTask(
}
const createFakeJSXCallStack = {
'react-stack-bottom-frame': function (
react_stack_bottom_frame: function (
response: Response,
stack: ReactStackTrace,
environmentName: string,
@@ -2459,7 +2459,7 @@ const createFakeJSXCallStackInDEV: (
environmentName: string,
) => Error = __DEV__
? // We use this technique to trick minifiers to preserve the function name.
(createFakeJSXCallStack['react-stack-bottom-frame'].bind(
(createFakeJSXCallStack.react_stack_bottom_frame.bind(
createFakeJSXCallStack,
): any)
: (null: any);
@@ -2563,7 +2563,7 @@ function getCurrentStackInDEV(): string {
}
const replayConsoleWithCallStack = {
'react-stack-bottom-frame': function (
react_stack_bottom_frame: function (
response: Response,
methodName: string,
stackTrace: ReactStackTrace,
@@ -2614,7 +2614,7 @@ const replayConsoleWithCallStackInDEV: (
args: Array<mixed>,
) => void = __DEV__
? // We use this technique to trick minifiers to preserve the function name.
(replayConsoleWithCallStack['react-stack-bottom-frame'].bind(
(replayConsoleWithCallStack.react_stack_bottom_frame.bind(
replayConsoleWithCallStack,
): any)
: (null: any);

View File

@@ -50,6 +50,7 @@ import {
gt,
gte,
parseSourceFromComponentStack,
parseSourceFromOwnerStack,
serializeToString,
} from 'react-devtools-shared/src/backend/utils';
import {
@@ -5758,15 +5759,13 @@ export function attach(
function getSourceForFiberInstance(
fiberInstance: FiberInstance,
): Source | null {
const unresolvedSource = fiberInstance.source;
if (
unresolvedSource !== null &&
typeof unresolvedSource === 'object' &&
!isError(unresolvedSource)
) {
// $FlowFixMe: isError should have refined it.
return unresolvedSource;
// Favor the owner source if we have one.
const ownerSource = getSourceForInstance(fiberInstance);
if (ownerSource !== null) {
return ownerSource;
}
// Otherwise fallback to the throwing trick.
const dispatcherRef = getDispatcherRef(renderer);
const stackFrame =
dispatcherRef == null
@@ -5777,10 +5776,7 @@ export function attach(
dispatcherRef,
);
if (stackFrame === null) {
// If we don't find a source location by throwing, try to get one
// from an owned child if possible. This is the same branch as
// for virtual instances.
return getSourceForInstance(fiberInstance);
return null;
}
const source = parseSourceFromComponentStack(stackFrame);
fiberInstance.source = source;
@@ -5788,7 +5784,7 @@ export function attach(
}
function getSourceForInstance(instance: DevToolsInstance): Source | null {
let unresolvedSource = instance.source;
const unresolvedSource = instance.source;
if (unresolvedSource === null) {
// We don't have any source yet. We can try again later in case an owned child mounts later.
// TODO: We won't have any information here if the child is filtered.
@@ -5801,7 +5797,9 @@ export function attach(
// any intermediate utility functions. This won't point to the top of the component function
// but it's at least somewhere within it.
if (isError(unresolvedSource)) {
unresolvedSource = formatOwnerStack((unresolvedSource: any));
return (instance.source = parseSourceFromOwnerStack(
(unresolvedSource: any),
));
}
if (typeof unresolvedSource === 'string') {
const idx = unresolvedSource.lastIndexOf('\n');

View File

@@ -13,8 +13,12 @@ export function formatOwnerStack(error: Error): string {
const prevPrepareStackTrace = Error.prepareStackTrace;
// $FlowFixMe[incompatible-type] It does accept undefined.
Error.prepareStackTrace = undefined;
let stack = error.stack;
const stack = error.stack;
Error.prepareStackTrace = prevPrepareStackTrace;
return formatOwnerStackString(stack);
}
export function formatOwnerStackString(stack: string): string {
if (stack.startsWith('Error: react-stack-top-frame\n')) {
// V8's default formatting prefixes with the error message which we
// don't want/need.
@@ -25,7 +29,10 @@ export function formatOwnerStack(error: Error): string {
// Pop the JSX frame.
stack = stack.slice(idx + 1);
}
idx = stack.indexOf('react-stack-bottom-frame');
idx = stack.indexOf('react_stack_bottom_frame');
if (idx === -1) {
idx = stack.indexOf('react-stack-bottom-frame');
}
if (idx !== -1) {
idx = stack.lastIndexOf('\n', idx);
}

View File

@@ -18,6 +18,8 @@ import type {DehydratedData} from 'react-devtools-shared/src/frontend/types';
export {default as formatWithStyles} from './formatWithStyles';
export {default as formatConsoleArguments} from './formatConsoleArguments';
import {formatOwnerStackString} from '../shared/DevToolsOwnerStack';
// TODO: update this to the first React version that has a corresponding DevTools backend
const FIRST_DEVTOOLS_BACKEND_LOCKSTEP_VER = '999.9.9';
export function hasAssignedBackend(version?: string): boolean {
@@ -345,6 +347,81 @@ export function parseSourceFromComponentStack(
return parseSourceFromFirefoxStack(componentStack);
}
let collectedLocation: Source | null = null;
function collectStackTrace(
error: Error,
structuredStackTrace: CallSite[],
): string {
let result: null | Source = null;
// Collect structured stack traces from the callsites.
// We mirror how V8 serializes stack frames and how we later parse them.
for (let i = 0; i < structuredStackTrace.length; i++) {
const callSite = structuredStackTrace[i];
const name = callSite.getFunctionName();
if (
name.includes('react_stack_bottom_frame') ||
name.includes('react-stack-bottom-frame')
) {
// We pick the last frame that matches before the bottom frame since
// that will be immediately inside the component as opposed to some helper.
// If we don't find a bottom frame then we bail to string parsing.
collectedLocation = result;
// Skip everything after the bottom frame since it'll be internals.
break;
} else {
const sourceURL = callSite.getScriptNameOrSourceURL();
const line =
// $FlowFixMe[prop-missing]
typeof callSite.getEnclosingLineNumber === 'function'
? (callSite: any).getEnclosingLineNumber()
: callSite.getLineNumber();
const col =
// $FlowFixMe[prop-missing]
typeof callSite.getEnclosingColumnNumber === 'function'
? (callSite: any).getEnclosingColumnNumber()
: callSite.getLineNumber();
if (!sourceURL || !line || !col) {
// Skip eval etc. without source url. They don't have location.
continue;
}
result = {
sourceURL,
line: line,
column: col,
};
}
}
// At the same time we generate a string stack trace just in case someone
// else reads it.
const name = error.name || 'Error';
const message = error.message || '';
let stack = name + ': ' + message;
for (let i = 0; i < structuredStackTrace.length; i++) {
stack += '\n at ' + structuredStackTrace[i].toString();
}
return stack;
}
export function parseSourceFromOwnerStack(error: Error): Source | null {
// First attempt to collected the structured data using prepareStackTrace.
collectedLocation = null;
const previousPrepare = Error.prepareStackTrace;
Error.prepareStackTrace = collectStackTrace;
let stack;
try {
stack = error.stack;
} finally {
Error.prepareStackTrace = previousPrepare;
}
if (collectedLocation !== null) {
return collectedLocation;
}
// Fallback to parsing the string form.
const componentStack = formatOwnerStackString(stack);
return parseSourceFromComponentStack(componentStack);
}
// 0.123456789 => 0.123
// Expects high-resolution timestamp in milliseconds, like from performance.now()
// Mainly used for optimizing the size of serialized profiling payload

View File

@@ -1,7 +1,7 @@
{
"name": "react-dom-bindings",
"description": "React implementation details for react-dom.",
"version": "19.1.0",
"version": "19.1.1",
"private": true,
"main": "index.js",
"repository": {
@@ -18,6 +18,6 @@
},
"homepage": "https://react.dev/",
"peerDependencies": {
"react": "^19.0.0"
"react": "^19.0.1"
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "react-dom",
"version": "19.1.0",
"version": "19.1.1",
"description": "React package for working with the DOM.",
"main": "index.js",
"repository": {
@@ -17,10 +17,10 @@
},
"homepage": "https://react.dev/",
"dependencies": {
"scheduler": "^0.25.0"
"scheduler": "^0.26.0"
},
"peerDependencies": {
"react": "^19.0.0"
"react": "^19.1.1"
},
"files": [
"LICENSE",
@@ -123,4 +123,4 @@
"./server.js": "./server.browser.js",
"./static.js": "./static.browser.js"
}
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "react-is",
"version": "19.1.0",
"version": "19.1.1",
"description": "Brand checking of React Elements.",
"main": "index.js",
"sideEffects": false,

View File

@@ -1,6 +1,6 @@
{
"name": "react-markup",
"version": "19.1.0",
"version": "19.1.1",
"description": "React package generating embedded markup such as e-mails with support for Server Components.",
"main": "index.js",
"repository": {
@@ -17,7 +17,7 @@
},
"homepage": "https://react.dev/",
"peerDependencies": {
"react": "^19.0.0"
"react": "^19.0.1"
},
"files": [
"LICENSE",

View File

@@ -8,9 +8,9 @@
"directory": "packages/react-native-renderer"
},
"dependencies": {
"scheduler": "^0.25.0"
"scheduler": "^0.26.0"
},
"peerDependencies": {
"react": "^18.0.0"
}
}
}

View File

@@ -58,7 +58,6 @@ import {
} from './ReactNativeFiberInspector';
import {
enableFabricCompleteRootInCommitPhase,
passChildrenWhenCloningPersistedNodes,
enableLazyPublicInstanceInFabric,
} from 'shared/ReactFeatureFlags';
@@ -534,19 +533,14 @@ export function finalizeContainerChildren(
container: Container,
newChildren: ChildSet,
): void {
if (!enableFabricCompleteRootInCommitPhase) {
completeRoot(container.containerTag, newChildren);
}
// Noop - children will be replaced in replaceContainerChildren
}
export function replaceContainerChildren(
container: Container,
newChildren: ChildSet,
): void {
// Noop - children will be replaced in finalizeContainerChildren
if (enableFabricCompleteRootInCommitPhase) {
completeRoot(container.containerTag, newChildren);
}
completeRoot(container.containerTag, newChildren);
}
export {getClosestInstanceFromNode as getInstanceFromNode};

View File

@@ -1,7 +1,7 @@
{
"name": "react-reconciler",
"description": "React package for creating custom renderers.",
"version": "0.32.0",
"version": "0.32.1",
"keywords": [
"react"
],
@@ -26,9 +26,9 @@
"node": ">=0.10.0"
},
"peerDependencies": {
"react": "^19.0.0"
"react": "^19.1.1"
},
"dependencies": {
"scheduler": "^0.25.0"
"scheduler": "^0.26.0"
}
}
}

View File

@@ -24,7 +24,7 @@ import {enableUseEffectCRUDOverload} from 'shared/ReactFeatureFlags';
// TODO: Consider marking the whole bundle instead of these boundaries.
const callComponent = {
'react-stack-bottom-frame': function <Props, Arg, R>(
react_stack_bottom_frame: function <Props, Arg, R>(
Component: (p: Props, arg: Arg) => R,
props: Props,
secondArg: Arg,
@@ -46,7 +46,7 @@ export const callComponentInDEV: <Props, Arg, R>(
secondArg: Arg,
) => R = __DEV__
? // We use this technique to trick minifiers to preserve the function name.
(callComponent['react-stack-bottom-frame'].bind(callComponent): any)
(callComponent.react_stack_bottom_frame.bind(callComponent): any)
: (null: any);
interface ClassInstance<R> {
@@ -62,7 +62,7 @@ interface ClassInstance<R> {
}
const callRender = {
'react-stack-bottom-frame': function <R>(instance: ClassInstance<R>): R {
react_stack_bottom_frame: function <R>(instance: ClassInstance<R>): R {
const wasRendering = isRendering;
setIsRendering(true);
try {
@@ -77,11 +77,11 @@ const callRender = {
export const callRenderInDEV: <R>(instance: ClassInstance<R>) => R => R =
__DEV__
? // We use this technique to trick minifiers to preserve the function name.
(callRender['react-stack-bottom-frame'].bind(callRender): any)
(callRender.react_stack_bottom_frame.bind(callRender): any)
: (null: any);
const callComponentDidMount = {
'react-stack-bottom-frame': function (
react_stack_bottom_frame: function (
finishedWork: Fiber,
instance: ClassInstance<any>,
): void {
@@ -98,13 +98,13 @@ export const callComponentDidMountInDEV: (
instance: ClassInstance<any>,
) => void = __DEV__
? // We use this technique to trick minifiers to preserve the function name.
(callComponentDidMount['react-stack-bottom-frame'].bind(
(callComponentDidMount.react_stack_bottom_frame.bind(
callComponentDidMount,
): any)
: (null: any);
const callComponentDidUpdate = {
'react-stack-bottom-frame': function (
react_stack_bottom_frame: function (
finishedWork: Fiber,
instance: ClassInstance<any>,
prevProps: Object,
@@ -127,13 +127,13 @@ export const callComponentDidUpdateInDEV: (
snaphot: Object,
) => void = __DEV__
? // We use this technique to trick minifiers to preserve the function name.
(callComponentDidUpdate['react-stack-bottom-frame'].bind(
(callComponentDidUpdate.react_stack_bottom_frame.bind(
callComponentDidUpdate,
): any)
: (null: any);
const callComponentDidCatch = {
'react-stack-bottom-frame': function (
react_stack_bottom_frame: function (
instance: ClassInstance<any>,
errorInfo: CapturedValue<mixed>,
): void {
@@ -150,13 +150,13 @@ export const callComponentDidCatchInDEV: (
errorInfo: CapturedValue<mixed>,
) => void = __DEV__
? // We use this technique to trick minifiers to preserve the function name.
(callComponentDidCatch['react-stack-bottom-frame'].bind(
(callComponentDidCatch.react_stack_bottom_frame.bind(
callComponentDidCatch,
): any)
: (null: any);
const callComponentWillUnmount = {
'react-stack-bottom-frame': function (
react_stack_bottom_frame: function (
current: Fiber,
nearestMountedAncestor: Fiber | null,
instance: ClassInstance<any>,
@@ -175,13 +175,13 @@ export const callComponentWillUnmountInDEV: (
instance: ClassInstance<any>,
) => void = __DEV__
? // We use this technique to trick minifiers to preserve the function name.
(callComponentWillUnmount['react-stack-bottom-frame'].bind(
(callComponentWillUnmount.react_stack_bottom_frame.bind(
callComponentWillUnmount,
): any)
: (null: any);
const callCreate = {
'react-stack-bottom-frame': function (
react_stack_bottom_frame: function (
effect: Effect,
): (() => void) | {...} | void | null {
if (!enableUseEffectCRUDOverload) {
@@ -234,11 +234,11 @@ const callCreate = {
export const callCreateInDEV: (effect: Effect) => (() => void) | void = __DEV__
? // We use this technique to trick minifiers to preserve the function name.
(callCreate['react-stack-bottom-frame'].bind(callCreate): any)
(callCreate.react_stack_bottom_frame.bind(callCreate): any)
: (null: any);
const callDestroy = {
'react-stack-bottom-frame': function (
react_stack_bottom_frame: function (
current: Fiber,
nearestMountedAncestor: Fiber | null,
destroy: () => void,
@@ -257,11 +257,11 @@ export const callDestroyInDEV: (
destroy: (() => void) | (({...}) => void),
) => void = __DEV__
? // We use this technique to trick minifiers to preserve the function name.
(callDestroy['react-stack-bottom-frame'].bind(callDestroy): any)
(callDestroy.react_stack_bottom_frame.bind(callDestroy): any)
: (null: any);
const callLazyInit = {
'react-stack-bottom-frame': function (lazy: LazyComponent<any, any>): any {
react_stack_bottom_frame: function (lazy: LazyComponent<any, any>): any {
const payload = lazy._payload;
const init = lazy._init;
return init(payload);
@@ -270,5 +270,5 @@ const callLazyInit = {
export const callLazyInitInDEV: (lazy: LazyComponent<any, any>) => any = __DEV__
? // We use this technique to trick minifiers to preserve the function name.
(callLazyInit['react-stack-bottom-frame'].bind(callLazyInit): any)
(callLazyInit.react_stack_bottom_frame.bind(callLazyInit): any)
: (null: any);

View File

@@ -57,6 +57,7 @@ import {
enableComponentPerformanceTrack,
enableViewTransition,
enableFragmentRefs,
enableEagerAlternateStateNodeCleanup,
} from 'shared/ReactFeatureFlags';
import {
FunctionComponent,
@@ -1947,6 +1948,20 @@ function commitMutationEffectsOnFiber(
}
}
}
} else {
if (enableEagerAlternateStateNodeCleanup) {
if (supportsPersistence) {
if (finishedWork.alternate !== null) {
// `finishedWork.alternate.stateNode` is pointing to a stale shadow
// node at this point, retaining it and its subtree. To reclaim
// memory, point `alternate.stateNode` to new shadow node. This
// prevents shadow node from staying in memory longer than it
// needs to. The correct behaviour of this is checked by test in
// React Native: ShadowNodeReferenceCounter-itest.js#L150
finishedWork.alternate.stateNode = finishedWork.stateNode;
}
}
}
}
break;
}

View File

@@ -1,7 +1,7 @@
{
"name": "react-server-dom-esm",
"description": "React Server Components bindings for DOM using ESM. This is intended to be integrated into meta-frameworks. It is not intended to be imported directly.",
"version": "19.1.0",
"version": "19.1.1",
"keywords": [
"react"
],
@@ -46,8 +46,8 @@
},
"main": "index.js",
"repository": {
"type" : "git",
"url" : "https://github.com/facebook/react.git",
"type": "git",
"url": "https://github.com/facebook/react.git",
"directory": "packages/react-server-dom-esm"
},
"engines": {

View File

@@ -8,10 +8,10 @@
"directory": "packages/react-server-dom-fb"
},
"dependencies": {
"scheduler": "^0.25.0"
"scheduler": "^0.26.0"
},
"peerDependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0"
}
}
}

View File

@@ -1,7 +1,7 @@
{
"name": "react-server-dom-parcel",
"description": "React Server Components bindings for DOM using Parcel. This is intended to be integrated into meta-frameworks. It is not intended to be imported directly.",
"version": "19.0.0",
"version": "19.1.1",
"keywords": [
"react"
],
@@ -71,15 +71,15 @@
},
"main": "index.js",
"repository": {
"type" : "git",
"url" : "https://github.com/facebook/react.git",
"type": "git",
"url": "https://github.com/facebook/react.git",
"directory": "packages/react-server-dom-parcel"
},
"engines": {
"node": ">=0.10.0"
},
"peerDependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0"
"react": "^19.1.1",
"react-dom": "^19.1.1"
}
}

View File

@@ -1,7 +1,7 @@
{
"name": "react-server-dom-turbopack",
"description": "React Server Components bindings for DOM using Turbopack. This is intended to be integrated into meta-frameworks. It is not intended to be imported directly.",
"version": "19.1.0",
"version": "19.1.1",
"keywords": [
"react"
],
@@ -79,8 +79,8 @@
"node": ">=0.10.0"
},
"peerDependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0"
"react": "^19.1.1",
"react-dom": "^19.1.1"
},
"dependencies": {
"acorn-loose": "^8.3.0",

View File

@@ -1,7 +1,7 @@
{
"name": "react-server-dom-webpack",
"description": "React Server Components bindings for DOM using Webpack. This is intended to be integrated into meta-frameworks. It is not intended to be imported directly.",
"version": "19.1.0",
"version": "19.1.1",
"keywords": [
"react"
],
@@ -100,8 +100,8 @@
"node": ">=0.10.0"
},
"peerDependencies": {
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"webpack": "^5.59.0"
},
"dependencies": {

View File

@@ -13,7 +13,7 @@ import type {LazyComponent} from 'react/src/ReactLazy';
// TODO: Consider marking the whole bundle instead of these boundaries.
const callComponent = {
'react-stack-bottom-frame': function <Props, Arg, R>(
react_stack_bottom_frame: function <Props, Arg, R>(
Component: (p: Props, arg: Arg) => R,
props: Props,
secondArg: Arg,
@@ -28,7 +28,7 @@ export const callComponentInDEV: <Props, Arg, R>(
secondArg: Arg,
) => R = __DEV__
? // We use this technique to trick minifiers to preserve the function name.
(callComponent['react-stack-bottom-frame'].bind(callComponent): any)
(callComponent.react_stack_bottom_frame.bind(callComponent): any)
: (null: any);
interface ClassInstance<R> {
@@ -36,7 +36,7 @@ interface ClassInstance<R> {
}
const callRender = {
'react-stack-bottom-frame': function <R>(instance: ClassInstance<R>): R {
react_stack_bottom_frame: function <R>(instance: ClassInstance<R>): R {
return instance.render();
},
};
@@ -44,11 +44,11 @@ const callRender = {
export const callRenderInDEV: <R>(instance: ClassInstance<R>) => R => R =
__DEV__
? // We use this technique to trick minifiers to preserve the function name.
(callRender['react-stack-bottom-frame'].bind(callRender): any)
(callRender.react_stack_bottom_frame.bind(callRender): any)
: (null: any);
const callLazyInit = {
'react-stack-bottom-frame': function (lazy: LazyComponent<any, any>): any {
react_stack_bottom_frame: function (lazy: LazyComponent<any, any>): any {
const payload = lazy._payload;
const init = lazy._init;
return init(payload);
@@ -57,5 +57,5 @@ const callLazyInit = {
export const callLazyInitInDEV: (lazy: LazyComponent<any, any>) => any = __DEV__
? // We use this technique to trick minifiers to preserve the function name.
(callLazyInit['react-stack-bottom-frame'].bind(callLazyInit): any)
(callLazyInit.react_stack_bottom_frame.bind(callLazyInit): any)
: (null: any);

View File

@@ -19,7 +19,7 @@ import {setCurrentOwner} from './flight/ReactFlightCurrentOwner';
// TODO: Consider marking the whole bundle instead of these boundaries.
const callComponent = {
'react-stack-bottom-frame': function <Props, R>(
react_stack_bottom_frame: function <Props, R>(
Component: (p: Props, arg: void) => R,
props: Props,
componentDebugInfo: ReactComponentInfo,
@@ -41,11 +41,11 @@ export const callComponentInDEV: <Props, R>(
componentDebugInfo: ReactComponentInfo,
) => R = __DEV__
? // We use this technique to trick minifiers to preserve the function name.
(callComponent['react-stack-bottom-frame'].bind(callComponent): any)
(callComponent.react_stack_bottom_frame.bind(callComponent): any)
: (null: any);
const callLazyInit = {
'react-stack-bottom-frame': function (lazy: LazyComponent<any, any>): any {
react_stack_bottom_frame: function (lazy: LazyComponent<any, any>): any {
const payload = lazy._payload;
const init = lazy._init;
return init(payload);
@@ -54,11 +54,11 @@ const callLazyInit = {
export const callLazyInitInDEV: (lazy: LazyComponent<any, any>) => any = __DEV__
? // We use this technique to trick minifiers to preserve the function name.
(callLazyInit['react-stack-bottom-frame'].bind(callLazyInit): any)
(callLazyInit.react_stack_bottom_frame.bind(callLazyInit): any)
: (null: any);
const callIterator = {
'react-stack-bottom-frame': function (
react_stack_bottom_frame: function (
iterator: $AsyncIterator<ReactClientValue, ReactClientValue, void>,
progress: (
entry:
@@ -81,5 +81,5 @@ export const callIteratorInDEV: (
error: (reason: mixed) => void,
) => void = __DEV__
? // We use this technique to trick minifiers to preserve the function name.
(callIterator['react-stack-bottom-frame'].bind(callIterator): any)
(callIterator.react_stack_bottom_frame.bind(callIterator): any)
: (null: any);

View File

@@ -45,7 +45,7 @@ export function parseStackTrace(
// don't want/need.
stack = stack.slice(29);
}
let idx = stack.indexOf('react-stack-bottom-frame');
let idx = stack.indexOf('react_stack_bottom_frame');
if (idx !== -1) {
idx = stack.lastIndexOf('\n', idx);
}

View File

@@ -1,6 +1,6 @@
{
"name": "react-test-renderer",
"version": "19.1.0",
"version": "19.1.1",
"description": "React package for snapshot testing.",
"main": "index.js",
"repository": {
@@ -19,11 +19,11 @@
},
"homepage": "https://react.dev/",
"dependencies": {
"react-is": "^19.0.0",
"scheduler": "^0.25.0"
"react-is": "^19.1.1",
"scheduler": "^0.26.0"
},
"peerDependencies": {
"react": "^19.0.0"
"react": "^19.1.1"
},
"files": [
"LICENSE",
@@ -32,4 +32,4 @@
"shallow.js",
"cjs/"
]
}
}

View File

@@ -4,7 +4,7 @@
"keywords": [
"react"
],
"version": "19.1.0",
"version": "19.1.1",
"homepage": "https://react.dev/",
"bugs": "https://github.com/facebook/react/issues",
"license": "MIT",

View File

@@ -66,7 +66,7 @@ function UnknownOwner() {
return (() => Error('react-stack-top-frame'))();
}
const createFakeCallStack = {
'react-stack-bottom-frame': function (callStackForError) {
react_stack_bottom_frame: function (callStackForError) {
return callStackForError();
},
};
@@ -81,7 +81,7 @@ if (__DEV__) {
didWarnAboutElementRef = {};
// We use this technique to trick minifiers to preserve the function name.
unknownOwnerDebugStack = createFakeCallStack['react-stack-bottom-frame'].bind(
unknownOwnerDebugStack = createFakeCallStack.react_stack_bottom_frame.bind(
createFakeCallStack,
UnknownOwner,
)();

View File

@@ -1,6 +1,6 @@
{
"name": "scheduler",
"version": "0.25.0",
"version": "0.26.0",
"description": "Cooperative scheduler for the browser environment.",
"repository": {
"type": "git",
@@ -24,4 +24,4 @@
"unstable_post_task.js",
"cjs/"
]
}
}

View File

@@ -96,11 +96,6 @@ export const enableSwipeTransition = __EXPERIMENTAL__;
export const enableScrollEndPolyfill = __EXPERIMENTAL__;
/**
* Switches the Fabric API from doing layout in commit work instead of complete work.
*/
export const enableFabricCompleteRootInCommitPhase = false;
/**
* Switches Fiber creation to a simple object instead of a constructor.
*/
@@ -140,6 +135,8 @@ export const enableShallowPropDiffing = false;
export const enableSiblingPrerendering = true;
export const enableEagerAlternateStateNodeCleanup = true;
/**
* Enables an expiration time for retry lanes to avoid starvation.
*/

View File

@@ -24,7 +24,7 @@ export function formatOwnerStack(error: Error): string {
// Pop the JSX frame.
stack = stack.slice(idx + 1);
}
idx = stack.indexOf('react-stack-bottom-frame');
idx = stack.indexOf('react_stack_bottom_frame');
if (idx !== -1) {
idx = stack.lastIndexOf('\n', idx);
}

View File

@@ -12,4 +12,4 @@
// TODO: This module is used both by the release scripts and to expose a version
// at runtime. We should instead inject the version number as part of the build
// process, and use the ReactVersions.js module as the single source of truth.
export default '19.1.0';
export default '19.1.1';

View File

@@ -22,8 +22,8 @@ export const enableObjectFiber = __VARIANT__;
export const enableHiddenSubtreeInsertionEffectCleanup = __VARIANT__;
export const enablePersistedModeClonedFlag = __VARIANT__;
export const enableShallowPropDiffing = __VARIANT__;
export const enableEagerAlternateStateNodeCleanup = __VARIANT__;
export const passChildrenWhenCloningPersistedNodes = __VARIANT__;
export const enableFabricCompleteRootInCommitPhase = __VARIANT__;
export const enableSiblingPrerendering = __VARIANT__;
export const enableUseEffectCRUDOverload = __VARIANT__;
export const enableFastAddPropertiesInDiffing = __VARIANT__;

View File

@@ -20,12 +20,12 @@ const dynamicFlags: DynamicExportsType = (dynamicFlagsUntyped: any);
// the exports object every time a flag is read.
export const {
alwaysThrottleRetries,
enableFabricCompleteRootInCommitPhase,
enableHiddenSubtreeInsertionEffectCleanup,
enableObjectFiber,
enablePersistedModeClonedFlag,
enableShallowPropDiffing,
enableUseEffectCRUDOverload,
enableEagerAlternateStateNodeCleanup,
passChildrenWhenCloningPersistedNodes,
enableSiblingPrerendering,
enableFastAddPropertiesInDiffing,

View File

@@ -30,7 +30,6 @@ export const enableAsyncIterableChildren = false;
export const enableCPUSuspense = false;
export const enableCreateEventHandleAPI = false;
export const enableDO_NOT_USE_disableStrictPassiveEffect = false;
export const enableFabricCompleteRootInCommitPhase = false;
export const enableMoveBefore = true;
export const enableFizzExternalRuntime = true;
export const enableHalt = false;
@@ -50,6 +49,7 @@ export const enableSchedulingProfiler = __PROFILE__;
export const enableComponentPerformanceTrack = false;
export const enableScopeAPI = false;
export const enableShallowPropDiffing = false;
export const enableEagerAlternateStateNodeCleanup = true;
export const enableSuspenseAvoidThisFallback = false;
export const enableSuspenseCallback = false;
export const enableTaint = true;

View File

@@ -36,7 +36,6 @@ export const enableUseEffectEventHook = false;
export const favorSafetyOverHydrationPerf = true;
export const enableLegacyFBSupport = false;
export const enableMoveBefore = false;
export const enableFabricCompleteRootInCommitPhase = false;
export const enableHiddenSubtreeInsertionEffectCleanup = false;
export const enableHydrationLaneScheduling = true;
@@ -66,6 +65,7 @@ export const enableShallowPropDiffing = false;
export const enableSiblingPrerendering = true;
export const enableUseEffectCRUDOverload = false;
export const enableEagerAlternateStateNodeCleanup = true;
export const enableYieldingBeforePassive = true;

View File

@@ -47,6 +47,7 @@ export const enableSchedulingProfiler = __PROFILE__;
export const enableComponentPerformanceTrack = false;
export const enableScopeAPI = false;
export const enableShallowPropDiffing = false;
export const enableEagerAlternateStateNodeCleanup = true;
export const enableSuspenseAvoidThisFallback = false;
export const enableSuspenseCallback = false;
export const enableTaint = true;
@@ -60,7 +61,6 @@ export const renameElementSymbol = false;
export const retryLaneExpirationMs = 5000;
export const syncLaneExpirationMs = 250;
export const transitionLaneExpirationMs = 5000;
export const enableFabricCompleteRootInCommitPhase = false;
export const enableSiblingPrerendering = true;
export const enableUseEffectCRUDOverload = true;
export const enableHydrationLaneScheduling = true;

View File

@@ -39,7 +39,6 @@ export const favorSafetyOverHydrationPerf = true;
export const enableLegacyFBSupport = false;
export const enableMoveBefore = false;
export const enableRenderableContext = false;
export const enableFabricCompleteRootInCommitPhase = false;
export const enableHiddenSubtreeInsertionEffectCleanup = true;
export const enableRetryLaneExpiration = false;
@@ -75,6 +74,7 @@ export const enableShallowPropDiffing = false;
export const enableSiblingPrerendering = true;
export const enableUseEffectCRUDOverload = false;
export const enableEagerAlternateStateNodeCleanup = true;
export const enableHydrationLaneScheduling = true;

View File

@@ -50,7 +50,6 @@ export const enableProfilerTimer = __PROFILE__;
export const enableProfilerCommitHooks = __PROFILE__;
export const enableProfilerNestedUpdatePhase = __PROFILE__;
export const enableUpdaterTracking = __PROFILE__;
export const enableFabricCompleteRootInCommitPhase = false;
export const enableSuspenseAvoidThisFallback = true;
@@ -111,6 +110,8 @@ export const disableLegacyMode = true;
export const enableShallowPropDiffing = false;
export const enableEagerAlternateStateNodeCleanup = true;
export const enableLazyPublicInstanceInFabric = false;
export const enableSwipeTransition = false;

View File

@@ -21,6 +21,6 @@
"rxjs": "^5.5.6"
},
"dependencies": {
"use-sync-external-store": "^1.4.0"
"use-sync-external-store": "^1.5.0"
}
}

View File

@@ -221,8 +221,7 @@ function processStable(buildDir) {
);
}
const rnVersionString =
ReactVersion + '-native-fb-' + sha + '-' + dateString;
const rnVersionString = ReactVersion + '-native-fb-' + sha + '-' + dateString;
if (fs.existsSync(buildDir + '/facebook-react-native')) {
updatePlaceholderReactVersionInCompiledArtifacts(
buildDir + '/facebook-react-native',
@@ -231,9 +230,16 @@ function processStable(buildDir) {
}
if (fs.existsSync(buildDir + '/react-native')) {
updatePlaceholderReactVersionInCompiledArtifactsFb(
updatePlaceholderReactVersionInCompiledArtifacts(
buildDir + '/react-native',
rnVersionString
rnVersionString,
(filename) => filename.endsWith('.fb.js')
);
updatePlaceholderReactVersionInCompiledArtifacts(
buildDir + '/react-native',
ReactVersion,
(filename) => !filename.endsWith('.fb.js') && filename.endsWith('.js')
);
}
@@ -340,9 +346,16 @@ function processExperimental(buildDir, version) {
}
if (fs.existsSync(buildDir + '/react-native')) {
updatePlaceholderReactVersionInCompiledArtifactsFb(
updatePlaceholderReactVersionInCompiledArtifacts(
buildDir + '/react-native',
rnVersionString
rnVersionString,
(filename) => filename.endsWith('.fb.js')
);
updatePlaceholderReactVersionInCompiledArtifacts(
buildDir + '/react-native',
ReactVersion,
(filename) => !filename.endsWith('.fb.js') && filename.endsWith('.js')
);
}
@@ -437,10 +450,15 @@ function updatePackageVersions(
function updatePlaceholderReactVersionInCompiledArtifacts(
artifactsDirectory,
newVersion
newVersion,
filteringClosure
) {
// Update the version of React in the compiled artifacts by searching for
// the placeholder string and replacing it with a new one.
if (filteringClosure == null) {
filteringClosure = filename => filename.endsWith('.js')
}
const artifactFilenames = String(
spawnSync('grep', [
'-lr',
@@ -451,7 +469,7 @@ function updatePlaceholderReactVersionInCompiledArtifacts(
)
.trim()
.split('\n')
.filter(filename => filename.endsWith('.js'));
.filter(filteringClosure);
for (const artifactFilename of artifactFilenames) {
const originalText = fs.readFileSync(artifactFilename, 'utf8');
@@ -463,33 +481,6 @@ function updatePlaceholderReactVersionInCompiledArtifacts(
}
}
function updatePlaceholderReactVersionInCompiledArtifactsFb(
artifactsDirectory,
newVersion
) {
// Update the version of React in the compiled artifacts by searching for
// the placeholder string and replacing it with a new one.
const artifactFilenames = String(
spawnSync('grep', [
'-lr',
PLACEHOLDER_REACT_VERSION,
'--',
artifactsDirectory,
]).stdout
)
.trim()
.split('\n')
.filter(filename => filename.endsWith('.fb.js'));
for (const artifactFilename of artifactFilenames) {
const originalText = fs.readFileSync(artifactFilename, 'utf8');
const replacedText = originalText.replaceAll(
PLACEHOLDER_REACT_VERSION,
newVersion
);
fs.writeFileSync(artifactFilename, replacedText);
}
}
/**
* cross-platform alternative to `rsync -ar`