[useFormState] Allow sync actions (#27571)

Updates useFormState to allow a sync function to be passed as an action.

A form action is almost always async, because it needs to talk to the
server. But since we support client-side actions, too, there's no reason
we can't allow sync actions, too.

I originally chose not to allow them to keep the implementation simpler
but it's not really that much more complicated because we already
support this for actions passed to startTransition. So now it's
consistent: anywhere an action is accepted, a sync client function is a
valid input.
This commit is contained in:
Andrew Clark
2023-10-31 23:32:31 -04:00
committed by GitHub
parent 08a39539fc
commit 77c4ac2ce8
31 changed files with 668 additions and 472 deletions
+5 -3
View File
@@ -108,9 +108,11 @@ function getModules() {
// TypeScript project and set up the config
// based on tsconfig.json
if (hasTsConfig) {
const ts = require(resolve.sync('typescript', {
basedir: paths.appNodeModules,
}));
const ts = require(
resolve.sync('typescript', {
basedir: paths.appNodeModules,
})
);
config = ts.readConfigFile(paths.appTsConfig, ts.sys.readFile).config;
// Otherwise we'll check if there is jsconfig.json
// for non TS projects.
+2 -2
View File
@@ -81,14 +81,14 @@
"minimist": "^1.2.3",
"mkdirp": "^0.5.1",
"ncp": "^2.0.0",
"prettier": "2.8.3",
"prettier": "3.0.3",
"pretty-format": "^29.4.1",
"prop-types": "^15.6.2",
"random-seed": "^0.3.0",
"react-lifecycles-compat": "^3.0.4",
"rimraf": "^3.0.0",
"rollup": "^3.17.1",
"rollup-plugin-prettier": "^3.0.0",
"rollup-plugin-prettier": "^4.1.1",
"rollup-plugin-strip-banner": "^3.0.0",
"semver": "^7.1.1",
"signedsource": "^2.0.0",
+5 -5
View File
@@ -572,11 +572,11 @@ function createServerReferenceProxy<A: Iterable<any>, T>(
}
// Since this is a fake Promise whose .then doesn't chain, we have to wrap it.
// TODO: Remove the wrapper once that's fixed.
return ((Promise.resolve(p): any): Promise<Array<any>>).then(function (
bound,
) {
return callServer(metaData.id, bound.concat(args));
});
return ((Promise.resolve(p): any): Promise<Array<any>>).then(
function (bound) {
return callServer(metaData.id, bound.concat(args));
},
);
};
registerServerReference(proxy, metaData);
return proxy;
@@ -40,12 +40,11 @@ describe('Profiler change descriptions', () => {
}
const MemoizedChild = React.memo(Child, areEqual);
const ForwardRefChild = React.forwardRef(function RefForwardingComponent(
props,
ref,
) {
return <Child />;
});
const ForwardRefChild = React.forwardRef(
function RefForwardingComponent(props, ref) {
return <Child />;
},
);
let forceUpdate = null;
@@ -33,20 +33,20 @@ export default class ProfilingCache {
this._profilerStore = profilerStore;
}
getCommitTree: ({
commitIndex: number,
rootID: number,
}) => CommitTree = ({commitIndex, rootID}) =>
getCommitTree: ({commitIndex: number, rootID: number}) => CommitTree = ({
commitIndex,
rootID,
}) =>
getCommitTree({
commitIndex,
profilerStore: this._profilerStore,
rootID,
});
getFiberCommits: ({
fiberID: number,
rootID: number,
}) => Array<number> = ({fiberID, rootID}) => {
getFiberCommits: ({fiberID: number, rootID: number}) => Array<number> = ({
fiberID,
rootID,
}) => {
const cachedFiberCommits = this._fiberCommits.get(fiberID);
if (cachedFiberCommits != null) {
return cachedFiberCommits;
@@ -70,9 +70,8 @@ describe('parseHookNames', () => {
const hooksList = flattenHooksList(hooksTree);
// Runs in the UI thread so it can share Network cache:
const locationKeyToHookSourceAndMetadata = await loadSourceAndMetadata(
hooksList,
);
const locationKeyToHookSourceAndMetadata =
await loadSourceAndMetadata(hooksList);
// Runs in a Worker because it's CPU intensive:
return parseSourceAndMetadata(
@@ -26,7 +26,7 @@ if (document.body != null) {
installFizzInstrObserver(document.body);
}
// $FlowFixMe[incompatible-cast]
handleExistingNodes((document.body /*: HTMLElement */));
handleExistingNodes((document.body: HTMLElement));
} else {
// Document must be loading -- body may not exist yet if the fizz external
// runtime is sent in <head> (e.g. as a preinit resource)
@@ -38,7 +38,7 @@ if (document.body != null) {
installFizzInstrObserver(document.body);
}
// $FlowFixMe[incompatible-cast]
handleExistingNodes((document.body /*: HTMLElement */));
handleExistingNodes((document.body: HTMLElement));
// We can call disconnect without takeRecord here,
// since we only expect a single document.body
@@ -49,15 +49,15 @@ if (document.body != null) {
domBodyObserver.observe(document.documentElement, {childList: true});
}
function handleExistingNodes(target /*: HTMLElement */) {
function handleExistingNodes(target: HTMLElement) {
const existingNodes = target.querySelectorAll('template');
for (let i = 0; i < existingNodes.length; i++) {
handleNode(existingNodes[i]);
}
}
function installFizzInstrObserver(target /*: Node */) {
const handleMutations = (mutations /*: Array<MutationRecord> */) => {
function installFizzInstrObserver(target: Node) {
const handleMutations = (mutations: Array<MutationRecord>) => {
for (let i = 0; i < mutations.length; i++) {
const addedNodes = mutations[i].addedNodes;
for (let j = 0; j < addedNodes.length; j++) {
@@ -80,13 +80,13 @@ function installFizzInstrObserver(target /*: Node */) {
});
}
function handleNode(node_ /*: Node */) {
function handleNode(node_: Node) {
// $FlowFixMe[incompatible-cast]
if (node_.nodeType !== 1 || !(node_ /*: HTMLElement */).dataset) {
if (node_.nodeType !== 1 || !(node_: HTMLElement).dataset) {
return;
}
// $FlowFixMe[incompatible-cast]
const node = (node_ /*: HTMLElement */);
const node = (node_: HTMLElement);
const dataset = node.dataset;
if (dataset['rxi'] != null) {
clientRenderBoundary(
@@ -8,6 +8,7 @@
*/
import type {Dispatcher} from 'react-reconciler/src/ReactInternalTypes';
import type {Awaited} from 'shared/ReactTypes';
import {enableAsyncActions, enableFormActions} from 'shared/ReactFeatureFlags';
import ReactSharedInternals from 'shared/ReactSharedInternals';
@@ -76,10 +77,10 @@ export function useFormStatus(): FormStatus {
}
export function useFormState<S, P>(
action: (S, P) => Promise<S>,
initialState: S,
action: (Awaited<S>, P) => S,
initialState: Awaited<S>,
permalink?: string,
): [S, (P) => void] {
): [Awaited<S>, (P) => void] {
if (!(enableFormActions && enableAsyncActions)) {
throw new Error('Not implemented.');
} else {
+4 -3
View File
@@ -31,6 +31,7 @@ export {
version,
} from './src/client/ReactDOM';
import type {Awaited} from 'shared/ReactTypes';
import type {FormStatus} from 'react-dom-bindings/src/shared/ReactDOMFormActions';
import {useFormStatus, useFormState} from './src/client/ReactDOM';
@@ -45,10 +46,10 @@ export function experimental_useFormStatus(): FormStatus {
}
export function experimental_useFormState<S, P>(
action: (S, P) => Promise<S>,
initialState: S,
action: (Awaited<S>, P) => S,
initialState: Awaited<S>,
permalink?: string,
): [S, (P) => void] {
): [Awaited<S>, (P) => void] {
if (__DEV__) {
console.error(
'useFormState is now in canary. Remove the experimental_ prefix. ' +
+4 -3
View File
@@ -34,6 +34,7 @@ import {
useFormStatus,
useFormState,
} from './src/server/ReactDOMServerRenderingStub';
import type {Awaited} from 'shared/ReactTypes';
export function experimental_useFormStatus(): FormStatus {
if (__DEV__) {
@@ -46,10 +47,10 @@ export function experimental_useFormStatus(): FormStatus {
}
export function experimental_useFormState<S, P>(
action: (S, P) => Promise<S>,
initialState: S,
action: (Awaited<S>, P) => S,
initialState: Awaited<S>,
permalink?: string,
): [S, (P) => void] {
): [Awaited<S>, (P) => void] {
if (__DEV__) {
console.error(
'useFormState is now in canary. Remove the experimental_ prefix. ' +
+167 -17
View File
@@ -1113,29 +1113,179 @@ describe('ReactDOMForm', () => {
// @gate enableFormActions
// @gate enableAsyncActions
test('useFormState: warns if action is not async', async () => {
let dispatch;
test('useFormState: works if action is sync', async () => {
let increment;
function App({stepSize}) {
const [state, dispatch] = useFormState(prevState => {
return prevState + stepSize;
}, 0);
increment = dispatch;
return <Text text={state} />;
}
// Initial render
const root = ReactDOMClient.createRoot(container);
await act(() => root.render(<App stepSize={1} />));
assertLog([0]);
// Perform an action. This will increase the state by 1, as defined by the
// stepSize prop.
await act(() => increment());
assertLog([1]);
// Now increase the stepSize prop to 10. Subsequent steps will increase
// by this amount.
await act(() => root.render(<App stepSize={10} />));
assertLog([1]);
// Increment again. The state should increase by 10.
await act(() => increment());
assertLog([11]);
});
// @gate enableFormActions
// @gate enableAsyncActions
test('useFormState: can mix sync and async actions', async () => {
let action;
function App() {
const [state, _dispatch] = useFormState(() => {}, 0);
dispatch = _dispatch;
const [state, dispatch] = useFormState((s, a) => a, 'A');
action = dispatch;
return <Text text={state} />;
}
const root = ReactDOMClient.createRoot(container);
await act(async () => {
root.render(<App />);
});
assertLog([0]);
await act(() => root.render(<App />));
assertLog(['A']);
expect(() => {
// This throws because React expects the action to return a promise.
expect(() => dispatch()).toThrow('Cannot read properties of undefined');
}).toErrorDev(
[
// In dev we also log a warning.
'The action passed to useFormState must be an async function',
],
{withoutStack: true},
await act(() => action(getText('B')));
await act(() => action('C'));
await act(() => action(getText('D')));
await act(() => action('E'));
await act(() => resolveText('B'));
await act(() => resolveText('D'));
assertLog(['E']);
expect(container.textContent).toBe('E');
});
// @gate enableFormActions
// @gate enableAsyncActions
test('useFormState: error handling (sync action)', async () => {
let resetErrorBoundary;
class ErrorBoundary extends React.Component {
state = {error: null};
static getDerivedStateFromError(error) {
return {error};
}
render() {
resetErrorBoundary = () => this.setState({error: null});
if (this.state.error !== null) {
return <Text text={'Caught an error: ' + this.state.error.message} />;
}
return this.props.children;
}
}
let action;
function App() {
const [state, dispatch] = useFormState((s, a) => {
if (a.endsWith('!')) {
throw new Error(a);
}
return a;
}, 'A');
action = dispatch;
return <Text text={state} />;
}
const root = ReactDOMClient.createRoot(container);
await act(() =>
root.render(
<ErrorBoundary>
<App />
</ErrorBoundary>,
),
);
assertLog(['A']);
await act(() => action('Oops!'));
assertLog(['Caught an error: Oops!', 'Caught an error: Oops!']);
expect(container.textContent).toBe('Caught an error: Oops!');
// Reset the error boundary
await act(() => resetErrorBoundary());
assertLog(['A']);
// Trigger an error again, but this time, perform another action that
// overrides the first one and fixes the error
await act(() => {
action('Oops!');
action('B');
});
assertLog(['B']);
expect(container.textContent).toBe('B');
});
// @gate enableFormActions
// @gate enableAsyncActions
test('useFormState: error handling (async action)', async () => {
let resetErrorBoundary;
class ErrorBoundary extends React.Component {
state = {error: null};
static getDerivedStateFromError(error) {
return {error};
}
render() {
resetErrorBoundary = () => this.setState({error: null});
if (this.state.error !== null) {
return <Text text={'Caught an error: ' + this.state.error.message} />;
}
return this.props.children;
}
}
let action;
function App() {
const [state, dispatch] = useFormState(async (s, a) => {
const text = await getText(a);
if (text.endsWith('!')) {
throw new Error(text);
}
return text;
}, 'A');
action = dispatch;
return <Text text={state} />;
}
const root = ReactDOMClient.createRoot(container);
await act(() =>
root.render(
<ErrorBoundary>
<App />
</ErrorBoundary>,
),
);
assertLog(['A']);
await act(() => action('Oops!'));
assertLog([]);
await act(() => resolveText('Oops!'));
assertLog(['Caught an error: Oops!', 'Caught an error: Oops!']);
expect(container.textContent).toBe('Caught an error: Oops!');
// Reset the error boundary
await act(() => resetErrorBoundary());
assertLog(['A']);
// Trigger an error again, but this time, perform another action that
// overrides the first one and fixes the error
await act(() => {
action('Oops!');
action('B');
});
assertLog([]);
await act(() => resolveText('B'));
assertLog(['B']);
expect(container.textContent).toBe('B');
});
});
+97 -96
View File
@@ -2089,38 +2089,40 @@ describe('ReactDOMInput', () => {
it('sets type, step, min, max before value always', () => {
const log = [];
const originalCreateElement = document.createElement;
spyOnDevAndProd(document, 'createElement').mockImplementation(function (
type,
) {
const el = originalCreateElement.apply(this, arguments);
let value = '';
let typeProp = '';
spyOnDevAndProd(document, 'createElement').mockImplementation(
function (type) {
const el = originalCreateElement.apply(this, arguments);
let value = '';
let typeProp = '';
if (type === 'input') {
Object.defineProperty(el, 'type', {
get: function () {
return typeProp;
},
set: function (val) {
typeProp = String(val);
log.push('set property type');
},
});
Object.defineProperty(el, 'value', {
get: function () {
return value;
},
set: function (val) {
value = String(val);
log.push('set property value');
},
});
spyOnDevAndProd(el, 'setAttribute').mockImplementation(function (name) {
log.push('set attribute ' + name);
});
}
return el;
});
if (type === 'input') {
Object.defineProperty(el, 'type', {
get: function () {
return typeProp;
},
set: function (val) {
typeProp = String(val);
log.push('set property type');
},
});
Object.defineProperty(el, 'value', {
get: function () {
return value;
},
set: function (val) {
value = String(val);
log.push('set property value');
},
});
spyOnDevAndProd(el, 'setAttribute').mockImplementation(
function (name) {
log.push('set attribute ' + name);
},
);
}
return el;
},
);
ReactDOM.render(
<input
@@ -2174,71 +2176,70 @@ describe('ReactDOMInput', () => {
const log = [];
const originalCreateElement = document.createElement;
spyOnDevAndProd(document, 'createElement').mockImplementation(function (
type,
) {
const el = originalCreateElement.apply(this, arguments);
const getDefaultValue = Object.getOwnPropertyDescriptor(
HTMLInputElement.prototype,
'defaultValue',
).get;
const setDefaultValue = Object.getOwnPropertyDescriptor(
HTMLInputElement.prototype,
'defaultValue',
).set;
const getValue = Object.getOwnPropertyDescriptor(
HTMLInputElement.prototype,
'value',
).get;
const setValue = Object.getOwnPropertyDescriptor(
HTMLInputElement.prototype,
'value',
).set;
const getType = Object.getOwnPropertyDescriptor(
HTMLInputElement.prototype,
'type',
).get;
const setType = Object.getOwnPropertyDescriptor(
HTMLInputElement.prototype,
'type',
).set;
if (type === 'input') {
Object.defineProperty(el, 'defaultValue', {
get: function () {
return getDefaultValue.call(this);
},
set: function (val) {
log.push(`node.defaultValue = ${strify(val)}`);
setDefaultValue.call(this, val);
},
});
Object.defineProperty(el, 'value', {
get: function () {
return getValue.call(this);
},
set: function (val) {
log.push(`node.value = ${strify(val)}`);
setValue.call(this, val);
},
});
Object.defineProperty(el, 'type', {
get: function () {
return getType.call(this);
},
set: function (val) {
log.push(`node.type = ${strify(val)}`);
setType.call(this, val);
},
});
spyOnDevAndProd(el, 'setAttribute').mockImplementation(function (
name,
val,
) {
log.push(`node.setAttribute(${strify(name)}, ${strify(val)})`);
});
}
return el;
});
spyOnDevAndProd(document, 'createElement').mockImplementation(
function (type) {
const el = originalCreateElement.apply(this, arguments);
const getDefaultValue = Object.getOwnPropertyDescriptor(
HTMLInputElement.prototype,
'defaultValue',
).get;
const setDefaultValue = Object.getOwnPropertyDescriptor(
HTMLInputElement.prototype,
'defaultValue',
).set;
const getValue = Object.getOwnPropertyDescriptor(
HTMLInputElement.prototype,
'value',
).get;
const setValue = Object.getOwnPropertyDescriptor(
HTMLInputElement.prototype,
'value',
).set;
const getType = Object.getOwnPropertyDescriptor(
HTMLInputElement.prototype,
'type',
).get;
const setType = Object.getOwnPropertyDescriptor(
HTMLInputElement.prototype,
'type',
).set;
if (type === 'input') {
Object.defineProperty(el, 'defaultValue', {
get: function () {
return getDefaultValue.call(this);
},
set: function (val) {
log.push(`node.defaultValue = ${strify(val)}`);
setDefaultValue.call(this, val);
},
});
Object.defineProperty(el, 'value', {
get: function () {
return getValue.call(this);
},
set: function (val) {
log.push(`node.value = ${strify(val)}`);
setValue.call(this, val);
},
});
Object.defineProperty(el, 'type', {
get: function () {
return getType.call(this);
},
set: function (val) {
log.push(`node.type = ${strify(val)}`);
setType.call(this, val);
},
});
spyOnDevAndProd(el, 'setAttribute').mockImplementation(
function (name, val) {
log.push(`node.setAttribute(${strify(name)}, ${strify(val)})`);
},
);
}
return el;
},
);
ReactDOM.render(<input type="date" defaultValue="1980-01-01" />, container);
+18 -18
View File
@@ -137,24 +137,24 @@ describe('ReactDOMTextarea', () => {
let counter = 0;
const originalCreateElement = document.createElement;
spyOnDevAndProd(document, 'createElement').mockImplementation(function (
type,
) {
const el = originalCreateElement.apply(this, arguments);
let value = '';
if (type === 'textarea') {
Object.defineProperty(el, 'value', {
get: function () {
return value;
},
set: function (val) {
value = String(val);
counter++;
},
});
}
return el;
});
spyOnDevAndProd(document, 'createElement').mockImplementation(
function (type) {
const el = originalCreateElement.apply(this, arguments);
let value = '';
if (type === 'textarea') {
Object.defineProperty(el, 'value', {
get: function () {
return value;
},
set: function (val) {
value = String(val);
counter++;
},
});
}
return el;
},
);
ReactDOM.render(<textarea value="" readOnly={true} />, container);
@@ -43,25 +43,21 @@ const RCTFabricUIManager = {
}
return result.join('\n');
},
createNode: jest.fn(function createNode(
reactTag,
viewName,
rootTag,
props,
eventTarget,
) {
if (allocatedTags.has(reactTag)) {
throw new Error(`Created two native views with tag ${reactTag}`);
}
createNode: jest.fn(
function createNode(reactTag, viewName, rootTag, props, eventTarget) {
if (allocatedTags.has(reactTag)) {
throw new Error(`Created two native views with tag ${reactTag}`);
}
allocatedTags.add(reactTag);
return {
reactTag: reactTag,
viewName: viewName,
props: props,
children: [],
};
}),
allocatedTags.add(reactTag);
return {
reactTag: reactTag,
viewName: viewName,
props: props,
children: [],
};
},
),
cloneNode: jest.fn(function cloneNode(node) {
return {
reactTag: node.reactTag,
@@ -70,28 +66,26 @@ const RCTFabricUIManager = {
children: node.children,
};
}),
cloneNodeWithNewChildren: jest.fn(function cloneNodeWithNewChildren(
node,
children,
) {
return {
reactTag: node.reactTag,
viewName: node.viewName,
props: node.props,
children: children ?? [],
};
}),
cloneNodeWithNewProps: jest.fn(function cloneNodeWithNewProps(
node,
newPropsDiff,
) {
return {
reactTag: node.reactTag,
viewName: node.viewName,
props: {...node.props, ...newPropsDiff},
children: node.children,
};
}),
cloneNodeWithNewChildren: jest.fn(
function cloneNodeWithNewChildren(node, children) {
return {
reactTag: node.reactTag,
viewName: node.viewName,
props: node.props,
children: children ?? [],
};
},
),
cloneNodeWithNewProps: jest.fn(
function cloneNodeWithNewProps(node, newPropsDiff) {
return {
reactTag: node.reactTag,
viewName: node.viewName,
props: {...node.props, ...newPropsDiff},
children: node.children,
};
},
),
cloneNodeWithNewChildrenAndProps: jest.fn(
function cloneNodeWithNewChildrenAndProps(node, newPropsDiff) {
let children = [];
@@ -171,34 +165,31 @@ const RCTFabricUIManager = {
return [10, 10, 100, 100];
}),
measureLayout: jest.fn(function measureLayout(
node,
relativeNode,
fail,
success,
) {
if (typeof node !== 'object') {
throw new Error(
`Expected node to be an object, was passed "${typeof node}"`,
);
}
measureLayout: jest.fn(
function measureLayout(node, relativeNode, fail, success) {
if (typeof node !== 'object') {
throw new Error(
`Expected node to be an object, was passed "${typeof node}"`,
);
}
if (typeof node.viewName !== 'string') {
throw new Error('Expected node to be a host node.');
}
if (typeof node.viewName !== 'string') {
throw new Error('Expected node to be a host node.');
}
if (typeof relativeNode !== 'object') {
throw new Error(
`Expected relative node to be an object, was passed "${typeof relativeNode}"`,
);
}
if (typeof relativeNode !== 'object') {
throw new Error(
`Expected relative node to be an object, was passed "${typeof relativeNode}"`,
);
}
if (typeof relativeNode.viewName !== 'string') {
throw new Error('Expected relative node to be a host node.');
}
if (typeof relativeNode.viewName !== 'string') {
throw new Error('Expected relative node to be a host node.');
}
success(1, 1, 100, 100);
}),
success(1, 1, 100, 100);
},
),
setIsJSResponder: jest.fn(),
};
@@ -173,24 +173,21 @@ const RCTUIManager = {
callback(10, 10, 100, 100);
}),
measureLayout: jest.fn(function measureLayout(
tag,
relativeTag,
fail,
success,
) {
if (typeof tag !== 'number') {
throw new Error(`Expected tag to be a number, was passed ${tag}`);
}
measureLayout: jest.fn(
function measureLayout(tag, relativeTag, fail, success) {
if (typeof tag !== 'number') {
throw new Error(`Expected tag to be a number, was passed ${tag}`);
}
if (typeof relativeTag !== 'number') {
throw new Error(
`Expected relativeTag to be a number, was passed ${relativeTag}`,
);
}
if (typeof relativeTag !== 'number') {
throw new Error(
`Expected relativeTag to be a number, was passed ${relativeTag}`,
);
}
success(1, 1, 100, 100);
}),
success(1, 1, 100, 100);
},
),
__takeSnapshot: jest.fn(),
};
@@ -528,14 +528,13 @@ describe('ReactFabric', () => {
}));
const snapshots = [];
nativeFabricUIManager.completeRoot.mockImplementation(function (
rootTag,
newChildSet,
) {
snapshots.push(
nativeFabricUIManager.__dumpChildSetForJestTestsOnly(newChildSet),
);
});
nativeFabricUIManager.completeRoot.mockImplementation(
function (rootTag, newChildSet) {
snapshots.push(
nativeFabricUIManager.__dumpChildSetForJestTestsOnly(newChildSet),
);
},
);
await act(() => {
ReactFabric.render(
+2 -2
View File
@@ -34,7 +34,7 @@ let currentEntangledPendingCount: number = 0;
let currentEntangledLane: Lane = NoLane;
export function requestAsyncActionContext<S>(
actionReturnValue: Thenable<mixed>,
actionReturnValue: Thenable<any>,
// If this is provided, this resulting thenable resolves to this value instead
// of the return value of the action. This is a perf trick to avoid composing
// an extra async function.
@@ -112,7 +112,7 @@ export function requestAsyncActionContext<S>(
}
export function requestSyncActionContext<S>(
actionReturnValue: mixed,
actionReturnValue: any,
// If this is provided, this resulting thenable resolves to this value instead
// of the return value of the action. This is a perf trick to avoid composing
// an extra async function.
+102 -89
View File
@@ -13,6 +13,7 @@ import type {
Usable,
Thenable,
RejectedThenable,
Awaited,
} from 'shared/ReactTypes';
import type {
Fiber,
@@ -1871,12 +1872,12 @@ function rerenderOptimistic<S, A>(
type FormStateActionQueue<S, P> = {
// This is the most recent state returned from an action. It's updated as
// soon as the action finishes running.
state: S,
state: Awaited<S>,
// A stable dispatch method, passed to the user.
dispatch: Dispatch<P>,
// This is the most recent action function that was rendered. It's updated
// during the commit phase.
action: (S, P) => Promise<S>,
action: (Awaited<S>, P) => S,
// This is a circular linked list of pending action payloads. It incudes the
// action that is currently running.
pending: FormStateActionQueueNode<P> | null,
@@ -1891,7 +1892,7 @@ type FormStateActionQueueNode<P> = {
function dispatchFormState<S, P>(
fiber: Fiber,
actionQueue: FormStateActionQueue<S, P>,
setState: Dispatch<Thenable<S>>,
setState: Dispatch<S | Awaited<S>>,
payload: P,
): void {
if (isRenderPhaseUpdate(fiber)) {
@@ -1907,7 +1908,7 @@ function dispatchFormState<S, P>(
};
newLast.next = actionQueue.pending = newLast;
runFormStateAction(actionQueue, setState, payload);
runFormStateAction(actionQueue, (setState: any), payload);
} else {
// There's already an action running. Add to the queue.
const first = last.next;
@@ -1921,7 +1922,7 @@ function dispatchFormState<S, P>(
function runFormStateAction<S, P>(
actionQueue: FormStateActionQueue<S, P>,
setState: Dispatch<Thenable<S>>,
setState: Dispatch<S | Awaited<S>>,
payload: P,
) {
const action = actionQueue.action;
@@ -1935,39 +1936,49 @@ function runFormStateAction<S, P>(
ReactCurrentBatchConfig.transition._updatedFibers = new Set();
}
try {
const promise = action(prevState, payload);
const returnValue = action(prevState, payload);
if (
returnValue !== null &&
typeof returnValue === 'object' &&
// $FlowFixMe[method-unbinding]
typeof returnValue.then === 'function'
) {
const thenable = ((returnValue: any): Thenable<Awaited<S>>);
if (__DEV__) {
if (
promise === null ||
typeof promise !== 'object' ||
typeof (promise: any).then !== 'function'
) {
console.error(
'The action passed to useFormState must be an async function.',
);
}
// Attach a listener to read the return state of the action. As soon as
// this resolves, we can run the next action in the sequence.
thenable.then(
(nextState: Awaited<S>) => {
actionQueue.state = nextState;
finishRunningFormStateAction(actionQueue, (setState: any));
},
() => finishRunningFormStateAction(actionQueue, (setState: any)),
);
const entangledResult = requestAsyncActionContext<S>(thenable, null);
setState((entangledResult: any));
} else {
// This is either `returnValue` or a thenable that resolves to
// `returnValue`, depending on whether we're inside an async action scope.
const entangledResult = requestSyncActionContext<S>(returnValue, null);
setState((entangledResult: any));
const nextState = ((returnValue: any): Awaited<S>);
actionQueue.state = nextState;
finishRunningFormStateAction(actionQueue, (setState: any));
}
// Attach a listener to read the return state of the action. As soon as this
// resolves, we can run the next action in the sequence.
promise.then(
(nextState: S) => {
actionQueue.state = nextState;
finishRunningFormStateAction(actionQueue, setState);
},
() => finishRunningFormStateAction(actionQueue, setState),
);
// Create a thenable that resolves once the current async action scope has
// finished. Then stash that thenable in state. We'll unwrap it with the
// `use` algorithm during render. This is the same logic used
// by startTransition.
const entangledThenable: Thenable<S> = requestAsyncActionContext(
promise,
null,
);
setState(entangledThenable);
} catch (error) {
// This is a trick to get the `useFormState` hook to rethrow the error.
// When it unwraps the thenable with the `use` algorithm, the error
// will be thrown.
const rejectedThenable: S = ({
then() {},
status: 'rejected',
reason: error,
// $FlowFixMe: Not sure why this doesn't work
}: RejectedThenable<Awaited<S>>);
setState(rejectedThenable);
finishRunningFormStateAction(actionQueue, (setState: any));
} finally {
ReactCurrentBatchConfig.transition = prevTransition;
@@ -1989,7 +2000,7 @@ function runFormStateAction<S, P>(
function finishRunningFormStateAction<S, P>(
actionQueue: FormStateActionQueue<S, P>,
setState: Dispatch<Thenable<S>>,
setState: Dispatch<S | Awaited<S>>,
) {
// The action finished running. Pop it from the queue and run the next pending
// action, if there are any.
@@ -2005,7 +2016,7 @@ function finishRunningFormStateAction<S, P>(
last.next = next;
// Run the next action.
runFormStateAction(actionQueue, setState, next.payload);
runFormStateAction(actionQueue, (setState: any), next.payload);
}
}
}
@@ -2015,11 +2026,11 @@ function formStateReducer<S>(oldState: S, newState: S): S {
}
function mountFormState<S, P>(
action: (S, P) => Promise<S>,
initialStateProp: S,
action: (Awaited<S>, P) => S,
initialStateProp: Awaited<S>,
permalink?: string,
): [S, (P) => void] {
let initialState = initialStateProp;
): [Awaited<S>, (P) => void] {
let initialState: Awaited<S> = initialStateProp;
if (getIsHydrating()) {
const root: FiberRoot = (getWorkInProgressRoot(): any);
const ssrFormState = root.formState;
@@ -2035,28 +2046,25 @@ function mountFormState<S, P>(
}
}
}
const initialStateThenable: Thenable<S> = {
status: 'fulfilled',
value: initialState,
then() {},
};
// State hook. The state is stored in a thenable which is then unwrapped by
// the `use` algorithm during render.
const stateHook = mountWorkInProgressHook();
stateHook.memoizedState = stateHook.baseState = initialStateThenable;
const stateQueue: UpdateQueue<Thenable<S>, Thenable<S>> = {
stateHook.memoizedState = stateHook.baseState = initialState;
// TODO: Typing this "correctly" results in recursion limit errors
// const stateQueue: UpdateQueue<S | Awaited<S>, S | Awaited<S>> = {
const stateQueue = {
pending: null,
lanes: NoLanes,
dispatch: null,
dispatch: (null: any),
lastRenderedReducer: formStateReducer,
lastRenderedState: initialStateThenable,
lastRenderedState: initialState,
};
stateHook.queue = stateQueue;
const setState: Dispatch<Thenable<S>> = (dispatchSetState.bind(
const setState: Dispatch<S | Awaited<S>> = (dispatchSetState.bind(
null,
currentlyRenderingFiber,
stateQueue,
((stateQueue: any): UpdateQueue<S | Awaited<S>, S | Awaited<S>>),
): any);
stateQueue.dispatch = setState;
@@ -2072,7 +2080,7 @@ function mountFormState<S, P>(
pending: null,
};
actionQueueHook.queue = actionQueue;
const dispatch = dispatchFormState.bind(
const dispatch = (dispatchFormState: any).bind(
null,
currentlyRenderingFiber,
actionQueue,
@@ -2089,10 +2097,10 @@ function mountFormState<S, P>(
}
function updateFormState<S, P>(
action: (S, P) => Promise<S>,
initialState: S,
action: (Awaited<S>, P) => S,
initialState: Awaited<S>,
permalink?: string,
): [S, (P) => void] {
): [Awaited<S>, (P) => void] {
const stateHook = updateWorkInProgressHook();
const currentStateHook = ((currentHook: any): Hook);
return updateFormStateImpl(
@@ -2107,18 +2115,24 @@ function updateFormState<S, P>(
function updateFormStateImpl<S, P>(
stateHook: Hook,
currentStateHook: Hook,
action: (S, P) => Promise<S>,
initialState: S,
action: (Awaited<S>, P) => S,
initialState: Awaited<S>,
permalink?: string,
): [S, (P) => void] {
const [thenable] = updateReducerImpl<Thenable<S>, Thenable<S>>(
): [Awaited<S>, (P) => void] {
const [actionResult] = updateReducerImpl<S | Thenable<S>, S | Thenable<S>>(
stateHook,
currentStateHook,
formStateReducer,
);
// This will suspend until the action finishes.
const state = useThenable(thenable);
const state: Awaited<S> =
typeof actionResult === 'object' &&
actionResult !== null &&
// $FlowFixMe[method-unbinding]
typeof actionResult.then === 'function'
? useThenable(((actionResult: any): Thenable<Awaited<S>>))
: (actionResult: any);
const actionQueueHook = updateWorkInProgressHook();
const actionQueue = actionQueueHook.queue;
@@ -2141,16 +2155,16 @@ function updateFormStateImpl<S, P>(
function formStateActionEffect<S, P>(
actionQueue: FormStateActionQueue<S, P>,
action: (S, P) => Promise<S>,
action: (Awaited<S>, P) => S,
): void {
actionQueue.action = action;
}
function rerenderFormState<S, P>(
action: (S, P) => Promise<S>,
initialState: S,
action: (Awaited<S>, P) => S,
initialState: Awaited<S>,
permalink?: string,
): [S, (P) => void] {
): [Awaited<S>, (P) => void] {
// Unlike useState, useFormState doesn't support render phase updates.
// Also unlike useState, we need to replay all pending updates again in case
// the passthrough value changed.
@@ -2173,8 +2187,7 @@ function rerenderFormState<S, P>(
}
// This is a mount. No updates to process.
const thenable: Thenable<S> = stateHook.memoizedState;
const state = useThenable(thenable);
const state: Awaited<S> = stateHook.memoizedState;
const actionQueueHook = updateWorkInProgressHook();
const actionQueue = actionQueueHook.queue;
@@ -3725,10 +3738,10 @@ if (__DEV__) {
useHostTransitionStatus;
(HooksDispatcherOnMountInDEV: Dispatcher).useFormState =
function useFormState<S, P>(
action: (S, P) => Promise<S>,
initialState: S,
action: (Awaited<S>, P) => S,
initialState: Awaited<S>,
permalink?: string,
): [S, (P) => void] {
): [Awaited<S>, (P) => void] {
currentHookNameInDev = 'useFormState';
mountHookTypesDev();
return mountFormState(action, initialState, permalink);
@@ -3895,10 +3908,10 @@ if (__DEV__) {
useHostTransitionStatus;
(HooksDispatcherOnMountWithHookTypesInDEV: Dispatcher).useFormState =
function useFormState<S, P>(
action: (S, P) => Promise<S>,
initialState: S,
action: (Awaited<S>, P) => S,
initialState: Awaited<S>,
permalink?: string,
): [S, (P) => void] {
): [Awaited<S>, (P) => void] {
currentHookNameInDev = 'useFormState';
updateHookTypesDev();
return mountFormState(action, initialState, permalink);
@@ -4067,10 +4080,10 @@ if (__DEV__) {
useHostTransitionStatus;
(HooksDispatcherOnUpdateInDEV: Dispatcher).useFormState =
function useFormState<S, P>(
action: (S, P) => Promise<S>,
initialState: S,
action: (Awaited<S>, P) => S,
initialState: Awaited<S>,
permalink?: string,
): [S, (P) => void] {
): [Awaited<S>, (P) => void] {
currentHookNameInDev = 'useFormState';
updateHookTypesDev();
return updateFormState(action, initialState, permalink);
@@ -4239,10 +4252,10 @@ if (__DEV__) {
useHostTransitionStatus;
(HooksDispatcherOnRerenderInDEV: Dispatcher).useFormState =
function useFormState<S, P>(
action: (S, P) => Promise<S>,
initialState: S,
action: (Awaited<S>, P) => S,
initialState: Awaited<S>,
permalink?: string,
): [S, (P) => void] {
): [Awaited<S>, (P) => void] {
currentHookNameInDev = 'useFormState';
updateHookTypesDev();
return rerenderFormState(action, initialState, permalink);
@@ -4432,10 +4445,10 @@ if (__DEV__) {
useHostTransitionStatus;
(InvalidNestedHooksDispatcherOnMountInDEV: Dispatcher).useFormState =
function useFormState<S, P>(
action: (S, P) => Promise<S>,
initialState: S,
action: (Awaited<S>, P) => S,
initialState: Awaited<S>,
permalink?: string,
): [S, (P) => void] {
): [Awaited<S>, (P) => void] {
currentHookNameInDev = 'useFormState';
warnInvalidHookAccess();
mountHookTypesDev();
@@ -4630,10 +4643,10 @@ if (__DEV__) {
useHostTransitionStatus;
(InvalidNestedHooksDispatcherOnUpdateInDEV: Dispatcher).useFormState =
function useFormState<S, P>(
action: (S, P) => Promise<S>,
initialState: S,
action: (Awaited<S>, P) => S,
initialState: Awaited<S>,
permalink?: string,
): [S, (P) => void] {
): [Awaited<S>, (P) => void] {
currentHookNameInDev = 'useFormState';
warnInvalidHookAccess();
updateHookTypesDev();
@@ -4828,10 +4841,10 @@ if (__DEV__) {
useHostTransitionStatus;
(InvalidNestedHooksDispatcherOnRerenderInDEV: Dispatcher).useFormState =
function useFormState<S, P>(
action: (S, P) => Promise<S>,
initialState: S,
action: (Awaited<S>, P) => S,
initialState: Awaited<S>,
permalink?: string,
): [S, (P) => void] {
): [Awaited<S>, (P) => void] {
currentHookNameInDev = 'useFormState';
warnInvalidHookAccess();
updateHookTypesDev();
+4 -3
View File
@@ -15,6 +15,7 @@ import type {
Wakeable,
Usable,
ReactFormState,
Awaited,
} from 'shared/ReactTypes';
import type {WorkTag} from './ReactWorkTags';
import type {TypeOfMode} from './ReactTypeOfMode';
@@ -418,10 +419,10 @@ export type Dispatcher = {
reducer: ?(S, A) => S,
) => [S, (A) => void],
useFormState?: <S, P>(
action: (S, P) => Promise<S>,
initialState: S,
action: (Awaited<S>, P) => S,
initialState: Awaited<S>,
permalink?: string,
) => [S, (P) => void],
) => [Awaited<S>, (P) => void],
};
export type CacheDispatcher = {
@@ -1543,11 +1543,20 @@ describe('ReactNewContext', () => {
}
function Root(props) {
return contextKeys.reduceRight((children, key) => {
const Context = contexts.get(key);
const value = props.values[key];
return <Context.Provider value={value}>{children}</Context.Provider>;
}, <ConsumerTree rand={props.rand} depth={0} maxDepth={props.maxDepth} />);
return contextKeys.reduceRight(
(children, key) => {
const Context = contexts.get(key);
const value = props.values[key];
return (
<Context.Provider value={value}>{children}</Context.Provider>
);
},
<ConsumerTree
rand={props.rand}
depth={0}
maxDepth={props.maxDepth}
/>,
);
}
const initialValues = contextKeys.reduce(
@@ -347,14 +347,14 @@ describe('ReactFlightDOMForm', () => {
// @gate enableFormActions
// @gate enableAsyncActions
it("useFormState's dispatch binds the initial state to the provided action", async () => {
const serverAction = serverExports(async function action(
prevState,
formData,
) {
return {
count: prevState.count + parseInt(formData.get('incrementAmount'), 10),
};
});
const serverAction = serverExports(
async function action(prevState, formData) {
return {
count:
prevState.count + parseInt(formData.get('incrementAmount'), 10),
};
},
);
const initialState = {count: 1};
function Client({action}) {
@@ -392,12 +392,11 @@ describe('ReactFlightDOMForm', () => {
// @gate enableFormActions
// @gate enableAsyncActions
it('useFormState can reuse state during MPA form submission', async () => {
const serverAction = serverExports(async function action(
prevState,
formData,
) {
return prevState + 1;
});
const serverAction = serverExports(
async function action(prevState, formData) {
return prevState + 1;
},
);
function Form({action}) {
const [count, dispatch] = useFormState(action, 1);
@@ -481,13 +480,11 @@ describe('ReactFlightDOMForm', () => {
'useFormState preserves state if arity is the same, but different ' +
'arguments are bound (i.e. inline closure)',
async () => {
const serverAction = serverExports(async function action(
stepSize,
prevState,
formData,
) {
return prevState + stepSize;
});
const serverAction = serverExports(
async function action(stepSize, prevState, formData) {
return prevState + stepSize;
},
);
function Form({action}) {
const [count, dispatch] = useFormState(action, 1);
@@ -597,19 +594,17 @@ describe('ReactFlightDOMForm', () => {
it('useFormState does not reuse state if action signatures are different', async () => {
// This is the same as the previous test, except instead of using bind to
// configure the server action (i.e. a closure), it swaps the action.
const increaseBy1 = serverExports(async function action(
prevState,
formData,
) {
return prevState + 1;
});
const increaseBy1 = serverExports(
async function action(prevState, formData) {
return prevState + 1;
},
);
const increaseBy5 = serverExports(async function action(
prevState,
formData,
) {
return prevState + 5;
});
const increaseBy5 = serverExports(
async function action(prevState, formData) {
return prevState + 5;
},
);
function Form({action}) {
const [count, dispatch] = useFormState(action, 1);
@@ -680,12 +675,11 @@ describe('ReactFlightDOMForm', () => {
// @gate enableFormActions
// @gate enableAsyncActions
it('when permalink is provided, useFormState compares that instead of the keypath', async () => {
const serverAction = serverExports(async function action(
prevState,
formData,
) {
return prevState + 1;
});
const serverAction = serverExports(
async function action(prevState, formData) {
return prevState + 1;
},
);
function Form({action, permalink}) {
const [count, dispatch] = useFormState(action, 1, permalink);
+4 -3
View File
@@ -15,6 +15,7 @@ import type {
Thenable,
Usable,
ReactCustomFormAction,
Awaited,
} from 'shared/ReactTypes';
import type {ResumableState} from './ReactFizzConfig';
@@ -612,10 +613,10 @@ function createPostbackFormStateKey(
}
function useFormState<S, P>(
action: (S, P) => Promise<S>,
initialState: S,
action: (Awaited<S>, P) => S,
initialState: Awaited<S>,
permalink?: string,
): [S, (P) => void] {
): [Awaited<S>, (P) => void] {
resolveCurrentlyRenderingComponent();
// Count the number of useFormState hooks per component. We also use this to
@@ -86,12 +86,11 @@ describe('forwardRef', () => {
);
}
const RefForwardingComponent = React.forwardRef(function NamedFunction(
props,
ref,
) {
return <FunctionComponent {...props} forwardedRef={ref} />;
});
const RefForwardingComponent = React.forwardRef(
function NamedFunction(props, ref) {
return <FunctionComponent {...props} forwardedRef={ref} />;
},
);
RefForwardingComponent.propTypes = {
optional: PropTypes.string,
required: PropTypes.string.isRequired,
+10
View File
@@ -184,3 +184,13 @@ export type ReactFormState<S, ReferenceId> = [
ReferenceId /* Server Reference ID */,
number /* number of bound arguments */,
];
export type Awaited<T> = T extends null | void
? T // special case for `null | undefined` when not in `--strictNullChecks` mode
: T extends Object // `await` only unwraps object types with a callable then. Non-object types are not unwrapped.
? T extends {then(onfulfilled: infer F): any} // thenable, extracts the first argument to `then()`
? F extends (value: infer V) => any // if the argument to `then` is callable, extracts the argument
? Awaited<V> // recursively unwrap the value
: empty // the argument to `then` was not callable.
: T // argument was not an object
: T; // non-thenable
+3 -4
View File
@@ -11,10 +11,9 @@ const inlinePackagePath = join(ROOT_PATH, 'packages', 'react-devtools-inline');
const shellPackagePath = join(ROOT_PATH, 'packages', 'react-devtools-shell');
const screenshotPath = join(ROOT_PATH, 'tmp', 'screenshots');
const {SUCCESSFUL_COMPILATION_MESSAGE} = require(join(
shellPackagePath,
'constants.js'
));
const {SUCCESSFUL_COMPILATION_MESSAGE} = require(
join(shellPackagePath, 'constants.js')
);
let buildProcess = null;
let serverProcess = null;
+1 -1
View File
@@ -13,7 +13,7 @@
* { 0: 'MUCH ERROR', 1: 'SUCH WRONG' }
*/
function invertObject(targetObj) {
const result /*: {[string]: string} */ = {};
const result = {};
const mapKeys = Object.keys(targetObj);
// eslint-disable-next-line no-for-of-loops/no-for-of-loops
+48 -38
View File
@@ -22,8 +22,6 @@ const shouldWrite = mode === 'write' || mode === 'write-changed';
const onlyChanged = mode === 'check-changed' || mode === 'write-changed';
const changedFiles = onlyChanged ? listChangedFiles() : null;
let didWarn = false;
let didError = false;
const prettierIgnoreFilePath = path.join(
__dirname,
@@ -66,44 +64,56 @@ if (!files.length) {
process.exit(0);
}
files.forEach(file => {
const options = prettier.resolveConfig.sync(file, {
config: prettierConfigPath,
});
try {
const input = fs.readFileSync(file, 'utf8');
if (shouldWrite) {
const output = prettier.format(input, options);
if (output !== input) {
fs.writeFileSync(file, output, 'utf8');
}
} else {
if (!prettier.check(input, options)) {
if (!didWarn) {
console.log(
'\n' +
chalk.red(
` This project uses prettier to format all JavaScript code.\n`
) +
chalk.dim(` Please run `) +
chalk.reset('yarn prettier-all') +
chalk.dim(
` and add changes to files listed below to your commit:`
) +
`\n\n`
);
didWarn = true;
async function main() {
let didWarn = false;
let didError = false;
await Promise.all(
files.map(async file => {
const options = await prettier.resolveConfig(file, {
config: prettierConfigPath,
});
try {
const input = fs.readFileSync(file, 'utf8');
if (shouldWrite) {
const output = await prettier.format(input, options);
if (output !== input) {
fs.writeFileSync(file, output, 'utf8');
}
} else {
const isFormatted = await prettier.check(input, options);
if (!isFormatted) {
if (!didWarn) {
console.log(
'\n' +
chalk.red(
` This project uses prettier to format all JavaScript code.\n`
) +
chalk.dim(` Please run `) +
chalk.reset('yarn prettier-all') +
chalk.dim(
` and add changes to files listed below to your commit:`
) +
`\n\n`
);
didWarn = true;
}
console.log(file);
}
}
} catch (error) {
didError = true;
console.log('\n\n' + error.message);
console.log(file);
}
}
} catch (error) {
didError = true;
console.log('\n\n' + error.message);
console.log(file);
})
);
if (didWarn || didError) {
process.exit(1);
}
});
if (didWarn || didError) {
process.exit(1);
}
main().catch(error => {
console.error(error);
process.exit(1);
});
+5
View File
@@ -89,6 +89,11 @@ gs([
'!**/__tests__/**/*.js',
'!**/__mocks__/**/*.js',
'!**/node_modules/**/*.js',
// TODO: The newer Flow type syntax in this file breaks the parser and I can't
// figure out how to get Babel to parse it. I wasted too much time on
// something so unimportant so I'm skipping this for now. There's no actual
// code or warnings in this file anyway.
'!packages/shared/ReactTypes.js',
]).pipe(
through.obj(transform, cb => {
process.stdout.write(Array.from(warnings).sort().join('\n') + '\n');
@@ -88,7 +88,7 @@ async function main() {
(_, variableName) => variableName
);
const prettyOutputCode = prettier.format(outputCode, prettierConfig);
const prettyOutputCode = await prettier.format(outputCode, prettierConfig);
fs.writeFileSync(inlineCodeStringsFilename, prettyOutputCode, 'utf8');
}
+2 -7
View File
@@ -3,12 +3,10 @@
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
'use strict';
function evalStringConcat(ast /*: Object */) /*: string */ {
function evalStringConcat(ast) {
switch (ast.type) {
case 'StringLiteral':
case 'Literal': // ESLint
@@ -24,10 +22,7 @@ function evalStringConcat(ast /*: Object */) /*: string */ {
}
exports.evalStringConcat = evalStringConcat;
function evalStringAndTemplateConcat(
ast /*: Object */,
args /*: Array<mixed> */
) /*: string */ {
function evalStringAndTemplateConcat(ast, args) {
switch (ast.type) {
case 'StringLiteral':
return ast.value;
+30 -11
View File
@@ -2379,6 +2379,11 @@
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24"
integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==
"@jridgewell/sourcemap-codec@^1.4.15":
version "1.4.15"
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32"
integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==
"@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.15", "@jridgewell/trace-mapping@^0.3.9":
version "0.3.17"
resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz#793041277af9073b0951a7fe0f0d8c4c98c36985"
@@ -3022,7 +3027,14 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-16.18.14.tgz#5465ce598486a703caddbefe8603f8a2cffa3461"
integrity sha512-wvzClDGQXOCVNU4APPopC2KtMYukaF1MN/W3xAmslx22Z4/IF1/izDMekuyoUlwfnDHYCIZGaj7jMwnJKBTxKw==
"@types/prettier@^1.0.0 || ^2.0.0", "@types/prettier@^2.1.5":
"@types/prettier@^1.0.0 || ^2.0.0 || ^3.0.0":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-3.0.0.tgz#e9bc8160230d3a461dab5c5b41cceef1ef723057"
integrity sha512-mFMBfMOz8QxhYVbuINtswBp9VL2b4Y0QqYHwqLz3YbgtfAcat2Dl6Y1o4e22S/OVE6Ebl9m7wWiMT2lSbAs1wA==
dependencies:
prettier "*"
"@types/prettier@^2.1.5":
version "2.7.2"
resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.7.2.tgz#6c2324641cc4ba050a8c710b2b251b377581fbf0"
integrity sha512-KufADq8uQqo1pYKVIYzfKbJfBAc0sOeXqGbFaSpv8MRmC/zXgowNZmFcbngndGk922QDmOASEXUZCaY48gs4cg==
@@ -10869,6 +10881,13 @@ magic-string@0.26.7:
dependencies:
sourcemap-codec "^1.4.8"
magic-string@0.30.5:
version "0.30.5"
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.5.tgz#1994d980bd1c8835dc6e78db7cbd4ae4f24746f9"
integrity sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==
dependencies:
"@jridgewell/sourcemap-codec" "^1.4.15"
magic-string@^0.27.0:
version "0.27.0"
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.27.0.tgz#e4a3413b4bab6d98d2becffd48b4a257effdbbf3"
@@ -12511,10 +12530,10 @@ prepend-http@^2.0.0:
resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897"
integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=
prettier@2.8.3:
version "2.8.3"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.3.tgz#ab697b1d3dd46fb4626fbe2f543afe0cc98d8632"
integrity sha512-tJ/oJ4amDihPoufT5sM0Z1SKEuKay8LfVAMlbbhnnkvt6BUserZylqo2PN+p9KeljLr0OHa2rXHU1T8reeoTrw==
prettier@*, prettier@3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.0.3.tgz#432a51f7ba422d1469096c0fdc28e235db8f9643"
integrity sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg==
pretty-format@^27.2.5, pretty-format@^27.3.1:
version "27.3.1"
@@ -13424,18 +13443,18 @@ roarr@^2.15.3:
semver-compare "^1.0.0"
sprintf-js "^1.1.2"
rollup-plugin-prettier@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/rollup-plugin-prettier/-/rollup-plugin-prettier-3.0.0.tgz#c208f31bc5ecef76ba69177bc9c1463667c9b19a"
integrity sha512-E0UqeVX1F+ATrHsXKXIywddjK+iFKOeOGI/drZY/wVq/xfHPjghviIhsFz7I0Wfuzp8jeN+4L7kVwQ/X84mOBw==
rollup-plugin-prettier@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/rollup-plugin-prettier/-/rollup-plugin-prettier-4.1.1.tgz#eb74bd47c3cc3ba68bdf34b5323d0d7a47be8cec"
integrity sha512-ugpi/EqW12yJa4NO3o4f/wt/YHwiQovVGC2jxZgxuKO9osjt4lVxVA427+itl87XmQc6089ZkpDc6OpaOZKWgQ==
dependencies:
"@types/prettier" "^1.0.0 || ^2.0.0"
"@types/prettier" "^1.0.0 || ^2.0.0 || ^3.0.0"
diff "5.1.0"
lodash.hasin "4.5.2"
lodash.isempty "4.4.0"
lodash.isnil "4.0.0"
lodash.omitby "4.6.0"
magic-string "0.26.7"
magic-string "0.30.5"
rollup-plugin-strip-banner@^3.0.0:
version "3.0.0"