mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Plugged react-window into commit selector
This commit is contained in:
+9
-7
@@ -203,16 +203,18 @@ Here is an example profile summary:
|
||||
rootID: 1,
|
||||
interactionCount: 2,
|
||||
|
||||
// Tuples of commit time (relative to when profiling started) and duration
|
||||
commits: [
|
||||
// Commit durations
|
||||
commitDurations: [
|
||||
10, // first commit took 10ms
|
||||
13, // second commit took 13ms
|
||||
5, // third commit took 5ms
|
||||
]
|
||||
|
||||
// Commit times (relative to when profiling started)
|
||||
commitTimes: [
|
||||
210, // first commit started 210ms after profiling began
|
||||
10, // and took 10ms
|
||||
|
||||
284, // second commit started 284ms after profiling began
|
||||
13, // and took 13ms
|
||||
|
||||
303, // third commit started 303ms after profiling began
|
||||
5, // and took 5ms
|
||||
],
|
||||
|
||||
// Tuples of fiber id and initial tree base duration
|
||||
|
||||
+1
-1
@@ -77,7 +77,7 @@
|
||||
"fbjs": "0.5.1",
|
||||
"fbjs-scripts": "0.7.0",
|
||||
"firefox-profile": "^1.0.2",
|
||||
"flow-bin": "^0.93.0",
|
||||
"flow-bin": "^0.94.0",
|
||||
"fs-extra": "^3.0.1",
|
||||
"gh-pages": "^1.0.0",
|
||||
"immutable": "3.7.6",
|
||||
|
||||
@@ -1381,14 +1381,16 @@ export function attach(
|
||||
|
||||
function getProfilingSummary(rootID: number): ProfilingSummary {
|
||||
const interactions = new Set();
|
||||
const commits = [];
|
||||
const commitDurations = [];
|
||||
const commitTimes = [];
|
||||
|
||||
const commitProfilingMetadata = ((rootToCommitProfilingMetadataMap: any): CommitProfilingMetadataMap).get(
|
||||
rootID
|
||||
);
|
||||
if (commitProfilingMetadata != null) {
|
||||
commitProfilingMetadata.forEach(metadata => {
|
||||
commits.push(metadata.commitTime, metadata.maxActualDuration);
|
||||
commitDurations.push(metadata.maxActualDuration);
|
||||
commitTimes.push(metadata.commitTime);
|
||||
metadata.interactions.forEach(({ name, timestamp }) => {
|
||||
interactions.add(`${timestamp}:${name}`);
|
||||
});
|
||||
@@ -1405,7 +1407,8 @@ export function attach(
|
||||
);
|
||||
|
||||
return {
|
||||
commits,
|
||||
commitDurations,
|
||||
commitTimes,
|
||||
initialTreeBaseDurations,
|
||||
interactionCount: interactions.size,
|
||||
rootID,
|
||||
|
||||
@@ -55,7 +55,8 @@ export type ReactRenderer = {
|
||||
};
|
||||
|
||||
export type ProfilingSummary = {|
|
||||
commits: Array<number>,
|
||||
commitDurations: Array<number>,
|
||||
commitTimes: Array<number>,
|
||||
initialTreeBaseDurations: Array<number>,
|
||||
interactionCount: number,
|
||||
rootID: number,
|
||||
|
||||
@@ -8,7 +8,7 @@ import type { Bridge } from '../types';
|
||||
import type { ProfilingSummary as ProfilingSummaryBackend } from 'src/backend/types';
|
||||
import type { ProfilingSummary as ProfilingSummaryFrontend } from 'src/devtools/views/Profiler/types';
|
||||
|
||||
type AAA = {|
|
||||
type RendererAndRootID = {|
|
||||
rootID: number,
|
||||
rendererID: number,
|
||||
|};
|
||||
@@ -19,18 +19,17 @@ export default class ProfilingCache {
|
||||
(profilingSummary: ProfilingSummaryFrontend) => void
|
||||
> = new Map();
|
||||
|
||||
ProfilingSummary: Resource<AAA, ProfilingSummaryFrontend>;
|
||||
|
||||
// TODO (profiling) renderer + root
|
||||
ProfilingSummary: Resource<RendererAndRootID, ProfilingSummaryFrontend>;
|
||||
|
||||
constructor(bridge: Bridge, store: Store) {
|
||||
this.ProfilingSummary = createResource(
|
||||
({ rendererID, rootID }: AAA) => {
|
||||
({ rendererID, rootID }: RendererAndRootID) => {
|
||||
return new Promise(resolve => {
|
||||
if (!store._profilingOperations.has(rootID)) {
|
||||
// If no profiling data was recorded for this root, skip the round trip.
|
||||
resolve({
|
||||
commits: [],
|
||||
commitDurations: [],
|
||||
commitTimes: [],
|
||||
initialTreeBaseDurations: new Map(),
|
||||
interactionCount: 0,
|
||||
});
|
||||
@@ -40,7 +39,7 @@ export default class ProfilingCache {
|
||||
}
|
||||
});
|
||||
},
|
||||
({ rendererID, rootID }: AAA) => rootID
|
||||
({ rendererID, rootID }: RendererAndRootID) => rootID
|
||||
);
|
||||
|
||||
bridge.addListener('profilingSummary', this.onProfileSummary);
|
||||
@@ -52,7 +51,8 @@ export default class ProfilingCache {
|
||||
}
|
||||
|
||||
onProfileSummary = ({
|
||||
commits,
|
||||
commitDurations,
|
||||
commitTimes,
|
||||
initialTreeBaseDurations,
|
||||
interactionCount,
|
||||
rootID,
|
||||
@@ -68,7 +68,8 @@ export default class ProfilingCache {
|
||||
}
|
||||
|
||||
resolve({
|
||||
commits,
|
||||
commitDurations,
|
||||
commitTimes,
|
||||
initialTreeBaseDurations: initialTreeBaseDurationsMap,
|
||||
interactionCount,
|
||||
});
|
||||
|
||||
@@ -275,6 +275,9 @@ export default class Store extends EventEmitter {
|
||||
}
|
||||
|
||||
startProfiling(): void {
|
||||
// Invalidate suspense cache if profiling data is being (re-)recorded.
|
||||
this._profilingCache.invalidate();
|
||||
|
||||
this._bridge.send('startProfiling');
|
||||
this._isProfiling = false;
|
||||
this.emit('isProfiling');
|
||||
|
||||
@@ -69,7 +69,7 @@ function ProfilerInner(_: Props) {
|
||||
</div>
|
||||
<div className={styles.Content}>
|
||||
{view}
|
||||
{isFilterModalShowing && ( // TODO (profiler) Position when snapshot graph is open
|
||||
{isFilterModalShowing && (
|
||||
<FilterModal dismissModal={dismissFilterModal} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -88,6 +88,12 @@ function ProfilerContextController({ children }: Props) {
|
||||
setCommitIndex(0);
|
||||
}
|
||||
|
||||
const [prevIsProfiling, setPrevIsProfiling] = useState(isProfiling);
|
||||
if (prevIsProfiling !== isProfiling) {
|
||||
setPrevIsProfiling(isProfiling);
|
||||
setCommitIndex(0);
|
||||
}
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
commitIndex,
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
// @flow
|
||||
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
import { FixedSizeList } from 'react-window';
|
||||
import SnapshotCommitListItem from './SnapshotCommitListItem';
|
||||
import { minBarWidth } from './constants';
|
||||
|
||||
import type { ProfilingSummary } from './types';
|
||||
|
||||
export type ItemData = {|
|
||||
commitDurations: Array<number>,
|
||||
commitTimes: Array<number>,
|
||||
isMouseDown: boolean,
|
||||
maxDuration: number,
|
||||
selectedCommitIndex: number,
|
||||
setCommitIndex: (index: number) => void,
|
||||
|};
|
||||
|
||||
type Props = {|
|
||||
profilingSummary: ProfilingSummary,
|
||||
selectedCommitIndex: number,
|
||||
setCommitIndex: (index: number) => void,
|
||||
viewNextCommit: () => void,
|
||||
viewPrevCommit: () => void,
|
||||
|};
|
||||
|
||||
export default function SnapshotCommitList(props: Props) {
|
||||
return (
|
||||
<AutoSizer>
|
||||
{({ height, width }) => <List height={height} width={width} {...props} />}
|
||||
</AutoSizer>
|
||||
);
|
||||
}
|
||||
|
||||
type ListProps = {|
|
||||
height: number,
|
||||
profilingSummary: ProfilingSummary,
|
||||
selectedCommitIndex: number,
|
||||
setCommitIndex: (index: number) => void,
|
||||
viewNextCommit: () => void,
|
||||
viewPrevCommit: () => void,
|
||||
width: number,
|
||||
|};
|
||||
|
||||
function List({
|
||||
height,
|
||||
profilingSummary,
|
||||
selectedCommitIndex,
|
||||
setCommitIndex,
|
||||
viewNextCommit,
|
||||
viewPrevCommit,
|
||||
width,
|
||||
}: ListProps) {
|
||||
const listRef = useRef<FixedSizeList<ItemData> | null>(null);
|
||||
const [isMouseDown, setIsMouseDown] = useState(false);
|
||||
const prevSelectedCommitIndexRef = useRef<number>(-1);
|
||||
|
||||
// Make sure any newly selected snapshot is visible within the list.
|
||||
useEffect(() => {
|
||||
if (selectedCommitIndex !== prevSelectedCommitIndexRef.current) {
|
||||
prevSelectedCommitIndexRef.current = selectedCommitIndex;
|
||||
if (listRef.current !== null) {
|
||||
listRef.current.scrollToItem(selectedCommitIndex);
|
||||
}
|
||||
}
|
||||
}, [listRef, selectedCommitIndex]);
|
||||
|
||||
const handleMouseDown = useCallback(() => {
|
||||
setIsMouseDown(true);
|
||||
}, []);
|
||||
const handleMouseUp = useCallback(() => {
|
||||
setIsMouseDown(false);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('mouseup', handleMouseUp);
|
||||
return () => {
|
||||
window.removeEventListener('mouseup', handleMouseUp);
|
||||
};
|
||||
}, [handleMouseUp]);
|
||||
|
||||
const { commitDurations, commitTimes } = profilingSummary;
|
||||
|
||||
const itemSize = useMemo(
|
||||
() => Math.max(minBarWidth, width / commitDurations.length),
|
||||
[commitDurations, width]
|
||||
);
|
||||
const maxDuration = useMemo(
|
||||
() =>
|
||||
commitDurations.reduce(
|
||||
(maxDuration, duration) => Math.max(maxDuration, duration),
|
||||
0
|
||||
),
|
||||
[commitDurations]
|
||||
);
|
||||
|
||||
// Pass required contextual data down to the ListItem renderer.
|
||||
const itemData = useMemo<ItemData>(
|
||||
() => ({
|
||||
commitDurations,
|
||||
commitTimes,
|
||||
isMouseDown,
|
||||
maxDuration,
|
||||
selectedCommitIndex,
|
||||
setCommitIndex,
|
||||
}),
|
||||
[
|
||||
commitDurations,
|
||||
commitTimes,
|
||||
isMouseDown,
|
||||
maxDuration,
|
||||
selectedCommitIndex,
|
||||
setCommitIndex,
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
onMouseDown={handleMouseDown}
|
||||
onMouseUp={handleMouseUp}
|
||||
style={{ height, width }}
|
||||
>
|
||||
{commitDurations.length > 0 && (
|
||||
<FixedSizeList
|
||||
direction="horizontal"
|
||||
height={height}
|
||||
itemCount={commitDurations.length}
|
||||
itemData={itemData}
|
||||
itemSize={itemSize}
|
||||
ref={(listRef: any) /* Flow bug? */}
|
||||
width={width}
|
||||
>
|
||||
{SnapshotCommitListItem}
|
||||
</FixedSizeList>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
// @flow
|
||||
|
||||
import React, { memo, useCallback } from 'react';
|
||||
import { areEqual } from 'react-window';
|
||||
import { getGradientColor, formatDuration, formatTime } from './utils';
|
||||
|
||||
import type { ItemData } from './SnapshotCommitList';
|
||||
|
||||
type Props = {
|
||||
data: ItemData,
|
||||
index: number,
|
||||
style: Object,
|
||||
};
|
||||
|
||||
function SnapshotCommitListItem({ data: itemData, index, style }: Props) {
|
||||
const {
|
||||
commitDurations,
|
||||
commitTimes,
|
||||
isMouseDown,
|
||||
maxDuration,
|
||||
selectedCommitIndex,
|
||||
setCommitIndex,
|
||||
} = itemData;
|
||||
|
||||
const commitDuration = commitDurations[index];
|
||||
const commitTime = commitTimes[index];
|
||||
|
||||
const handleClick = useCallback(() => setCommitIndex(index), [
|
||||
index,
|
||||
setCommitIndex,
|
||||
]);
|
||||
|
||||
// Guard against commits with duration 0
|
||||
const percentage =
|
||||
Math.min(1, Math.max(0, commitDuration / maxDuration)) || 0;
|
||||
const isSelected = selectedCommitIndex === index;
|
||||
|
||||
// Leave a 1px gap between snapshots
|
||||
const width = parseFloat(style.width) - 1;
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={handleClick}
|
||||
onMouseEnter={isMouseDown ? handleClick : null}
|
||||
style={{
|
||||
...style,
|
||||
width,
|
||||
userSelect: 'none',
|
||||
cursor: 'pointer',
|
||||
borderBottom: isSelected
|
||||
? '3px solid var(--color-tree-node-selected)'
|
||||
: '3px solid transparent',
|
||||
paddingTop: 4,
|
||||
paddingBottom: 1,
|
||||
display: 'flex',
|
||||
alignItems: 'flex-end',
|
||||
}}
|
||||
title={`Duration ${formatDuration(commitDuration)}ms at ${formatTime(
|
||||
commitTime
|
||||
)}s`}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width,
|
||||
height: `${Math.round(percentage * 100)}%`,
|
||||
minHeight: 5,
|
||||
backgroundColor:
|
||||
percentage === 0
|
||||
? 'var(--color-commit-did-not-render)'
|
||||
: getGradientColor(percentage),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo<Props>(SnapshotCommitListItem, areEqual);
|
||||
@@ -1,5 +1,7 @@
|
||||
.SnapshotSelector {
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
color: var(--color-text-color);
|
||||
margin-left: 0.5rem;
|
||||
@@ -10,6 +12,8 @@
|
||||
}
|
||||
|
||||
.Commits {
|
||||
flex: 1 1 150px;
|
||||
height: 2.25rem;
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
||||
@@ -19,3 +23,7 @@
|
||||
background-color: var(--color-border);
|
||||
margin: 0 0.25rem;
|
||||
}
|
||||
|
||||
.Number {
|
||||
font-family: var(--font-family-monospace);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import Button from '../Button';
|
||||
import ButtonIcon from '../ButtonIcon';
|
||||
import { StoreContext } from '../context';
|
||||
import { ProfilerContext } from './ProfilerContext';
|
||||
import SnapshotCommitList from './SnapshotCommitList';
|
||||
|
||||
import styles from './SnapshotSelector.css';
|
||||
|
||||
@@ -34,7 +35,7 @@ function SnapshotSelector(_: Props) {
|
||||
rootID: ((rootID: any): number),
|
||||
});
|
||||
|
||||
const numCommits = profilingSummary.commits.length / 2;
|
||||
const numCommits = profilingSummary.commitDurations.length;
|
||||
|
||||
if (numCommits === 0) {
|
||||
return null;
|
||||
@@ -51,7 +52,10 @@ function SnapshotSelector(_: Props) {
|
||||
<Fragment>
|
||||
<div className={styles.VRule} />
|
||||
<div className={styles.SnapshotSelector}>
|
||||
{commitIndex + 1} / {numCommits}
|
||||
<span className={styles.Number}>
|
||||
{`${commitIndex + 1}`.padStart(`${numCommits}`.length, '0')} /{' '}
|
||||
{numCommits}
|
||||
</span>
|
||||
<Button
|
||||
className={styles.Button}
|
||||
disabled={commitIndex <= 0}
|
||||
@@ -60,7 +64,13 @@ function SnapshotSelector(_: Props) {
|
||||
<ButtonIcon type="previous" />
|
||||
</Button>
|
||||
<div className={styles.Commits}>
|
||||
[] {/* TODO (profiling) Add FixedSizeList selector */}
|
||||
<SnapshotCommitList
|
||||
profilingSummary={profilingSummary}
|
||||
selectedCommitIndex={commitIndex}
|
||||
setCommitIndex={setCommitIndex}
|
||||
viewNextCommit={viewNextCommit}
|
||||
viewPrevCommit={viewPrevCommit}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
className={styles.Button}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
// @flow
|
||||
|
||||
export const barHeight = 20;
|
||||
export const barWidth = 100;
|
||||
export const barWidthThreshold = 2;
|
||||
export const minBarHeight = 5;
|
||||
export const minBarWidth = 5;
|
||||
export const textHeight = 18;
|
||||
@@ -29,8 +29,11 @@ export type CommitDetails = {|
|
||||
|};
|
||||
|
||||
export type ProfilingSummary = {|
|
||||
// Tuples of commit time (relative to when profiling started) and duration
|
||||
commits: Array<number>,
|
||||
// Commit durations
|
||||
commitDurations: Array<number>,
|
||||
|
||||
// Commit times (relative to when profiling started)
|
||||
commitTimes: Array<number>,
|
||||
|
||||
// Map of fiber id to (initial) tree base duration
|
||||
initialTreeBaseDurations: Map<number, number>,
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
// @flow
|
||||
|
||||
const commitGradient = [
|
||||
'var(--color-commit-gradient-0)',
|
||||
'var(--color-commit-gradient-1)',
|
||||
'var(--color-commit-gradient-2)',
|
||||
'var(--color-commit-gradient-3)',
|
||||
'var(--color-commit-gradient-4)',
|
||||
'var(--color-commit-gradient-5)',
|
||||
'var(--color-commit-gradient-6)',
|
||||
'var(--color-commit-gradient-7)',
|
||||
'var(--color-commit-gradient-8)',
|
||||
'var(--color-commit-gradient-9)',
|
||||
];
|
||||
|
||||
export const getGradientColor = (value: number) => {
|
||||
const maxIndex = commitGradient.length - 1;
|
||||
let index;
|
||||
if (Number.isNaN(value)) {
|
||||
index = 0;
|
||||
} else if (!Number.isFinite(value)) {
|
||||
index = maxIndex;
|
||||
} else {
|
||||
index = Math.max(0, Math.min(maxIndex, value)) * maxIndex;
|
||||
}
|
||||
return commitGradient[Math.round(index)];
|
||||
};
|
||||
|
||||
export const formatDuration = (duration: number) =>
|
||||
Math.round(duration * 10) / 10;
|
||||
export const formatPercentage = (percentage: number) =>
|
||||
Math.round(percentage * 100);
|
||||
export const formatTime = (timestamp: number) =>
|
||||
Math.round(Math.round(timestamp) / 100) / 10;
|
||||
@@ -134,6 +134,17 @@ function updateThemeVariables(theme: Theme): void {
|
||||
updateStyleHelper(theme, 'color-button-disabled');
|
||||
updateStyleHelper(theme, 'color-button-focus');
|
||||
updateStyleHelper(theme, 'color-button-hover');
|
||||
updateStyleHelper(theme, 'color-commit-did-not-render');
|
||||
updateStyleHelper(theme, 'color-commit-gradient-0');
|
||||
updateStyleHelper(theme, 'color-commit-gradient-1');
|
||||
updateStyleHelper(theme, 'color-commit-gradient-2');
|
||||
updateStyleHelper(theme, 'color-commit-gradient-3');
|
||||
updateStyleHelper(theme, 'color-commit-gradient-4');
|
||||
updateStyleHelper(theme, 'color-commit-gradient-5');
|
||||
updateStyleHelper(theme, 'color-commit-gradient-6');
|
||||
updateStyleHelper(theme, 'color-commit-gradient-7');
|
||||
updateStyleHelper(theme, 'color-commit-gradient-8');
|
||||
updateStyleHelper(theme, 'color-commit-gradient-9');
|
||||
updateStyleHelper(theme, 'color-component-name');
|
||||
updateStyleHelper(theme, 'color-component-name-inverted');
|
||||
updateStyleHelper(theme, 'color-dim');
|
||||
|
||||
@@ -54,7 +54,7 @@ export function useLocalStorage<T>(
|
||||
}
|
||||
|
||||
export function useModalDismissSignal(
|
||||
modalRef: React$Ref<any>,
|
||||
modalRef: { current: HTMLDivElement | null },
|
||||
dismissCallback: Function
|
||||
): void {
|
||||
useEffect(() => {
|
||||
|
||||
@@ -16,6 +16,17 @@
|
||||
--light-color-button-focus: #3578e5;
|
||||
--light-color-button-hover: #3578e5;
|
||||
--light-color-border: #eeeeee;
|
||||
--light-color-commit-did-not-render: #777d88;
|
||||
--light-color-commit-gradient-0: #37afa9;
|
||||
--light-color-commit-gradient-1: #63b19e;
|
||||
--light-color-commit-gradient-2: #80b393;
|
||||
--light-color-commit-gradient-3: #97b488;
|
||||
--light-color-commit-gradient-4: #abb67d;
|
||||
--light-color-commit-gradient-5: #beb771;
|
||||
--light-color-commit-gradient-6: #cfb965;
|
||||
--light-color-commit-gradient-7: #dfba57;
|
||||
--light-color-commit-gradient-8: #efbb49;
|
||||
--light-color-commit-gradient-9: #febc38;
|
||||
--light-color-component-name: #8155cb;
|
||||
--light-color-component-name-inverted: #ffffff;
|
||||
--light-color-dim: #777d88;
|
||||
@@ -46,6 +57,17 @@
|
||||
--dark-color-button-focus: #a2e9fc;
|
||||
--dark-color-button-hover: #a2e9fc;
|
||||
--dark-color-border: #3d424a;
|
||||
--dark-color-commit-did-not-render: #8f949d;
|
||||
--dark-color-commit-gradient-0: #37afa9;
|
||||
--dark-color-commit-gradient-1: #63b19e;
|
||||
--dark-color-commit-gradient-2: #80b393;
|
||||
--dark-color-commit-gradient-3: #97b488;
|
||||
--dark-color-commit-gradient-4: #abb67d;
|
||||
--dark-color-commit-gradient-5: #beb771;
|
||||
--dark-color-commit-gradient-6: #cfb965;
|
||||
--dark-color-commit-gradient-7: #dfba57;
|
||||
--dark-color-commit-gradient-8: #efbb49;
|
||||
--dark-color-commit-gradient-9: #febc38;
|
||||
--dark-color-component-name: #61dafb;
|
||||
--dark-color-component-name-inverted: ##282828;
|
||||
--dark-color-dim: #8f949d;
|
||||
|
||||
@@ -4671,10 +4671,10 @@ flatstr@^1.0.4:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/flatstr/-/flatstr-1.0.5.tgz#5b451b08cbd48e2eac54a2bbe0bf46165aa14be3"
|
||||
|
||||
flow-bin@^0.93.0:
|
||||
version "0.93.0"
|
||||
resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.93.0.tgz#9192a08d88db2a8da0ff55e42420f44539791430"
|
||||
integrity sha512-p8yq4ocOlpyJgOEBEj0v0GzCP25c9WP0ilFQ8hXSbrTR7RPKuR+Whr+OitlVyp8ocdX0j1MrIwQ8x28dacy1pg==
|
||||
flow-bin@^0.94.0:
|
||||
version "0.94.0"
|
||||
resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.94.0.tgz#b5d58fe7559705b73a18229f97edfc3ab6ffffcb"
|
||||
integrity sha512-DYF7r9CJ/AksfmmB4+q+TyLMoeQPRnqtF1Pk7KY3zgfkB/nVuA3nXyzqgsIPIvnMSiFEXQcFK4z+iPxSLckZhQ==
|
||||
|
||||
flush-write-stream@^1.0.0:
|
||||
version "1.1.1"
|
||||
|
||||
Reference in New Issue
Block a user