feat(gui): load and merge server params with config
This commit is contained in:
parent
03fd728ab0
commit
37efd51341
|
@ -2,10 +2,18 @@
|
|||
"api": {
|
||||
"root": "http://127.0.0.1:5000"
|
||||
},
|
||||
"default": {
|
||||
"model": "stable-diffusion-onnx-v1-5",
|
||||
"platform": "amd",
|
||||
"scheduler": "euler-a",
|
||||
"prompt": "an astronaut eating a hamburger"
|
||||
"params": {
|
||||
"model": {
|
||||
"default": "stable-diffusion-onnx-v1-5"
|
||||
},
|
||||
"platform": {
|
||||
"default": "amd"
|
||||
},
|
||||
"scheduler": {
|
||||
"default": "euler-a"
|
||||
},
|
||||
"prompt": {
|
||||
"default": "an astronaut eating a hamburger"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -13,7 +13,9 @@
|
|||
"@mui/icons-material": "^5.11.0",
|
||||
"@mui/lab": "^5.0.0-alpha.114",
|
||||
"@mui/material": "^5.11.3",
|
||||
"@types/lodash": "^4.14.191",
|
||||
"@types/node": "^18.11.18",
|
||||
"lodash": "^4.17.21",
|
||||
"noicejs": "^5.0.0-3",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { doesExist, NotImplementedError } from '@apextoaster/js-utils';
|
||||
import { ConfigParams } from '../config';
|
||||
|
||||
export interface BaseImgParams {
|
||||
/**
|
||||
|
@ -57,6 +58,7 @@ export interface ApiResponse {
|
|||
|
||||
export interface ApiClient {
|
||||
models(): Promise<Array<string>>;
|
||||
params(): Promise<ConfigParams>;
|
||||
platforms(): Promise<Array<string>>;
|
||||
schedulers(): Promise<Array<string>>;
|
||||
|
||||
|
@ -136,6 +138,11 @@ export function makeClient(root: string, f = fetch): ApiClient {
|
|||
const res = await f(path);
|
||||
return await res.json() as Array<string>;
|
||||
},
|
||||
async params(): Promise<ConfigParams> {
|
||||
const path = new URL(joinPath('settings', 'params'), root);
|
||||
const res = await f(path);
|
||||
return await res.json() as ConfigParams;
|
||||
},
|
||||
async schedulers(): Promise<Array<string>> {
|
||||
const path = new URL(joinPath('settings', 'schedulers'), root);
|
||||
const res = await f(path);
|
||||
|
|
|
@ -1,28 +1,30 @@
|
|||
import { doesExist } from '@apextoaster/js-utils';
|
||||
import { Casino } from '@mui/icons-material';
|
||||
import { IconButton, Stack, TextField } from '@mui/material';
|
||||
import { Button, Stack, TextField } from '@mui/material';
|
||||
import * as React from 'react';
|
||||
|
||||
import { BaseImgParams } from '../api/client.js';
|
||||
import { CONFIG_DEFAULTS } from '../config.js';
|
||||
import { ConfigParams } from '../config.js';
|
||||
import { NumericField } from './NumericField.js';
|
||||
|
||||
export interface ImageControlProps {
|
||||
config: ConfigParams;
|
||||
params: BaseImgParams;
|
||||
|
||||
onChange?: (params: BaseImgParams) => void;
|
||||
}
|
||||
|
||||
export function ImageControl(props: ImageControlProps) {
|
||||
const { params } = props;
|
||||
const { config, params } = props;
|
||||
|
||||
return <Stack spacing={2}>
|
||||
<Stack direction='row' spacing={4}>
|
||||
<NumericField
|
||||
decimal
|
||||
label='CFG'
|
||||
min={CONFIG_DEFAULTS.cfg.min}
|
||||
max={CONFIG_DEFAULTS.cfg.max}
|
||||
step={CONFIG_DEFAULTS.cfg.step}
|
||||
min={config.cfg.min}
|
||||
max={config.cfg.max}
|
||||
step={config.cfg.step}
|
||||
value={params.cfg}
|
||||
onChange={(cfg) => {
|
||||
if (doesExist(props.onChange)) {
|
||||
|
@ -35,9 +37,9 @@ export function ImageControl(props: ImageControlProps) {
|
|||
/>
|
||||
<NumericField
|
||||
label='Steps'
|
||||
min={CONFIG_DEFAULTS.steps.min}
|
||||
max={CONFIG_DEFAULTS.steps.max}
|
||||
step={CONFIG_DEFAULTS.steps.step}
|
||||
min={config.steps.min}
|
||||
max={config.steps.max}
|
||||
step={config.steps.step}
|
||||
value={params.steps}
|
||||
onChange={(steps) => {
|
||||
if (doesExist(props.onChange)) {
|
||||
|
@ -50,9 +52,9 @@ export function ImageControl(props: ImageControlProps) {
|
|||
/>
|
||||
<NumericField
|
||||
label='Seed'
|
||||
min={CONFIG_DEFAULTS.seed.min}
|
||||
max={CONFIG_DEFAULTS.seed.max}
|
||||
step={CONFIG_DEFAULTS.seed.step}
|
||||
min={config.seed.min}
|
||||
max={config.seed.max}
|
||||
step={config.seed.step}
|
||||
value={params.seed}
|
||||
onChange={(seed) => {
|
||||
if (doesExist(props.onChange)) {
|
||||
|
@ -63,17 +65,21 @@ export function ImageControl(props: ImageControlProps) {
|
|||
}
|
||||
}}
|
||||
/>
|
||||
<IconButton onClick={() => {
|
||||
const seed = Math.floor(Math.random() * CONFIG_DEFAULTS.seed.max);
|
||||
<Button
|
||||
variant='outlined'
|
||||
startIcon={<Casino />}
|
||||
onClick={() => {
|
||||
const seed = Math.floor(Math.random() * config.seed.max);
|
||||
if (doesExist(props.onChange)) {
|
||||
props.onChange({
|
||||
...params,
|
||||
seed,
|
||||
});
|
||||
}
|
||||
}}>
|
||||
<Casino />
|
||||
</IconButton>
|
||||
}}
|
||||
>
|
||||
New Seed
|
||||
</Button>
|
||||
</Stack>
|
||||
<TextField label='Prompt' variant='outlined' value={params.prompt} onChange={(event) => {
|
||||
if (doesExist(props.onChange)) {
|
||||
|
|
|
@ -4,7 +4,7 @@ import * as React from 'react';
|
|||
import { useMutation, useQuery } from 'react-query';
|
||||
|
||||
import { ApiClient, BaseImgParams } from '../api/client.js';
|
||||
import { Config, CONFIG_DEFAULTS, IMAGE_FILTER, STALE_TIME } from '../config.js';
|
||||
import { ConfigParams, IMAGE_FILTER, STALE_TIME } from '../config.js';
|
||||
import { SCHEDULER_LABELS } from '../strings.js';
|
||||
import { ImageInput } from './ImageInput.js';
|
||||
import { ImageCard } from './ImageCard.js';
|
||||
|
@ -17,7 +17,7 @@ const { useState } = React;
|
|||
|
||||
export interface Img2ImgProps {
|
||||
client: ApiClient;
|
||||
config: Config;
|
||||
config: ConfigParams;
|
||||
|
||||
model: string;
|
||||
platform: string;
|
||||
|
@ -43,14 +43,14 @@ export function Img2Img(props: Img2ImgProps) {
|
|||
});
|
||||
|
||||
const [source, setSource] = useState<File>();
|
||||
const [strength, setStrength] = useState(CONFIG_DEFAULTS.strength.default);
|
||||
const [strength, setStrength] = useState(config.strength.default);
|
||||
const [params, setParams] = useState<BaseImgParams>({
|
||||
cfg: CONFIG_DEFAULTS.cfg.default,
|
||||
seed: CONFIG_DEFAULTS.seed.default,
|
||||
steps: CONFIG_DEFAULTS.steps.default,
|
||||
prompt: config.default.prompt,
|
||||
cfg: config.cfg.default,
|
||||
seed: config.seed.default,
|
||||
steps: config.steps.default,
|
||||
prompt: config.prompt.default,
|
||||
});
|
||||
const [scheduler, setScheduler] = useState(config.default.scheduler);
|
||||
const [scheduler, setScheduler] = useState(config.scheduler.default);
|
||||
|
||||
return <Box>
|
||||
<Stack spacing={2}>
|
||||
|
@ -67,15 +67,15 @@ export function Img2Img(props: Img2ImgProps) {
|
|||
/>
|
||||
</Stack>
|
||||
<ImageInput filter={IMAGE_FILTER} label='Source' onChange={setSource} />
|
||||
<ImageControl params={params} onChange={(newParams) => {
|
||||
<ImageControl config={config} params={params} onChange={(newParams) => {
|
||||
setParams(newParams);
|
||||
}} />
|
||||
<NumericField
|
||||
decimal
|
||||
label='Strength'
|
||||
min={CONFIG_DEFAULTS.strength.min}
|
||||
max={CONFIG_DEFAULTS.strength.max}
|
||||
step={CONFIG_DEFAULTS.strength.step}
|
||||
min={config.strength.min}
|
||||
max={config.strength.max}
|
||||
step={config.strength.step}
|
||||
value={strength}
|
||||
onChange={(value) => {
|
||||
setStrength(value);
|
||||
|
|
|
@ -5,7 +5,7 @@ import * as React from 'react';
|
|||
import { useMutation, useQuery } from 'react-query';
|
||||
|
||||
import { ApiClient, ApiResponse, BaseImgParams, equalResponse } from '../api/client.js';
|
||||
import { Config, CONFIG_DEFAULTS, DEFAULT_BRUSH, IMAGE_FILTER, STALE_TIME } from '../config.js';
|
||||
import { Config, ConfigParams, DEFAULT_BRUSH, IMAGE_FILTER, STALE_TIME } from '../config.js';
|
||||
import { SCHEDULER_LABELS } from '../strings.js';
|
||||
import { ImageInput } from './ImageInput.js';
|
||||
import { ImageCard } from './ImageCard.js';
|
||||
|
@ -57,7 +57,7 @@ export interface Point {
|
|||
|
||||
export interface InpaintProps {
|
||||
client: ApiClient;
|
||||
config: Config;
|
||||
config: ConfigParams;
|
||||
|
||||
model: string;
|
||||
platform: string;
|
||||
|
@ -166,12 +166,12 @@ export function Inpaint(props: InpaintProps) {
|
|||
const [mask, setMask] = useState<File>();
|
||||
const [source, setSource] = useState<File>();
|
||||
const [params, setParams] = useState<BaseImgParams>({
|
||||
cfg: CONFIG_DEFAULTS.cfg.default,
|
||||
seed: CONFIG_DEFAULTS.seed.default,
|
||||
steps: CONFIG_DEFAULTS.steps.default,
|
||||
prompt: config.default.prompt,
|
||||
cfg: config.cfg.default,
|
||||
seed: config.seed.default,
|
||||
steps: config.steps.default,
|
||||
prompt: config.prompt.default,
|
||||
});
|
||||
const [scheduler, setScheduler] = useState(config.default.scheduler);
|
||||
const [scheduler, setScheduler] = useState(config.scheduler.default);
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = mustExist(canvasRef.current);
|
||||
|
@ -190,11 +190,11 @@ export function Inpaint(props: InpaintProps) {
|
|||
function renderCanvas() {
|
||||
return <canvas
|
||||
ref={canvasRef}
|
||||
height={CONFIG_DEFAULTS.height.default}
|
||||
width={CONFIG_DEFAULTS.width.default}
|
||||
height={config.height.default}
|
||||
width={config.width.default}
|
||||
style={{
|
||||
maxHeight: CONFIG_DEFAULTS.height.default,
|
||||
maxWidth: CONFIG_DEFAULTS.width.default,
|
||||
maxHeight: config.height.default,
|
||||
maxWidth: config.width.default,
|
||||
}}
|
||||
onClick={(event) => {
|
||||
const canvas = mustExist(canvasRef.current);
|
||||
|
@ -271,22 +271,25 @@ export function Inpaint(props: InpaintProps) {
|
|||
}}
|
||||
/>
|
||||
<Button
|
||||
variant='outlined'
|
||||
startIcon={<FormatColorFill />}
|
||||
onClick={() => floodMask(floodBelow)}>
|
||||
Gray to black
|
||||
</Button>
|
||||
<Button
|
||||
variant='outlined'
|
||||
startIcon={<Gradient />}
|
||||
onClick={() => grayscaleMask()}>
|
||||
Grayscale
|
||||
</Button>
|
||||
<Button
|
||||
variant='outlined'
|
||||
startIcon={<FormatColorFill />}
|
||||
onClick={() => floodMask(floodAbove)}>
|
||||
Gray to white
|
||||
</Button>
|
||||
</Stack>
|
||||
<ImageControl params={params} onChange={(newParams) => {
|
||||
<ImageControl config={config} params={params} onChange={(newParams) => {
|
||||
setParams(newParams);
|
||||
}} />
|
||||
<Button onClick={() => upload.mutate()}>Generate</Button>
|
||||
|
|
|
@ -4,7 +4,7 @@ import * as React from 'react';
|
|||
import { useQuery } from 'react-query';
|
||||
|
||||
import { ApiClient } from '../api/client.js';
|
||||
import { Config, STALE_TIME } from '../config.js';
|
||||
import { Config, ConfigParams, STALE_TIME } from '../config.js';
|
||||
import { MODEL_LABELS, PLATFORM_LABELS } from '../strings.js';
|
||||
import { Img2Img } from './Img2Img.js';
|
||||
import { Inpaint } from './Inpaint.js';
|
||||
|
@ -15,15 +15,15 @@ const { useState } = React;
|
|||
|
||||
export interface OnnxWebProps {
|
||||
client: ApiClient;
|
||||
config: Config;
|
||||
config: ConfigParams;
|
||||
}
|
||||
|
||||
export function OnnxWeb(props: OnnxWebProps) {
|
||||
const { client, config } = props;
|
||||
|
||||
const [tab, setTab] = useState('txt2img');
|
||||
const [model, setModel] = useState(config.default.model);
|
||||
const [platform, setPlatform] = useState(config.default.platform);
|
||||
const [model, setModel] = useState(config.model.default);
|
||||
const [platform, setPlatform] = useState(config.platform.default);
|
||||
|
||||
const models = useQuery('models', async () => client.models(), {
|
||||
staleTime: STALE_TIME,
|
||||
|
|
|
@ -3,7 +3,7 @@ import * as React from 'react';
|
|||
import { useMutation, useQuery } from 'react-query';
|
||||
|
||||
import { ApiClient, BaseImgParams } from '../api/client.js';
|
||||
import { Config, CONFIG_DEFAULTS, STALE_TIME } from '../config.js';
|
||||
import { ConfigParams, STALE_TIME } from '../config.js';
|
||||
import { SCHEDULER_LABELS } from '../strings.js';
|
||||
import { ImageCard } from './ImageCard.js';
|
||||
import { ImageControl } from './ImageControl.js';
|
||||
|
@ -15,7 +15,7 @@ const { useState } = React;
|
|||
|
||||
export interface Txt2ImgProps {
|
||||
client: ApiClient;
|
||||
config: Config;
|
||||
config: ConfigParams;
|
||||
|
||||
model: string;
|
||||
platform: string;
|
||||
|
@ -40,15 +40,15 @@ export function Txt2Img(props: Txt2ImgProps) {
|
|||
staleTime: STALE_TIME,
|
||||
});
|
||||
|
||||
const [height, setHeight] = useState(CONFIG_DEFAULTS.height.default);
|
||||
const [width, setWidth] = useState(CONFIG_DEFAULTS.width.default);
|
||||
const [height, setHeight] = useState(config.height.default);
|
||||
const [width, setWidth] = useState(config.width.default);
|
||||
const [params, setParams] = useState<BaseImgParams>({
|
||||
cfg: CONFIG_DEFAULTS.cfg.default,
|
||||
seed: CONFIG_DEFAULTS.seed.default,
|
||||
steps: CONFIG_DEFAULTS.steps.default,
|
||||
prompt: config.default.prompt,
|
||||
cfg: config.cfg.default,
|
||||
seed: config.seed.default,
|
||||
steps: config.steps.default,
|
||||
prompt: config.prompt.default,
|
||||
});
|
||||
const [scheduler, setScheduler] = useState(config.default.scheduler);
|
||||
const [scheduler, setScheduler] = useState(config.scheduler.default);
|
||||
|
||||
return <Box>
|
||||
<Stack spacing={2}>
|
||||
|
@ -64,15 +64,15 @@ export function Txt2Img(props: Txt2ImgProps) {
|
|||
}}
|
||||
/>
|
||||
</Stack>
|
||||
<ImageControl params={params} onChange={(newParams) => {
|
||||
<ImageControl config={config} params={params} onChange={(newParams) => {
|
||||
setParams(newParams);
|
||||
}} />
|
||||
<Stack direction='row' spacing={4}>
|
||||
<NumericField
|
||||
label='Width'
|
||||
min={CONFIG_DEFAULTS.width.min}
|
||||
max={CONFIG_DEFAULTS.width.max}
|
||||
step={CONFIG_DEFAULTS.width.step}
|
||||
min={config.width.min}
|
||||
max={config.width.max}
|
||||
step={config.width.step}
|
||||
value={width}
|
||||
onChange={(value) => {
|
||||
setWidth(value);
|
||||
|
@ -80,9 +80,9 @@ export function Txt2Img(props: Txt2ImgProps) {
|
|||
/>
|
||||
<NumericField
|
||||
label='Height'
|
||||
min={CONFIG_DEFAULTS.height.min}
|
||||
max={CONFIG_DEFAULTS.height.max}
|
||||
step={CONFIG_DEFAULTS.height.step}
|
||||
min={config.height.min}
|
||||
max={config.height.max}
|
||||
step={config.height.step}
|
||||
value={height}
|
||||
onChange={(value) => {
|
||||
setHeight(value);
|
||||
|
|
|
@ -1,17 +1,45 @@
|
|||
import { Img2ImgParams, STATUS_SUCCESS, Txt2ImgParams } from './api/client.js';
|
||||
|
||||
export interface ConfigNumber {
|
||||
default: number;
|
||||
min: number;
|
||||
max: number;
|
||||
step: number;
|
||||
}
|
||||
|
||||
export interface ConfigString {
|
||||
default: string;
|
||||
keys: Array<string>;
|
||||
}
|
||||
|
||||
export type KeyFilter<T extends object> = {
|
||||
[K in keyof T]: T[K] extends number ? K : T[K] extends string ? K : never;
|
||||
}[keyof T];
|
||||
|
||||
export type ConfigRanges<T extends object> = {
|
||||
[K in KeyFilter<T>]: T[K] extends number ? ConfigNumber : T[K] extends string ? ConfigString : never;
|
||||
};
|
||||
|
||||
export type ConfigParams = ConfigRanges<Required<Img2ImgParams & Txt2ImgParams>>;
|
||||
|
||||
export interface Config {
|
||||
api: {
|
||||
root: string;
|
||||
};
|
||||
default: {
|
||||
model: string;
|
||||
platform: string;
|
||||
scheduler: string;
|
||||
prompt: string;
|
||||
params: {
|
||||
model: ConfigString;
|
||||
platform: ConfigString;
|
||||
scheduler: ConfigString;
|
||||
prompt: ConfigString;
|
||||
};
|
||||
}
|
||||
|
||||
export const DEFAULT_BRUSH = 8;
|
||||
export const IMAGE_FILTER = '.bmp, .jpg, .jpeg, .png';
|
||||
export const IMAGE_STEP = 8;
|
||||
export const IMAGE_MAX = 512;
|
||||
export const STALE_TIME = 3_000;
|
||||
|
||||
export async function loadConfig(): Promise<Config> {
|
||||
const configPath = new URL('./config.json', window.origin);
|
||||
const configReq = await fetch(configPath);
|
||||
|
@ -21,70 +49,3 @@ export async function loadConfig(): Promise<Config> {
|
|||
throw new Error('could not load config');
|
||||
}
|
||||
}
|
||||
|
||||
export interface ConfigRange {
|
||||
default: number;
|
||||
min: number;
|
||||
max: number;
|
||||
step: number;
|
||||
}
|
||||
|
||||
export type KeyFilter<T extends object> = {
|
||||
[K in keyof T]: T[K] extends number ? K : T[K] extends string ? K : never;
|
||||
}[keyof T];
|
||||
|
||||
export type ConfigRanges<T extends object> = {
|
||||
[K in KeyFilter<T>]: T[K] extends number ? ConfigRange : T[K] extends string ? string : never;
|
||||
};
|
||||
|
||||
export const DEFAULT_BRUSH = 8;
|
||||
export const IMAGE_FILTER = '.bmp, .jpg, .jpeg, .png';
|
||||
export const IMAGE_STEP = 8;
|
||||
export const IMAGE_MAX = 512;
|
||||
|
||||
export const CONFIG_DEFAULTS: ConfigRanges<Required<Img2ImgParams & Txt2ImgParams>> = {
|
||||
cfg: {
|
||||
default: 6,
|
||||
min: 1,
|
||||
max: 30,
|
||||
step: 0.1,
|
||||
},
|
||||
height: {
|
||||
default: IMAGE_MAX,
|
||||
min: IMAGE_STEP,
|
||||
max: IMAGE_MAX,
|
||||
step: IMAGE_STEP,
|
||||
},
|
||||
model: '',
|
||||
negativePrompt: '',
|
||||
platform: '',
|
||||
prompt: 'an astronaut eating a hamburger',
|
||||
scheduler: '',
|
||||
steps: {
|
||||
default: 25,
|
||||
min: 1,
|
||||
max: 200,
|
||||
step: 1,
|
||||
},
|
||||
seed: {
|
||||
default: -1,
|
||||
min: -1,
|
||||
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
|
||||
max: (2 ** 32) - 1,
|
||||
step: 1,
|
||||
},
|
||||
strength: {
|
||||
default: 0.5,
|
||||
min: 0,
|
||||
max: 1,
|
||||
step: 0.01,
|
||||
},
|
||||
width: {
|
||||
default: IMAGE_MAX,
|
||||
min: IMAGE_STEP,
|
||||
max: IMAGE_MAX,
|
||||
step: IMAGE_STEP,
|
||||
},
|
||||
};
|
||||
|
||||
export const STALE_TIME = 3_000;
|
||||
|
|
|
@ -1,22 +1,25 @@
|
|||
/* eslint-disable no-console */
|
||||
import { mustExist } from '@apextoaster/js-utils';
|
||||
import { merge } from 'lodash';
|
||||
import * as React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
|
||||
import { makeClient } from './api/client.js';
|
||||
import { OnnxWeb } from './components/OnnxWeb.js';
|
||||
import { loadConfig } from './config.js';
|
||||
import { ConfigParams, loadConfig } from './config.js';
|
||||
|
||||
export async function main() {
|
||||
const config = await loadConfig();
|
||||
const client = makeClient(config.api.root);
|
||||
const params = await client.params();
|
||||
const merged = merge(params, config.params) as ConfigParams;
|
||||
const query = new QueryClient();
|
||||
|
||||
const appElement = mustExist(document.getElementById('app'));
|
||||
const app = ReactDOM.createRoot(appElement);
|
||||
app.render(<QueryClientProvider client={query}>
|
||||
<OnnxWeb client={client} config={config} />
|
||||
<OnnxWeb client={client} config={merged} />
|
||||
</QueryClientProvider>);
|
||||
}
|
||||
|
||||
|
|
|
@ -541,6 +541,11 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
|
||||
integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==
|
||||
|
||||
"@types/lodash@^4.14.191":
|
||||
version "4.14.191"
|
||||
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.191.tgz#09511e7f7cba275acd8b419ddac8da9a6a79e2fa"
|
||||
integrity sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==
|
||||
|
||||
"@types/mocha@^10.0.1":
|
||||
version "10.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-10.0.1.tgz#2f4f65bb08bc368ac39c96da7b2f09140b26851b"
|
||||
|
@ -2028,6 +2033,11 @@ lodash.merge@^4.6.2:
|
|||
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
|
||||
integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
|
||||
|
||||
lodash@^4.17.21:
|
||||
version "4.17.21"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
||||
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
||||
|
||||
log-symbols@4.1.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503"
|
||||
|
|
Loading…
Reference in New Issue