Add squiggle notebooks
This commit is contained in:
parent
fd6c5d3eb8
commit
f122b5fd7f
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@quri/squiggle-components",
|
||||
"version": "0.1.5",
|
||||
"version": "0.1.6",
|
||||
"dependencies": {
|
||||
"@quri/squiggle-lang": "0.2.2",
|
||||
"@testing-library/jest-dom": "^5.16.2",
|
||||
|
@ -72,7 +72,8 @@
|
|||
"react-codejar": "^1.1.2",
|
||||
"ts-loader": "^9.2.8",
|
||||
"webpack": "^5.70.0",
|
||||
"webpack-cli": "^4.9.2"
|
||||
"webpack-cli": "^4.9.2",
|
||||
"webpack-dev-server": "^4.7.4"
|
||||
},
|
||||
"resolutions": {
|
||||
"@types/react": "17.0.39"
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import * as React from 'react';
|
||||
import * as _ from 'lodash';
|
||||
import _ from 'lodash';
|
||||
import type { Spec } from 'vega';
|
||||
import { run } from '@squiggle/lang';
|
||||
import type { DistPlus, SamplingInputs } from '@squiggle/lang';
|
||||
import { run } from '@quri/squiggle-lang';
|
||||
import type { DistPlus, SamplingInputs, exportEnv, exportDistribution } from '@quri/squiggle-lang';
|
||||
import { createClassFromSpec } from 'react-vega';
|
||||
import * as chartSpecification from './spec-distributions.json'
|
||||
import * as percentilesSpec from './spec-pertentiles.json'
|
||||
|
@ -26,7 +26,11 @@ export interface SquiggleChartProps {
|
|||
/** If the result is a function, where the function ends */
|
||||
diagramStop? : number,
|
||||
/** If the result is a function, how many points along the function it samples */
|
||||
diagramCount? : number
|
||||
diagramCount? : number,
|
||||
/** variables declared before this expression */
|
||||
environment? : exportEnv,
|
||||
/** When the environment changes */
|
||||
onEnvChange?(env: exportEnv): void
|
||||
}
|
||||
|
||||
export const SquiggleChart : React.FC<SquiggleChartProps> = props => {
|
||||
|
@ -38,11 +42,13 @@ export const SquiggleChart : React.FC<SquiggleChartProps> = props => {
|
|||
}
|
||||
|
||||
|
||||
let result = run(props.squiggleString, samplingInputs);
|
||||
console.log(result)
|
||||
let result = run(props.squiggleString, samplingInputs, props.environment);
|
||||
if (result.tag === "Ok") {
|
||||
let chartResults = result.value.map(chartResult => {
|
||||
console.log(chartResult)
|
||||
let environment = result.value.environment
|
||||
let exports = result.value.exports
|
||||
if(props.onEnvChange)
|
||||
props.onEnvChange(environment)
|
||||
let chartResults = exports.map((chartResult:exportDistribution )=> {
|
||||
if(chartResult["NAME"] === "Float"){
|
||||
return <MakeNumberShower precision={3} number={chartResult["VAL"]} />;
|
||||
}
|
||||
|
|
103
packages/components/src/SquiggleEditor.tsx
Normal file
103
packages/components/src/SquiggleEditor.tsx
Normal file
|
@ -0,0 +1,103 @@
|
|||
import * as React from 'react';
|
||||
import * as ReactDOM from 'react-dom';
|
||||
import { SquiggleChart } from './SquiggleChart'
|
||||
import { ReactCodeJar } from "react-codejar";
|
||||
import type { exportEnv } from '@quri/squiggle-lang'
|
||||
|
||||
|
||||
export interface SquiggleEditorProps {
|
||||
/** The input string for squiggle */
|
||||
initialSquiggleString? : string,
|
||||
|
||||
/** If the output requires monte carlo sampling, the amount of samples */
|
||||
sampleCount? : number,
|
||||
/** The amount of points returned to draw the distribution */
|
||||
outputXYPoints? : number,
|
||||
kernelWidth? : number,
|
||||
pointDistLength? : number,
|
||||
/** If the result is a function, where the function starts */
|
||||
diagramStart? : number,
|
||||
/** If the result is a function, where the function ends */
|
||||
diagramStop? : number,
|
||||
/** If the result is a function, how many points along the function it samples */
|
||||
diagramCount? : number,
|
||||
/** The environment, other variables that were already declared */
|
||||
environment?: exportEnv,
|
||||
/** when the environment changes. Used again for notebook magic*/
|
||||
onEnvChange?(env: exportEnv) : void
|
||||
}
|
||||
|
||||
const highlight = (editor: HTMLInputElement) => {
|
||||
let code = editor.textContent;
|
||||
code = code.replace(/\((\w+?)(\b)/g, '(<font color="#8a2be2">$1</font>$2');
|
||||
editor.innerHTML = code;
|
||||
};
|
||||
|
||||
interface SquiggleEditorState {
|
||||
expression: string,
|
||||
env: exportEnv
|
||||
}
|
||||
|
||||
export class SquiggleEditor extends React.Component<SquiggleEditorProps, SquiggleEditorState>{
|
||||
constructor(props: SquiggleEditorProps) {
|
||||
super(props)
|
||||
let code = props.initialSquiggleString ? props.initialSquiggleString : ""
|
||||
this.state = {expression: code, env: props.environment }
|
||||
|
||||
}
|
||||
render() {
|
||||
let {expression, env} = this.state
|
||||
let props = this.props
|
||||
return (
|
||||
<div>
|
||||
<ReactCodeJar
|
||||
code={expression}
|
||||
onUpdate={e => {
|
||||
this.setState({expression: e})
|
||||
}}
|
||||
style={{
|
||||
borderRadius: "6px",
|
||||
width: "530px",
|
||||
border: "1px solid grey",
|
||||
fontFamily: "'Source Code Pro', monospace",
|
||||
fontSize: "14px",
|
||||
fontWeight: "400",
|
||||
letterSpacing: "normal",
|
||||
lineHeight: "20px",
|
||||
padding: "10px",
|
||||
tabSize: "4"
|
||||
}}
|
||||
highlight={highlight}
|
||||
lineNumbers={false}
|
||||
/>
|
||||
<SquiggleChart
|
||||
squiggleString={expression}
|
||||
sampleCount={props.sampleCount}
|
||||
outputXYPoints={props.outputXYPoints}
|
||||
kernelWidth={props.kernelWidth}
|
||||
pointDistLength={props.pointDistLength}
|
||||
diagramStart={props.diagramStart}
|
||||
diagramStop={props.diagramStop}
|
||||
diagramCount={props.diagramCount}
|
||||
environment={env}
|
||||
onEnvChange={props.onEnvChange}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export function renderSquiggleEditor(props : SquiggleEditorProps) {
|
||||
let parent = document.createElement("div")
|
||||
ReactDOM.render(<SquiggleEditor {...props} onEnvChange={env => {
|
||||
|
||||
// I can set the value here because I need it for creating notebooks
|
||||
// @ts-ignore
|
||||
parent.value = env
|
||||
|
||||
parent.dispatchEvent(new CustomEvent("input"))
|
||||
if(props.onEnvChange)
|
||||
props.onEnvChange(env)
|
||||
}} /> , parent)
|
||||
return parent
|
||||
}
|
|
@ -1 +1,2 @@
|
|||
export { SquiggleChart } from './SquiggleChart';
|
||||
export { SquiggleEditor, renderSquiggleEditor } from './SquiggleEditor';
|
||||
|
|
|
@ -2,6 +2,7 @@ const path = require('path');
|
|||
|
||||
module.exports = {
|
||||
mode: 'production',
|
||||
devtool: 'source-map',
|
||||
entry: './src/index.ts',
|
||||
module: {
|
||||
rules: [
|
||||
|
@ -23,4 +24,11 @@ module.exports = {
|
|||
type: 'umd',
|
||||
},
|
||||
},
|
||||
devServer: {
|
||||
static: {
|
||||
directory: path.join(__dirname, 'public'),
|
||||
},
|
||||
compress: true,
|
||||
port: 9000,
|
||||
},
|
||||
};
|
||||
|
|
1
packages/squiggle-lang/.gitignore
vendored
1
packages/squiggle-lang/.gitignore
vendored
|
@ -14,5 +14,6 @@ yarn-error.log
|
|||
.netlify
|
||||
.idea
|
||||
*.gen.ts
|
||||
*.gen.tsx
|
||||
*.gen.js
|
||||
dist
|
||||
|
|
|
@ -1,19 +1,29 @@
|
|||
import { run } from '../src/js/index';
|
||||
|
||||
let testRun = (x: string) => {
|
||||
let result = run(x)
|
||||
if(result.tag == 'Ok'){
|
||||
return { tag: 'Ok', value: result.value.exports }
|
||||
}
|
||||
else {
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
describe("A simple result", () => {
|
||||
test("mean(normal(5,2))", () => {
|
||||
expect(run("mean(normal(5,2))")).toEqual({ tag: 'Ok', value: [ { NAME: 'Float', VAL: 5 } ] })
|
||||
expect(testRun("mean(normal(5,2))")).toEqual({ tag: 'Ok', value: [ { NAME: 'Float', VAL: 5 } ] })
|
||||
})
|
||||
test("10+10", () => {
|
||||
let foo = run("10 + 10")
|
||||
let foo = testRun("10 + 10")
|
||||
expect(foo).toEqual({ tag: 'Ok', value: [ { NAME: 'Float', VAL: 20 } ] })
|
||||
})
|
||||
test("log(1) = 0", () => {
|
||||
let foo = run("log(1)")
|
||||
let foo = testRun("log(1)")
|
||||
expect(foo).toEqual({ tag: 'Ok', value: [ { NAME: 'Float', VAL: 0} ]})
|
||||
})
|
||||
test("mm(0,0,[0,0,0])", () => {
|
||||
let foo = run("mm(0,0,[0,0,0])")
|
||||
let foo = testRun("mm(0,0,[0,0,0])")
|
||||
expect(foo).toEqual({ "tag": "Error", "value": "Function multimodal error: Too many weights provided" })
|
||||
})
|
||||
});
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import {runAll} from '../rescript/ProgramEvaluator.gen';
|
||||
import type { Inputs_SamplingInputs_t as SamplingInputs } from '../rescript/ProgramEvaluator.gen';
|
||||
export type { SamplingInputs }
|
||||
import type { Inputs_SamplingInputs_t as SamplingInputs,exportEnv, exportType, exportDistribution} from '../rescript/ProgramEvaluator.gen';
|
||||
export type { SamplingInputs , exportEnv, exportDistribution }
|
||||
export type {t as DistPlus} from '../rescript/pointSetDist/DistPlus.gen';
|
||||
|
||||
export let defaultSamplingInputs : SamplingInputs = {
|
||||
|
@ -9,7 +9,9 @@ export let defaultSamplingInputs : SamplingInputs = {
|
|||
pointDistLength : 1000
|
||||
}
|
||||
|
||||
export function run(squiggleString : string, samplingInputs? : SamplingInputs) {
|
||||
export function run(squiggleString : string, samplingInputs? : SamplingInputs, environment?: exportEnv) : { tag: "Ok"; value: exportType }
|
||||
| { tag: "Error"; value: string } {
|
||||
let si : SamplingInputs = samplingInputs ? samplingInputs : defaultSamplingInputs
|
||||
return runAll(squiggleString, si)
|
||||
let env : exportEnv = environment ? environment : []
|
||||
return runAll(squiggleString, si, env)
|
||||
}
|
||||
|
|
|
@ -36,12 +36,20 @@ module Inputs = {
|
|||
}
|
||||
}
|
||||
|
||||
type exportType = [
|
||||
type exportDistribution = [
|
||||
| #DistPlus(DistPlus.t)
|
||||
| #Float(float)
|
||||
| #Function((float) => Belt.Result.t<DistPlus.t,string>)
|
||||
]
|
||||
|
||||
type exportEnv = array<(string, ASTTypes.node)>
|
||||
|
||||
type exportType = {
|
||||
environment : exportEnv,
|
||||
exports: array<exportDistribution>
|
||||
}
|
||||
|
||||
|
||||
module Internals = {
|
||||
let addVariable = (
|
||||
{samplingInputs, squiggleString, environment}: Inputs.inputs,
|
||||
|
@ -71,135 +79,125 @@ module Internals = {
|
|||
let runNode = (inputs, node) =>
|
||||
AST.toLeaf(makeInputs(inputs), inputs.environment, node)
|
||||
|
||||
let renderIfNeeded = (inputs: Inputs.inputs, node: ASTTypes.node): result<
|
||||
ASTTypes.node,
|
||||
string,
|
||||
> =>
|
||||
node |> (
|
||||
x =>
|
||||
switch x {
|
||||
| #Normalize(_) as n
|
||||
| #SymbolicDist(_) as n =>
|
||||
#Render(n)
|
||||
|> runNode(inputs)
|
||||
|> (
|
||||
x =>
|
||||
switch x {
|
||||
| Ok(#RenderedDist(_)) as r => r
|
||||
| Error(r) => Error(r)
|
||||
| _ => Error("Didn't render, but intended to")
|
||||
}
|
||||
)
|
||||
|
||||
| n => Ok(n)
|
||||
}
|
||||
)
|
||||
|
||||
let outputToDistPlus = (inputs: Inputs.inputs, pointSetDist: PointSetTypes.pointSetDist) =>
|
||||
DistPlus.make(~pointSetDist, ~squiggleString=Some(inputs.squiggleString), ())
|
||||
|
||||
let rec returnDist = (functionInfo : (array<string>, ASTTypes.node),
|
||||
inputs : Inputs.inputs,
|
||||
env : ASTTypes.environment) => {
|
||||
(input : float) => {
|
||||
let foo: Inputs.inputs = {...inputs, environment: env};
|
||||
evaluateFunction(
|
||||
foo,
|
||||
functionInfo,
|
||||
[#SymbolicDist(#Float(input))],
|
||||
) |> E.R.bind(_, a =>
|
||||
switch a {
|
||||
| #DistPlus(d) => Ok(DistPlus.T.normalize(d))
|
||||
| n =>
|
||||
Js.log2("Error here", n)
|
||||
Error("wrong type")
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
// TODO: Consider using ExpressionTypes.ExpressionTree.getFloat or similar in this function
|
||||
and coersionToExportedTypes = (
|
||||
inputs,
|
||||
env: ASTTypes.environment,
|
||||
ex: ASTTypes.node,
|
||||
): result<exportDistribution, string> =>
|
||||
ex
|
||||
|> renderIfNeeded(inputs)
|
||||
|> E.R.bind(_, x =>
|
||||
switch x {
|
||||
| #RenderedDist(Discrete({xyShape: {xs: [x], ys: [1.0]}})) => Ok(#Float(x))
|
||||
| #SymbolicDist(#Float(x)) => Ok(#Float(x))
|
||||
| #RenderedDist(n) => Ok(#DistPlus(outputToDistPlus(inputs, n)))
|
||||
| #Function(n) => Ok(#Function(returnDist(n, inputs, env)))
|
||||
| n => Error("Didn't output a rendered distribution. Format:" ++ AST.toString(n))
|
||||
}
|
||||
)
|
||||
|
||||
and evaluateFunction = (
|
||||
inputs: Inputs.inputs,
|
||||
fn: (array<string>, ASTTypes.node),
|
||||
fnInputs,
|
||||
) => {
|
||||
let output = AST.runFunction(
|
||||
makeInputs(inputs),
|
||||
inputs.environment,
|
||||
fnInputs,
|
||||
fn,
|
||||
)
|
||||
output |> E.R.bind(_, coersionToExportedTypes(inputs, inputs.environment))
|
||||
}
|
||||
|
||||
let runProgram = (inputs: Inputs.inputs, p: ASTTypes.program) => {
|
||||
let ins = ref(inputs)
|
||||
p
|
||||
|> E.A.fmap(x =>
|
||||
switch x {
|
||||
| #Assignment(name, node) =>
|
||||
ins := addVariable(ins.contents, name, node)
|
||||
None
|
||||
| #Expression(node) =>
|
||||
Some(runNode(ins.contents, node) |> E.R.fmap(r => (ins.contents.environment, r)))
|
||||
}
|
||||
)
|
||||
|> E.A.O.concatSomes
|
||||
|> E.A.R.firstErrorOrOpen
|
||||
|> E.A.fmap(x =>
|
||||
switch x {
|
||||
| #Assignment(name, node) =>
|
||||
ins := addVariable(ins.contents, name, node)
|
||||
None
|
||||
| #Expression(node) =>
|
||||
Some(runNode(ins.contents, node))
|
||||
}
|
||||
)
|
||||
|> E.A.O.concatSomes
|
||||
|> E.A.R.firstErrorOrOpen
|
||||
|> E.R.bind(_, d =>
|
||||
d
|
||||
|> E.A.fmap(x => coersionToExportedTypes(inputs, ins.contents.environment, x))
|
||||
|> E.A.R.firstErrorOrOpen
|
||||
)
|
||||
|> E.R.fmap(ex =>
|
||||
{
|
||||
environment: Belt.Map.String.toArray(ins.contents.environment),
|
||||
exports: ex
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
let inputsToLeaf = (inputs: Inputs.inputs) =>
|
||||
Parser.fromString(inputs.squiggleString) |> E.R.bind(_, g => runProgram(inputs, g))
|
||||
|
||||
let outputToDistPlus = (inputs: Inputs.inputs, pointSetDist: PointSetTypes.pointSetDist) =>
|
||||
DistPlus.make(~pointSetDist, ~squiggleString=Some(inputs.squiggleString), ())
|
||||
}
|
||||
|
||||
let renderIfNeeded = (inputs: Inputs.inputs, node: ASTTypes.node): result<
|
||||
ASTTypes.node,
|
||||
string,
|
||||
> =>
|
||||
node |> (
|
||||
x =>
|
||||
switch x {
|
||||
| #Normalize(_) as n
|
||||
| #SymbolicDist(_) as n =>
|
||||
#Render(n)
|
||||
|> Internals.runNode(inputs)
|
||||
|> (
|
||||
x =>
|
||||
switch x {
|
||||
| Ok(#RenderedDist(_)) as r => r
|
||||
| Error(r) => Error(r)
|
||||
| _ => Error("Didn't render, but intended to")
|
||||
}
|
||||
)
|
||||
|
||||
| n => Ok(n)
|
||||
}
|
||||
)
|
||||
|
||||
let rec returnDist = (functionInfo : (array<string>, ASTTypes.node),
|
||||
inputs : Inputs.inputs,
|
||||
env : ASTTypes.environment) => {
|
||||
(input : float) => {
|
||||
let foo: Inputs.inputs = {...inputs, environment: env};
|
||||
evaluateFunction(
|
||||
foo,
|
||||
functionInfo,
|
||||
[#SymbolicDist(#Float(input))],
|
||||
) |> E.R.bind(_, a =>
|
||||
switch a {
|
||||
| #DistPlus(d) => Ok(DistPlus.T.normalize(d))
|
||||
| n =>
|
||||
Js.log2("Error here", n)
|
||||
Error("wrong type")
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
// TODO: Consider using ExpressionTypes.ExpressionTree.getFloat or similar in this function
|
||||
and coersionToExportedTypes = (
|
||||
inputs,
|
||||
env: ASTTypes.environment,
|
||||
node: ASTTypes.node,
|
||||
): result<exportType, string> =>
|
||||
node
|
||||
|> renderIfNeeded(inputs)
|
||||
|> E.R.bind(_, x =>
|
||||
switch x {
|
||||
| #RenderedDist(Discrete({xyShape: {xs: [x], ys: [1.0]}})) => Ok(#Float(x))
|
||||
| #SymbolicDist(#Float(x)) => Ok(#Float(x))
|
||||
| #RenderedDist(n) => Ok(#DistPlus(Internals.outputToDistPlus(inputs, n)))
|
||||
| #Function(n) => Ok(#Function(returnDist(n, inputs, env)))
|
||||
| n => Error("Didn't output a rendered distribution. Format:" ++ AST.toString(n))
|
||||
}
|
||||
)
|
||||
|
||||
and evaluateFunction = (
|
||||
inputs: Inputs.inputs,
|
||||
fn: (array<string>, ASTTypes.node),
|
||||
fnInputs,
|
||||
) => {
|
||||
let output = AST.runFunction(
|
||||
Internals.makeInputs(inputs),
|
||||
inputs.environment,
|
||||
fnInputs,
|
||||
fn,
|
||||
)
|
||||
output |> E.R.bind(_, coersionToExportedTypes(inputs, inputs.environment))
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
let rec mapM = (f, xs) =>
|
||||
switch xs {
|
||||
| [] => Ok([])
|
||||
| arr =>
|
||||
switch f(arr[0]) {
|
||||
| Error(err) => Error(err)
|
||||
| Ok(val) =>
|
||||
switch mapM(f, Belt.Array.sliceToEnd(arr, 1)) {
|
||||
| Error(err) => Error(err)
|
||||
| Ok(restList) => Ok(Belt.Array.concat([val], restList))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let evaluateProgram = (inputs: Inputs.inputs) =>
|
||||
inputs
|
||||
|> Internals.inputsToLeaf
|
||||
|> E.R.bind(_, xs => mapM(((a, b)) => coersionToExportedTypes(inputs, a, b), xs))
|
||||
|
||||
|
||||
@genType
|
||||
let runAll = (squiggleString: string, samplingInputs: Inputs.SamplingInputs.t) => {
|
||||
let runAll : (string, Inputs.SamplingInputs.t, exportEnv) => result<exportType,string> =
|
||||
(squiggleString, samplingInputs, environment) => {
|
||||
let inputs = Inputs.make(
|
||||
~samplingInputs,
|
||||
~squiggleString,
|
||||
~environment=[]->Belt.Map.String.fromArray,
|
||||
~environment=Belt.Map.String.fromArray(environment),
|
||||
(),
|
||||
)
|
||||
let response1 = evaluateProgram(inputs);
|
||||
response1
|
||||
Internals.inputsToLeaf(inputs)
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
@genType
|
||||
type rec hash = array<(string, node)>
|
||||
and node = [
|
||||
| #SymbolicDist(SymbolicDistTypes.symbolicDist)
|
||||
|
@ -229,4 +230,4 @@ module SamplingDistribution = {
|
|||
pointSetDist |> E.R.fmap(r => #Normalize(#RenderedDist(r)))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,6 +31,7 @@ type triangular = {
|
|||
high: float,
|
||||
}
|
||||
|
||||
@genType
|
||||
type symbolicDist = [
|
||||
| #Normal(normal)
|
||||
| #Beta(beta)
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
// This file has no dependencies. It's used outside of the interpreter, but the interpreter depends on it.
|
||||
|
||||
@genType
|
||||
type algebraicOperation = [
|
||||
| #Add
|
||||
| #Multiply
|
||||
|
@ -7,6 +8,7 @@ type algebraicOperation = [
|
|||
| #Divide
|
||||
| #Exponentiate
|
||||
]
|
||||
@genType
|
||||
type pointwiseOperation = [#Add | #Multiply | #Exponentiate]
|
||||
type scaleOperation = [#Multiply | #Exponentiate | #Log]
|
||||
type distToFloatOperation = [
|
||||
|
@ -109,4 +111,4 @@ module Truncate = {
|
|||
let right = right |> E.O.dimap(Js.Float.toString, () => "inf")
|
||||
j`truncate($nodeToString, $left, $right)`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"jsx": "react",
|
||||
"allowJs": true,
|
||||
"noImplicitAny": true,
|
||||
"removeComments": true,
|
||||
|
|
|
@ -18696,7 +18696,7 @@ webpack-dev-middleware@^5.3.1:
|
|||
range-parser "^1.2.1"
|
||||
schema-utils "^4.0.0"
|
||||
|
||||
webpack-dev-server@^4.6.0, webpack-dev-server@^4.7.1:
|
||||
webpack-dev-server@^4.6.0, webpack-dev-server@^4.7.1, webpack-dev-server@^4.7.4:
|
||||
version "4.7.4"
|
||||
resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-4.7.4.tgz#d0ef7da78224578384e795ac228d8efb63d5f945"
|
||||
integrity sha512-nfdsb02Zi2qzkNmgtZjkrMOcXnYZ6FLKcQwpxT7MvmHKc+oTtDsBju8j+NMyAygZ9GW1jMEUpy3itHtqgEhe1A==
|
||||
|
|
Loading…
Reference in New Issue
Block a user