mirror of
https://github.com/mastodon/mastodon.git
synced 2025-10-04 16:17:30 +00:00
Replace react-router-scroll-4
with inlined implementation (#36253)
This commit is contained in:
parent
6d2493ca7c
commit
d801cf8e59
|
@ -1,6 +1,7 @@
|
|||
import type { PropsWithChildren } from 'react';
|
||||
import type React from 'react';
|
||||
|
||||
import type { useLocation } from 'react-router';
|
||||
import { Router as OriginalRouter, useHistory } from 'react-router';
|
||||
|
||||
import type {
|
||||
|
@ -18,7 +19,9 @@ interface MastodonLocationState {
|
|||
mastodonModalKey?: string;
|
||||
}
|
||||
|
||||
type LocationState = MastodonLocationState | null | undefined;
|
||||
export type LocationState = MastodonLocationState | null | undefined;
|
||||
|
||||
export type MastodonLocation = ReturnType<typeof useLocation<LocationState>>;
|
||||
|
||||
type HistoryPath = Path | LocationDescriptor<LocationState>;
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ import { connect } from 'react-redux';
|
|||
import { supportsPassiveEvents } from 'detect-passive-events';
|
||||
import { throttle } from 'lodash';
|
||||
|
||||
import ScrollContainer from 'mastodon/containers/scroll_container';
|
||||
import { ScrollContainer } from 'mastodon/containers/scroll_container';
|
||||
|
||||
import IntersectionObserverArticleContainer from '../containers/intersection_observer_article_container';
|
||||
import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../features/ui/util/fullscreen';
|
||||
|
|
|
@ -5,7 +5,6 @@ import { Route } from 'react-router-dom';
|
|||
|
||||
import { Provider as ReduxProvider } from 'react-redux';
|
||||
|
||||
import { ScrollContext } from 'react-router-scroll-4';
|
||||
|
||||
import { fetchCustomEmojis } from 'mastodon/actions/custom_emojis';
|
||||
import { hydrateStore } from 'mastodon/actions/store';
|
||||
|
@ -20,6 +19,8 @@ import { store } from 'mastodon/store';
|
|||
import { isProduction } from 'mastodon/utils/environment';
|
||||
import { BodyScrollLock } from 'mastodon/features/ui/components/body_scroll_lock';
|
||||
|
||||
import { ScrollContext } from './scroll_container/scroll_context';
|
||||
|
||||
const title = isProduction() ? siteTitle : `${siteTitle} (Dev)`;
|
||||
|
||||
const hydrateAction = hydrateStore(initialState);
|
||||
|
@ -45,10 +46,6 @@ export default class Mastodon extends PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
shouldUpdateScroll (prevRouterProps, { location }) {
|
||||
return !(location.state?.mastodonModalKey && location.state?.mastodonModalKey !== prevRouterProps?.location?.state?.mastodonModalKey);
|
||||
}
|
||||
|
||||
render () {
|
||||
return (
|
||||
<IdentityContext.Provider value={this.identity}>
|
||||
|
@ -56,7 +53,7 @@ export default class Mastodon extends PureComponent {
|
|||
<ReduxProvider store={store}>
|
||||
<ErrorBoundary>
|
||||
<Router>
|
||||
<ScrollContext shouldUpdateScroll={this.shouldUpdateScroll}>
|
||||
<ScrollContext>
|
||||
<Route path='/' component={UI} />
|
||||
</ScrollContext>
|
||||
<BodyScrollLock />
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
import { ScrollContainer as OriginalScrollContainer } from 'react-router-scroll-4';
|
||||
|
||||
// ScrollContainer is used to automatically scroll to the top when pushing a
|
||||
// new history state and remembering the scroll position when going back.
|
||||
// There are a few things we need to do differently, though.
|
||||
const defaultShouldUpdateScroll = (prevRouterProps, { location }) => {
|
||||
// If the change is caused by opening a modal, do not scroll to top
|
||||
return !(location.state?.mastodonModalKey && location.state?.mastodonModalKey !== prevRouterProps?.location?.state?.mastodonModalKey);
|
||||
};
|
||||
|
||||
export default
|
||||
class ScrollContainer extends OriginalScrollContainer {
|
||||
|
||||
static defaultProps = {
|
||||
shouldUpdateScroll: defaultShouldUpdateScroll,
|
||||
};
|
||||
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
import type { MastodonLocation } from 'mastodon/components/router';
|
||||
|
||||
export type ShouldUpdateScrollFn = (
|
||||
prevLocationContext: MastodonLocation | null,
|
||||
locationContext: MastodonLocation,
|
||||
) => boolean;
|
||||
|
||||
/**
|
||||
* ScrollBehavior will automatically scroll to the top on navigations
|
||||
* or restore saved scroll positions, but on some location changes we
|
||||
* need to prevent this.
|
||||
*/
|
||||
|
||||
export const defaultShouldUpdateScroll: ShouldUpdateScrollFn = (
|
||||
prevLocation,
|
||||
location,
|
||||
) => {
|
||||
// If the change is caused by opening a modal, do not scroll to top
|
||||
const shouldUpdateScroll = !(
|
||||
location.state?.mastodonModalKey &&
|
||||
location.state.mastodonModalKey !== prevLocation?.state?.mastodonModalKey
|
||||
);
|
||||
|
||||
return shouldUpdateScroll;
|
||||
};
|
|
@ -0,0 +1,62 @@
|
|||
import React, { useContext, useEffect, useRef } from 'react';
|
||||
|
||||
import { defaultShouldUpdateScroll } from './default_should_update_scroll';
|
||||
import type { ShouldUpdateScrollFn } from './default_should_update_scroll';
|
||||
import { ScrollBehaviorContext } from './scroll_context';
|
||||
|
||||
interface ScrollContainerProps {
|
||||
/**
|
||||
* This key must be static for the element & not change
|
||||
* while the component is mounted.
|
||||
*/
|
||||
scrollKey: string;
|
||||
shouldUpdateScroll?: ShouldUpdateScrollFn;
|
||||
children: React.ReactElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* `ScrollContainer` is used to manage the scroll position of elements on the page
|
||||
* that can be scrolled independently of the page body.
|
||||
* This component is a port of the unmaintained https://github.com/ytase/react-router-scroll/
|
||||
*/
|
||||
|
||||
export const ScrollContainer: React.FC<ScrollContainerProps> = ({
|
||||
children,
|
||||
scrollKey,
|
||||
shouldUpdateScroll = defaultShouldUpdateScroll,
|
||||
}) => {
|
||||
const scrollBehaviorContext = useContext(ScrollBehaviorContext);
|
||||
|
||||
const containerRef = useRef<HTMLElement>();
|
||||
|
||||
/**
|
||||
* Register/unregister scrollable element with ScrollBehavior
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (!scrollBehaviorContext || !containerRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
scrollBehaviorContext.registerElement(
|
||||
scrollKey,
|
||||
containerRef.current,
|
||||
(prevLocation, location) => {
|
||||
// Hack to allow accessing scrollBehavior._stateStorage
|
||||
return shouldUpdateScroll.call(
|
||||
scrollBehaviorContext.scrollBehavior,
|
||||
prevLocation,
|
||||
location,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
return () => {
|
||||
scrollBehaviorContext.unregisterElement(scrollKey);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return React.Children.only(
|
||||
React.cloneElement(children, { ref: containerRef }),
|
||||
);
|
||||
};
|
|
@ -0,0 +1,141 @@
|
|||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { useLocation, useHistory } from 'react-router-dom';
|
||||
|
||||
import type { LocationBase } from 'scroll-behavior';
|
||||
import ScrollBehavior from 'scroll-behavior';
|
||||
|
||||
import type {
|
||||
LocationState,
|
||||
MastodonLocation,
|
||||
} from 'mastodon/components/router';
|
||||
import { usePrevious } from 'mastodon/hooks/usePrevious';
|
||||
|
||||
import { defaultShouldUpdateScroll } from './default_should_update_scroll';
|
||||
import type { ShouldUpdateScrollFn } from './default_should_update_scroll';
|
||||
import { SessionStorage } from './state_storage';
|
||||
|
||||
type ScrollBehaviorInstance = InstanceType<
|
||||
typeof ScrollBehavior<LocationBase, MastodonLocation>
|
||||
>;
|
||||
|
||||
export interface ScrollBehaviorContextType {
|
||||
registerElement: (
|
||||
key: string,
|
||||
element: HTMLElement,
|
||||
shouldUpdateScroll: (
|
||||
prevLocationContext: MastodonLocation | null,
|
||||
locationContext: MastodonLocation,
|
||||
) => boolean,
|
||||
) => void;
|
||||
unregisterElement: (key: string) => void;
|
||||
scrollBehavior?: ScrollBehaviorInstance;
|
||||
}
|
||||
|
||||
export const ScrollBehaviorContext =
|
||||
React.createContext<ScrollBehaviorContextType | null>(null);
|
||||
|
||||
interface ScrollContextProps {
|
||||
shouldUpdateScroll?: ShouldUpdateScrollFn;
|
||||
children: React.ReactElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* A top-level wrapper that provides the app with an instance of the
|
||||
* ScrollBehavior object. scroll-behavior is a library for managing the
|
||||
* scroll position of a single-page app in the same way the browser would
|
||||
* normally do for a multi-page app. This means it'll scroll back to top
|
||||
* when navigating to a new page, and will restore the scroll position
|
||||
* when navigating e.g. using `history.back`.
|
||||
* The library keeps a record of scroll positions in session storage.
|
||||
*
|
||||
* This component is a port of the unmaintained https://github.com/ytase/react-router-scroll/
|
||||
*/
|
||||
|
||||
export const ScrollContext: React.FC<ScrollContextProps> = ({
|
||||
children,
|
||||
shouldUpdateScroll = defaultShouldUpdateScroll,
|
||||
}) => {
|
||||
const location = useLocation<LocationState>();
|
||||
const history = useHistory<LocationState>();
|
||||
|
||||
/**
|
||||
* Keep the current location in a mutable ref so that ScrollBehavior's
|
||||
* `getCurrentLocation` can access it without having to recreate the
|
||||
* whole ScrollBehavior object
|
||||
*/
|
||||
const currentLocationRef = useRef(location);
|
||||
useEffect(() => {
|
||||
currentLocationRef.current = location;
|
||||
}, [location]);
|
||||
|
||||
/**
|
||||
* Initialise ScrollBehavior object once – using state rather
|
||||
* than a ref to simplify the types and ensure it's defined immediately.
|
||||
*/
|
||||
const [scrollBehavior] = useState(
|
||||
(): ScrollBehaviorInstance =>
|
||||
new ScrollBehavior({
|
||||
addNavigationListener: history.listen.bind(history),
|
||||
stateStorage: new SessionStorage(),
|
||||
getCurrentLocation: () =>
|
||||
currentLocationRef.current as unknown as LocationBase,
|
||||
shouldUpdateScroll: (
|
||||
prevLocationContext: MastodonLocation | null,
|
||||
locationContext: MastodonLocation,
|
||||
) =>
|
||||
// Hack to allow accessing scrollBehavior._stateStorage
|
||||
shouldUpdateScroll.call(
|
||||
scrollBehavior,
|
||||
prevLocationContext,
|
||||
locationContext,
|
||||
),
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* Handle scroll update when location changes
|
||||
*/
|
||||
const prevLocation = usePrevious(location) ?? null;
|
||||
useEffect(() => {
|
||||
scrollBehavior.updateScroll(prevLocation, location);
|
||||
}, [location, prevLocation, scrollBehavior]);
|
||||
|
||||
/**
|
||||
* Stop Scrollbehavior on unmount
|
||||
*/
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
scrollBehavior.stop();
|
||||
};
|
||||
}, [scrollBehavior]);
|
||||
|
||||
/**
|
||||
* Provide the app with a way to register separately scrollable
|
||||
* elements to also be tracked by ScrollBehavior. (By default
|
||||
* ScrollBehavior only handles scrolling on the main document body.)
|
||||
*/
|
||||
const contextValue = useMemo<ScrollBehaviorContextType>(
|
||||
() => ({
|
||||
registerElement: (key, element, shouldUpdateScroll) => {
|
||||
scrollBehavior.registerElement(
|
||||
key,
|
||||
element,
|
||||
shouldUpdateScroll,
|
||||
location,
|
||||
);
|
||||
},
|
||||
unregisterElement: (key) => {
|
||||
scrollBehavior.unregisterElement(key);
|
||||
},
|
||||
scrollBehavior,
|
||||
}),
|
||||
[location, scrollBehavior],
|
||||
);
|
||||
|
||||
return (
|
||||
<ScrollBehaviorContext.Provider value={contextValue}>
|
||||
{React.Children.only(children)}
|
||||
</ScrollBehaviorContext.Provider>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,46 @@
|
|||
import type { LocationBase, ScrollPosition } from 'scroll-behavior';
|
||||
|
||||
const STATE_KEY_PREFIX = '@@scroll|';
|
||||
|
||||
interface LocationBaseWithKey extends LocationBase {
|
||||
key?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* This module is part of our port of https://github.com/ytase/react-router-scroll/
|
||||
* and handles storing scroll positions in SessionStorage.
|
||||
* Stored positions (`[x, y]`) are keyed by the location key and an optional
|
||||
* `scrollKey` that's used for to track separately scrollable elements other
|
||||
* than the document body.
|
||||
*/
|
||||
|
||||
export class SessionStorage {
|
||||
read(
|
||||
location: LocationBaseWithKey,
|
||||
key: string | null,
|
||||
): ScrollPosition | null {
|
||||
const stateKey = this.getStateKey(location, key);
|
||||
|
||||
try {
|
||||
const value = sessionStorage.getItem(stateKey);
|
||||
return value ? (JSON.parse(value) as ScrollPosition) : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
save(location: LocationBaseWithKey, key: string | null, value: unknown) {
|
||||
const stateKey = this.getStateKey(location, key);
|
||||
const storedValue = JSON.stringify(value);
|
||||
|
||||
try {
|
||||
sessionStorage.setItem(stateKey, storedValue);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
getStateKey(location: LocationBaseWithKey, key: string | null) {
|
||||
const locationKey = location.key;
|
||||
const stateKeyBase = `${STATE_KEY_PREFIX}${locationKey}`;
|
||||
return key == null ? stateKeyBase : `${stateKeyBase}|${key}`;
|
||||
}
|
||||
}
|
|
@ -21,7 +21,7 @@ import { ColumnHeader } from 'mastodon/components/column_header';
|
|||
import { LoadMore } from 'mastodon/components/load_more';
|
||||
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
|
||||
import { RadioButton } from 'mastodon/components/radio_button';
|
||||
import ScrollContainer from 'mastodon/containers/scroll_container';
|
||||
import { ScrollContainer } from 'mastodon/containers/scroll_container';
|
||||
import { useSearchParam } from 'mastodon/hooks/useSearchParam';
|
||||
import { useAppDispatch, useAppSelector } from 'mastodon/store';
|
||||
|
||||
|
@ -206,7 +206,6 @@ export const Directory: React.FC<{
|
|||
/>
|
||||
|
||||
{multiColumn && !pinned ? (
|
||||
// @ts-expect-error ScrollContainer is not properly typed yet
|
||||
<ScrollContainer scrollKey='directory'>
|
||||
{scrollableArea}
|
||||
</ScrollContainer>
|
||||
|
|
|
@ -16,7 +16,7 @@ import VisibilityOffIcon from '@/material-icons/400-24px/visibility_off.svg?reac
|
|||
import { Hotkeys } from 'mastodon/components/hotkeys';
|
||||
import { Icon } from 'mastodon/components/icon';
|
||||
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
|
||||
import ScrollContainer from 'mastodon/containers/scroll_container';
|
||||
import { ScrollContainer } from 'mastodon/containers/scroll_container';
|
||||
import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';
|
||||
import { identityContextPropShape, withIdentity } from 'mastodon/identity_context';
|
||||
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
|
||||
|
@ -526,9 +526,9 @@ class Status extends ImmutablePureComponent {
|
|||
this.setState({ fullscreen: isFullscreen() });
|
||||
};
|
||||
|
||||
shouldUpdateScroll = (prevRouterProps, { location }) => {
|
||||
shouldUpdateScroll = (prevLocation, location) => {
|
||||
// Do not change scroll when opening a modal
|
||||
if (location.state?.mastodonModalKey !== prevRouterProps?.location?.state?.mastodonModalKey) {
|
||||
if (location.state?.mastodonModalKey !== prevLocation?.state?.mastodonModalKey) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
@ -99,7 +99,6 @@
|
|||
"react-redux-loading-bar": "^5.0.8",
|
||||
"react-router": "^5.3.4",
|
||||
"react-router-dom": "^5.3.4",
|
||||
"react-router-scroll-4": "^1.0.0-beta.1",
|
||||
"react-select": "^5.7.3",
|
||||
"react-sparklines": "^1.7.0",
|
||||
"react-swipeable-views": "^0.14.0",
|
||||
|
@ -111,6 +110,7 @@
|
|||
"rollup-plugin-gzip": "^4.1.1",
|
||||
"rollup-plugin-visualizer": "^6.0.3",
|
||||
"sass": "^1.62.1",
|
||||
"scroll-behavior": "^0.11.0",
|
||||
"stacktrace-js": "^2.0.2",
|
||||
"stringz": "^2.1.0",
|
||||
"substring-trie": "^1.0.2",
|
||||
|
|
55
yarn.lock
55
yarn.lock
|
@ -2836,7 +2836,6 @@ __metadata:
|
|||
react-redux-loading-bar: "npm:^5.0.8"
|
||||
react-router: "npm:^5.3.4"
|
||||
react-router-dom: "npm:^5.3.4"
|
||||
react-router-scroll-4: "npm:^1.0.0-beta.1"
|
||||
react-select: "npm:^5.7.3"
|
||||
react-sparklines: "npm:^1.7.0"
|
||||
react-swipeable-views: "npm:^0.14.0"
|
||||
|
@ -2849,6 +2848,7 @@ __metadata:
|
|||
rollup-plugin-gzip: "npm:^4.1.1"
|
||||
rollup-plugin-visualizer: "npm:^6.0.3"
|
||||
sass: "npm:^1.62.1"
|
||||
scroll-behavior: "npm:^0.11.0"
|
||||
stacktrace-js: "npm:^2.0.2"
|
||||
storybook: "npm:^9.1.1"
|
||||
stringz: "npm:^2.1.0"
|
||||
|
@ -6478,16 +6478,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"dom-helpers@npm:^3.4.0":
|
||||
version: 3.4.0
|
||||
resolution: "dom-helpers@npm:3.4.0"
|
||||
dependencies:
|
||||
"@babel/runtime": "npm:^7.1.2"
|
||||
checksum: 10c0/1d2d3e4eadac2c4f4c8c7470a737ab32b7ec28237c4d094ea967ec3184168fd12452196fcc424a5d7860b6176117301aeaecba39467bf1a6e8492a8e5c9639d1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"dom-helpers@npm:^5.0.1, dom-helpers@npm:^5.2.0":
|
||||
"dom-helpers@npm:^5.0.1, dom-helpers@npm:^5.1.4, dom-helpers@npm:^5.2.0":
|
||||
version: 5.2.1
|
||||
resolution: "dom-helpers@npm:5.2.1"
|
||||
dependencies:
|
||||
|
@ -10036,6 +10027,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"page-lifecycle@npm:^0.1.2":
|
||||
version: 0.1.2
|
||||
resolution: "page-lifecycle@npm:0.1.2"
|
||||
checksum: 10c0/509dbbc2ad2000dffcf591f66ab13d80fb1dba9337d85c76269173f7a5c3959b5a876e3bfb1e4494f6b932c1dc02a0b5824ebd452ab1a7204d4abdf498cb27c5
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"parent-module@npm:^1.0.0":
|
||||
version: 1.0.1
|
||||
resolution: "parent-module@npm:1.0.1"
|
||||
|
@ -11277,21 +11275,6 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-router-scroll-4@npm:^1.0.0-beta.1":
|
||||
version: 1.0.0-beta.2
|
||||
resolution: "react-router-scroll-4@npm:1.0.0-beta.2"
|
||||
dependencies:
|
||||
scroll-behavior: "npm:^0.9.1"
|
||||
warning: "npm:^3.0.0"
|
||||
peerDependencies:
|
||||
prop-types: ^15.6.0
|
||||
react: ^15.0.0 || ^16.0.0
|
||||
react-dom: ^15.0.0 || ^16.0.0
|
||||
react-router-dom: ^4.0
|
||||
checksum: 10c0/ad195b7359fd3146530cf299ec437f0a619c577b2cacfb2c76a156d3cd9d5d3e97af56e17c300c37ca8c485041e93124fe63f0c86db6aea468caf838281e62cb
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-router@npm:5.3.4, react-router@npm:^5.3.4":
|
||||
version: 5.3.4
|
||||
resolution: "react-router@npm:5.3.4"
|
||||
|
@ -12051,13 +12034,14 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"scroll-behavior@npm:^0.9.1":
|
||||
version: 0.9.12
|
||||
resolution: "scroll-behavior@npm:0.9.12"
|
||||
"scroll-behavior@npm:^0.11.0":
|
||||
version: 0.11.0
|
||||
resolution: "scroll-behavior@npm:0.11.0"
|
||||
dependencies:
|
||||
dom-helpers: "npm:^3.4.0"
|
||||
dom-helpers: "npm:^5.1.4"
|
||||
invariant: "npm:^2.2.4"
|
||||
checksum: 10c0/4f438c48b93a1dcc2ab51a18670fac6f5ce41885291d8aa13251b4a187be9d0c6dd518ee974eb52ac9bbe227b9811c2615ecca73192a1a190b78dfdadb9c2cf2
|
||||
page-lifecycle: "npm:^0.1.2"
|
||||
checksum: 10c0/c54010c9fdd9fc360fd7887ecf64f16972f9557ac679723709612cd54fc4778c7433ab46a9637933179ef31471f78e2591fb35351dc0e15537fecf1c8c89d32c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
@ -14013,15 +13997,6 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"warning@npm:^3.0.0":
|
||||
version: 3.0.0
|
||||
resolution: "warning@npm:3.0.0"
|
||||
dependencies:
|
||||
loose-envify: "npm:^1.0.0"
|
||||
checksum: 10c0/6a2a56ab3139d3927193d926a027e74e1449fa47cc692feea95f8a81a4bb5b7f10c312def94cce03f3b58cb26ba3247858e75d17d596451d2c483a62e8204705
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"warning@npm:^4.0.1, warning@npm:^4.0.3":
|
||||
version: 4.0.3
|
||||
resolution: "warning@npm:4.0.3"
|
||||
|
|
Loading…
Reference in New Issue
Block a user