mirror of
https://github.com/zhigang1992/react-navigation.git
synced 2026-01-12 22:51:18 +08:00
feat: make deep link handling more flexible
This adds ability to specify a custom config to control how to convert between state and path.
Example:
```js
{
Chat: {
path: 'chat/:author/:id',
parse: { id: Number }
}
}
```
The above config can parse a path matching the provided pattern: `chat/jane/42` to a valid state:
```js
{
routes: [
{
name: 'Chat',
params: { author: 'jane', id: 42 },
},
],
}
```
This makes it much easier to control the parsing without having to specify a custom function.
This commit is contained in:
committed by
Satyajit Sahoo
parent
17045f5b6d
commit
849d952703
@@ -31,7 +31,7 @@
|
||||
"@types/jest": "^24.0.13",
|
||||
"codecov": "^3.5.0",
|
||||
"commitlint": "^8.0.0",
|
||||
"core-js": "^3.1.4",
|
||||
"core-js": "^3.2.1",
|
||||
"eslint": "^5.16.0",
|
||||
"eslint-config-satya164": "^2.4.1",
|
||||
"husky": "^2.4.0",
|
||||
|
||||
@@ -29,6 +29,8 @@
|
||||
"clean": "del lib"
|
||||
},
|
||||
"dependencies": {
|
||||
"escape-string-regexp": "^2.0.0",
|
||||
"query-string": "^6.8.3",
|
||||
"shortid": "^2.2.14",
|
||||
"use-subscription": "^1.0.0"
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import getPathFromState from '../getPathFromState';
|
||||
|
||||
it('converts path string to initial state', () => {
|
||||
it('converts state to path string', () => {
|
||||
expect(
|
||||
getPathFromState({
|
||||
routes: [
|
||||
@@ -12,11 +12,12 @@ it('converts path string to initial state', () => {
|
||||
{ name: 'boo' },
|
||||
{
|
||||
name: 'bar',
|
||||
params: { fruit: 'apple' },
|
||||
state: {
|
||||
routes: [
|
||||
{
|
||||
name: 'baz qux',
|
||||
params: { author: 'jane & co', valid: true },
|
||||
params: { author: 'jane', valid: true },
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -26,9 +27,50 @@ it('converts path string to initial state', () => {
|
||||
},
|
||||
],
|
||||
})
|
||||
).toMatchInlineSnapshot(
|
||||
`"/foo/bar/baz%20qux?author=%22jane%20%26%20co%22&valid=true"`
|
||||
);
|
||||
).toMatchInlineSnapshot(`"/foo/bar/baz%20qux?author=jane&valid=true"`);
|
||||
});
|
||||
|
||||
it('converts state to path string with config', () => {
|
||||
expect(
|
||||
getPathFromState(
|
||||
{
|
||||
routes: [
|
||||
{
|
||||
name: 'Foo',
|
||||
state: {
|
||||
index: 1,
|
||||
routes: [
|
||||
{ name: 'boo' },
|
||||
{
|
||||
name: 'Bar',
|
||||
params: { fruit: 'apple', type: 'sweet', avaliable: false },
|
||||
state: {
|
||||
routes: [
|
||||
{
|
||||
name: 'Baz',
|
||||
params: { author: 'Jane', valid: true, id: 10 },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
Foo: 'few',
|
||||
Bar: 'bar/:type/:fruit',
|
||||
Baz: {
|
||||
path: 'baz/:author',
|
||||
stringify: {
|
||||
author: author => author.toLowerCase(),
|
||||
id: id => `x${id}`,
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
).toMatchInlineSnapshot(`"/few/bar/sweet/apple/baz/jane?id=x10&valid=true"`);
|
||||
});
|
||||
|
||||
it('handles route without param', () => {
|
||||
|
||||
@@ -2,9 +2,7 @@ import getStateFromPath from '../getStateFromPath';
|
||||
|
||||
it('converts path string to initial state', () => {
|
||||
expect(
|
||||
getStateFromPath(
|
||||
'foo/bar/baz%20qux?author=%22jane%20%26%20co%22&valid=true'
|
||||
)
|
||||
getStateFromPath('foo/bar/baz%20qux?author=jane%20%26%20co&valid=true')
|
||||
).toEqual({
|
||||
routes: [
|
||||
{
|
||||
@@ -17,7 +15,55 @@ it('converts path string to initial state', () => {
|
||||
routes: [
|
||||
{
|
||||
name: 'baz qux',
|
||||
params: { author: 'jane & co', valid: true },
|
||||
params: { author: 'jane & co', valid: 'true' },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('converts path string to initial state with config', () => {
|
||||
expect(
|
||||
getStateFromPath(
|
||||
'/few/bar/sweet/apple/baz/jane?count=10&answer=42&valid=true',
|
||||
{
|
||||
Foo: 'few',
|
||||
Bar: 'bar/:type/:fruit',
|
||||
Baz: {
|
||||
path: 'baz/:author',
|
||||
parse: {
|
||||
author: (author: string) =>
|
||||
author.replace(/^\w/, c => c.toUpperCase()),
|
||||
count: Number,
|
||||
valid: Boolean,
|
||||
},
|
||||
},
|
||||
}
|
||||
)
|
||||
).toEqual({
|
||||
routes: [
|
||||
{
|
||||
name: 'Foo',
|
||||
state: {
|
||||
routes: [
|
||||
{
|
||||
name: 'Bar',
|
||||
params: { fruit: 'apple', type: 'sweet' },
|
||||
state: {
|
||||
routes: [
|
||||
{
|
||||
name: 'Baz',
|
||||
params: {
|
||||
author: 'Jane',
|
||||
count: 10,
|
||||
answer: '42',
|
||||
valid: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -38,7 +84,7 @@ it('handles leading slash when converting', () => {
|
||||
routes: [
|
||||
{
|
||||
name: 'bar',
|
||||
params: { count: 42 },
|
||||
params: { count: '42' },
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -56,7 +102,7 @@ it('handles ending slash when converting', () => {
|
||||
routes: [
|
||||
{
|
||||
name: 'bar',
|
||||
params: { count: 42 },
|
||||
params: { count: '42' },
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -1,14 +1,45 @@
|
||||
import queryString from 'query-string';
|
||||
import { NavigationState, PartialState, Route } from './types';
|
||||
|
||||
type State = NavigationState | Omit<PartialState<NavigationState>, 'stale'>;
|
||||
|
||||
type StringifyConfig = { [key: string]: (value: any) => string };
|
||||
|
||||
type Options = {
|
||||
[routeName: string]: string | { path: string; stringify?: StringifyConfig };
|
||||
};
|
||||
|
||||
/**
|
||||
* Utility to serialize a navigation state object to a path string.
|
||||
*
|
||||
* Example:
|
||||
* ```js
|
||||
* getPathFromState(
|
||||
* {
|
||||
* routes: [
|
||||
* {
|
||||
* name: 'Chat',
|
||||
* params: { author: 'Jane', id: 42 },
|
||||
* },
|
||||
* ],
|
||||
* },
|
||||
* {
|
||||
* Chat: {
|
||||
* path: 'chat/:author/:id',
|
||||
* stringify: { author: author => author.toLowerCase() }
|
||||
* }
|
||||
* }
|
||||
* )
|
||||
* ```
|
||||
*
|
||||
* @param state Navigation state to serialize.
|
||||
* @param options Extra options to fine-tune how to serialize the path.
|
||||
* @returns Path representing the state, e.g. /foo/bar?count=42.
|
||||
*/
|
||||
export default function getPathFromState(state: State): string {
|
||||
export default function getPathFromState(
|
||||
state: State,
|
||||
options: Options = {}
|
||||
): string {
|
||||
let path = '/';
|
||||
|
||||
let current: State | undefined = state;
|
||||
@@ -19,24 +50,51 @@ export default function getPathFromState(state: State): string {
|
||||
state?: State | undefined;
|
||||
};
|
||||
|
||||
path += encodeURIComponent(route.name);
|
||||
const config =
|
||||
options[route.name] !== undefined
|
||||
? (options[route.name] as { stringify?: StringifyConfig }).stringify
|
||||
: undefined;
|
||||
|
||||
const params = route.params
|
||||
? // Stringify all of the param values before we use them
|
||||
Object.entries(route.params).reduce<{
|
||||
[key: string]: string;
|
||||
}>((acc, [key, value]) => {
|
||||
acc[key] = config && config[key] ? config[key](value) : String(value);
|
||||
return acc;
|
||||
}, {})
|
||||
: undefined;
|
||||
|
||||
if (options[route.name] !== undefined) {
|
||||
const pattern =
|
||||
typeof options[route.name] === 'string'
|
||||
? (options[route.name] as string)
|
||||
: (options[route.name] as { path: string }).path;
|
||||
|
||||
path += pattern
|
||||
.split('/')
|
||||
.map(p => {
|
||||
const name = p.replace(/^:/, '');
|
||||
|
||||
// If the path has a pattern for a param, put the param in the path
|
||||
if (params && name in params && p.startsWith(':')) {
|
||||
const value = params[name];
|
||||
// Remove the used value from the params object since we'll use the rest for query string
|
||||
delete params[name];
|
||||
return encodeURIComponent(value);
|
||||
}
|
||||
|
||||
return encodeURIComponent(p);
|
||||
})
|
||||
.join('/');
|
||||
} else {
|
||||
path += encodeURIComponent(route.name);
|
||||
}
|
||||
|
||||
if (route.state) {
|
||||
path += '/';
|
||||
} else if (route.params) {
|
||||
const query = [];
|
||||
|
||||
for (const param in route.params) {
|
||||
const value = (route.params as { [key: string]: any })[param];
|
||||
|
||||
query.push(
|
||||
`${encodeURIComponent(param)}=${encodeURIComponent(
|
||||
JSON.stringify(value)
|
||||
)}`
|
||||
);
|
||||
}
|
||||
|
||||
path += `?${query.join('&')}`;
|
||||
} else if (params) {
|
||||
path += `?${queryString.stringify(params)}`;
|
||||
}
|
||||
|
||||
current = route.state;
|
||||
|
||||
@@ -1,50 +1,149 @@
|
||||
import escape from 'escape-string-regexp';
|
||||
import queryString from 'query-string';
|
||||
import { NavigationState, PartialState } from './types';
|
||||
|
||||
type ParseConfig = { [key: string]: (value: string) => any };
|
||||
|
||||
type Options = {
|
||||
[routeName: string]: string | { path: string; parse?: ParseConfig };
|
||||
};
|
||||
|
||||
/**
|
||||
* Utility to parse a path string to initial state object accepted by the container.
|
||||
* This is useful for deep linking when we need to handle the incoming URL.
|
||||
*
|
||||
* Example:
|
||||
* ```js
|
||||
* getStateFromPath(
|
||||
* '/chat/jane/42',
|
||||
* {
|
||||
* Chat: {
|
||||
* path: 'chat/:author/:id',
|
||||
* parse: { id: Number }
|
||||
* }
|
||||
* }
|
||||
* )
|
||||
* ```
|
||||
* @param path Path string to parse and convert, e.g. /foo/bar?count=42.
|
||||
* @param options Extra options to fine-tune how to parse the path.
|
||||
*/
|
||||
export default function getStateFromPath(
|
||||
path: string
|
||||
path: string,
|
||||
options: Options = {}
|
||||
): PartialState<NavigationState> | undefined {
|
||||
const parts = path.split('?');
|
||||
const segments = parts[0].split('/').filter(Boolean);
|
||||
const query = parts[1] ? parts[1].split('&') : undefined;
|
||||
// Create a normalized config array which will be easier to use
|
||||
const routeConfig = Object.keys(options).map(key => {
|
||||
const pattern =
|
||||
typeof options[key] === 'string'
|
||||
? (options[key] as string)
|
||||
: (options[key] as { path: string }).path;
|
||||
|
||||
// Create a regex from the provided path pattern
|
||||
// With the pattern, we can match segements containing params and extract them
|
||||
const match = new RegExp(
|
||||
'^' + escape(pattern).replace(/:[a-z0-9]+/gi, '([^/]+)') + '/?'
|
||||
);
|
||||
|
||||
return {
|
||||
match,
|
||||
pattern,
|
||||
routeName: key,
|
||||
// @ts-ignore
|
||||
parse: options[key].parse,
|
||||
};
|
||||
});
|
||||
|
||||
let result: PartialState<NavigationState> | undefined;
|
||||
let current: PartialState<NavigationState> | undefined;
|
||||
|
||||
while (segments.length) {
|
||||
let remaining = path
|
||||
.replace(/[/]+/, '/') // Replace multiple slash (//) with single ones
|
||||
.replace(/^\//, '') // Remove extra leading slash
|
||||
.replace(/\?.*/, ''); // Remove query params which we will handle later
|
||||
|
||||
while (remaining) {
|
||||
let routeName;
|
||||
let params;
|
||||
|
||||
// Go through all configs, and see if the next path segment matches our regex
|
||||
for (const config of routeConfig) {
|
||||
const match = remaining.match(config.match);
|
||||
|
||||
// If our regex matches, we need to extract params from the path
|
||||
if (match) {
|
||||
routeName = config.routeName;
|
||||
|
||||
const paramPatterns = config.pattern
|
||||
.split('/')
|
||||
.filter(p => p.startsWith(':'));
|
||||
|
||||
if (paramPatterns.length) {
|
||||
params = paramPatterns.reduce<{ [key: string]: any }>((acc, p, i) => {
|
||||
const key = p.replace(/^:/, '');
|
||||
const value = match[i + 1]; // The param segments start from index 1 in the regex match result
|
||||
|
||||
acc[key] =
|
||||
config.parse && config.parse[key]
|
||||
? config.parse[key](value)
|
||||
: value;
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
// Remove the matched segment from the remaining path
|
||||
remaining = remaining.replace(match[0], '');
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If we hadn't matched any segments earlier, use the path as route name
|
||||
if (routeName === undefined) {
|
||||
const segments = remaining.split('/');
|
||||
|
||||
routeName = decodeURIComponent(segments[0]);
|
||||
segments.shift();
|
||||
remaining = segments.join('/');
|
||||
}
|
||||
|
||||
const state = {
|
||||
routes: [{ name: decodeURIComponent(segments[0]) }],
|
||||
routes: [{ name: routeName, params }],
|
||||
};
|
||||
|
||||
if (current) {
|
||||
// The state should be nested inside the route we parsed before
|
||||
current.routes[0].state = state;
|
||||
} else {
|
||||
result = state;
|
||||
}
|
||||
|
||||
current = state;
|
||||
segments.shift();
|
||||
}
|
||||
|
||||
if (current == null || result == null) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const query = path.split('?')[1];
|
||||
|
||||
if (query) {
|
||||
const params = query.reduce<{ [key: string]: any }>((acc, curr) => {
|
||||
const [key, value] = curr.split('=');
|
||||
const route = current.routes[0];
|
||||
|
||||
acc[decodeURIComponent(key)] = JSON.parse(decodeURIComponent(value));
|
||||
const params = queryString.parse(query);
|
||||
const config = options[route.name]
|
||||
? (options[route.name] as { parse?: ParseConfig }).parse
|
||||
: undefined;
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
if (config) {
|
||||
Object.keys(params).forEach(name => {
|
||||
if (config[name] && typeof params[name] === 'string') {
|
||||
params[name] = config[name](params[name] as string);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
current.routes[0].params = params;
|
||||
route.params = { ...route.params, ...params };
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
@@ -7,6 +7,12 @@ import {
|
||||
PartialState,
|
||||
} from '@react-navigation/core';
|
||||
|
||||
type Config = {
|
||||
[routeName: string]:
|
||||
| string
|
||||
| { path: string; parse?: { [key: string]: (value: string) => any } };
|
||||
};
|
||||
|
||||
type Options = {
|
||||
/**
|
||||
* The prefixes are stripped from the URL before parsing them.
|
||||
@@ -14,27 +20,44 @@ type Options = {
|
||||
*/
|
||||
prefixes: string[];
|
||||
/**
|
||||
* Custom function to parse the URL object to a valid navigation state.
|
||||
* Config to fine-tune how to parse the path.
|
||||
*
|
||||
* Example:
|
||||
* ```js
|
||||
* {
|
||||
* Chat: {
|
||||
* path: 'chat/:author/:id',
|
||||
* parse: { id: Number }
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
config?: Config;
|
||||
/**
|
||||
* Custom function to parse the URL object to a valid navigation state (advanced).
|
||||
*/
|
||||
getStateFromPath?: (
|
||||
path: string
|
||||
path: string,
|
||||
options?: Config
|
||||
) => PartialState<NavigationState> | undefined;
|
||||
};
|
||||
|
||||
export default function useLinking(
|
||||
ref: React.RefObject<NavigationContainerRef>,
|
||||
{ prefixes, getStateFromPath = getStateFromPathDefault }: Options
|
||||
{ prefixes, config, getStateFromPath = getStateFromPathDefault }: Options
|
||||
) {
|
||||
// We store these options in ref to avoid re-creating getInitialState and re-subscribing listeners
|
||||
// This lets user avoid wrapping the items in `React.useCallback` or `React.useMemo`
|
||||
// Not re-creating `getInitialState` is important coz it makes it easier for the user to use in an effect
|
||||
const prefixesRef = React.useRef(prefixes);
|
||||
const configRef = React.useRef(config);
|
||||
const getStateFromPathRef = React.useRef(getStateFromPath);
|
||||
|
||||
React.useEffect(() => {
|
||||
prefixesRef.current = prefixes;
|
||||
configRef.current = config;
|
||||
getStateFromPathRef.current = getStateFromPath;
|
||||
}, [getStateFromPath, prefixes]);
|
||||
}, [config, getStateFromPath, prefixes]);
|
||||
|
||||
const extractPathFromURL = React.useCallback((url: string) => {
|
||||
for (const prefix of prefixesRef.current) {
|
||||
@@ -51,7 +74,7 @@ export default function useLinking(
|
||||
const path = url ? extractPathFromURL(url) : null;
|
||||
|
||||
if (path) {
|
||||
return getStateFromPathRef.current(path);
|
||||
return getStateFromPathRef.current(path, configRef.current);
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
@@ -63,7 +86,7 @@ export default function useLinking(
|
||||
const navigation = ref.current;
|
||||
|
||||
if (navigation && path) {
|
||||
const state = getStateFromPathRef.current(path);
|
||||
const state = getStateFromPathRef.current(path, configRef.current);
|
||||
|
||||
if (state) {
|
||||
navigation.resetRoot(state);
|
||||
|
||||
26
yarn.lock
26
yarn.lock
@@ -4941,7 +4941,7 @@ core-js@^2.2.2, core-js@^2.4.0, core-js@^2.4.1, core-js@^2.5.0, core-js@^2.6.5:
|
||||
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.9.tgz#6b4b214620c834152e179323727fc19741b084f2"
|
||||
integrity sha512-HOpZf6eXmnl7la+cUdMnLvUxKNqLUzJvgIziQ0DiF3JwSImNphIqdGqzj6hIKyX04MmV0poclQ7+wjWvxQyR2A==
|
||||
|
||||
core-js@^3.0.0, core-js@^3.1.4:
|
||||
core-js@^3.0.0, core-js@^3.2.1:
|
||||
version "3.2.1"
|
||||
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.2.1.tgz#cd41f38534da6cc59f7db050fe67307de9868b09"
|
||||
integrity sha512-Qa5XSVefSVPRxy2XfUC13WbvqkxhkwB3ve+pgCQveNgYzbM/UxZeu1dcOX/xr4UmfUd+muuvsaxilQzCyUurMw==
|
||||
@@ -5960,6 +5960,11 @@ escape-string-regexp@1.0.5, escape-string-regexp@^1.0.2, escape-string-regexp@^1
|
||||
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
|
||||
integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=
|
||||
|
||||
escape-string-regexp@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344"
|
||||
integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==
|
||||
|
||||
escodegen@^1.9.1:
|
||||
version "1.12.0"
|
||||
resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.12.0.tgz#f763daf840af172bb3a2b6dd7219c0e17f7ff541"
|
||||
@@ -12593,6 +12598,15 @@ qs@^6.5.0:
|
||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.8.0.tgz#87b763f0d37ca54200334cd57bb2ef8f68a1d081"
|
||||
integrity sha512-tPSkj8y92PfZVbinY1n84i1Qdx75lZjMQYx9WZhnkofyxzw2r7Ho39G3/aEvSUdebxpnnM4LZJCtvE/Aq3+s9w==
|
||||
|
||||
query-string@^6.8.3:
|
||||
version "6.8.3"
|
||||
resolved "https://registry.yarnpkg.com/query-string/-/query-string-6.8.3.tgz#fd9fb7ffb068b79062b43383685611ee47777d4b"
|
||||
integrity sha512-llcxWccnyaWlODe7A9hRjkvdCKamEKTh+wH8ITdTc3OhchaqUZteiSCX/2ablWHVrkVIe04dntnaZJ7BdyW0lQ==
|
||||
dependencies:
|
||||
decode-uri-component "^0.2.0"
|
||||
split-on-first "^1.0.0"
|
||||
strict-uri-encode "^2.0.0"
|
||||
|
||||
querystring-es3@^0.2.0:
|
||||
version "0.2.1"
|
||||
resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73"
|
||||
@@ -14225,6 +14239,11 @@ spdy@^4.0.0:
|
||||
select-hose "^2.0.0"
|
||||
spdy-transport "^3.0.0"
|
||||
|
||||
split-on-first@^1.0.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/split-on-first/-/split-on-first-1.1.0.tgz#f610afeee3b12bce1d0c30425e76398b78249a5f"
|
||||
integrity sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==
|
||||
|
||||
split-string@^3.0.1, split-string@^3.0.2:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/split-string/-/split-string-3.1.0.tgz#7cb09dda3a86585705c64b39a6466038682e8fe2"
|
||||
@@ -14381,6 +14400,11 @@ stream-shift@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.0.tgz#d5c752825e5367e786f78e18e445ea223a155952"
|
||||
integrity sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=
|
||||
|
||||
strict-uri-encode@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz#b9c7330c7042862f6b142dc274bbcc5866ce3546"
|
||||
integrity sha1-ucczDHBChi9rFC3CdLvMWGbONUY=
|
||||
|
||||
string-length@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/string-length/-/string-length-2.0.0.tgz#d40dbb686a3ace960c1cffca562bf2c45f8363ed"
|
||||
|
||||
Reference in New Issue
Block a user