├── .golangci.yaml
├── ui
├── webpack.config.js
├── package.json
├── src
│ ├── index.js
│ └── plain.js
├── html
│ ├── index.html.tmpl
│ └── job.html.tmpl
├── css
│ └── styles.css
└── package-lock.json
├── .pre-commit-config.yaml
├── Dockerfile
├── utils.go
├── .gitignore
├── example
├── goflow-example.go
├── docker-compose.yml
└── goflow-example-postgres.go
├── .github
├── workflows
│ ├── dependency-review.yml
│ ├── go.yml
│ ├── golangci-lint.yml
│ └── deploy-image.yml
└── dependabot.yml
├── utils_test.go
├── log.go
├── LICENSE
├── stream.go
├── dag_test.go
├── go.mod
├── task.go
├── operator.go
├── execution.go
├── operator_test.go
├── dag.go
├── example.go
├── goflow.go
├── job_test.go
├── job.go
├── routes.go
├── goflow_test.go
├── go.sum
├── swagger.json
└── Readme.md
/.golangci.yaml:
--------------------------------------------------------------------------------
1 | linters:
2 | disable:
3 | - errcheck
4 |
--------------------------------------------------------------------------------
/ui/webpack.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | mode: "production",
3 | output: {
4 | filename: "./dist.js",
5 | library: "goflowUI"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | repos:
2 | - repo: https://github.com/golangci/golangci-lint
3 | rev: v1.56.2
4 | hooks:
5 | - id: golangci-lint
6 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM centos
2 |
3 | ENV GIN_MODE=release
4 |
5 | WORKDIR /opt
6 | COPY goflow-example goflow-example
7 | COPY ui ui
8 |
9 | EXPOSE 8181
10 |
11 | CMD ["./goflow-example"]
12 |
--------------------------------------------------------------------------------
/utils.go:
--------------------------------------------------------------------------------
1 | package goflow
2 |
3 | func equal(a, b []string) bool {
4 | if len(a) != len(b) {
5 | return false
6 | }
7 | for i, v := range a {
8 | if v != b[i] {
9 | return false
10 | }
11 | }
12 | return true
13 | }
14 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Output of the go test coverage tool
2 | *.out
3 |
4 | # The goflow-example binary
5 | goflow-example
6 |
7 | # Front-end dependencies
8 | ui/node_modules
9 |
10 | # Front-end compiled assets
11 | ui/dist
12 |
13 | # The default goflow database
14 | goflow.db
15 |
16 | # The test database
17 | test.db
18 | .vscode
--------------------------------------------------------------------------------
/example/goflow-example.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "github.com/fieldryand/goflow/v2"
5 | )
6 |
7 | func main() {
8 | options := goflow.Options{
9 | UIPath: "ui/",
10 | ShowExamples: true,
11 | WithSeconds: true,
12 | }
13 | gf := goflow.New(options)
14 | gf.Use(goflow.DefaultLogger())
15 | gf.Run(":8181")
16 | }
17 |
--------------------------------------------------------------------------------
/.github/workflows/dependency-review.yml:
--------------------------------------------------------------------------------
1 | name: Dependency review
2 |
3 | on:
4 | pull_request:
5 | branches: [ main ]
6 |
7 | jobs:
8 |
9 | build:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v2
13 |
14 | - name: Dependency Review
15 | uses: actions/dependency-review-action@v3.0.6
16 |
17 |
18 |
--------------------------------------------------------------------------------
/example/docker-compose.yml:
--------------------------------------------------------------------------------
1 | # for testing goflow-example-postgres.go
2 | version: '3.1'
3 |
4 | services:
5 |
6 | db:
7 | image: postgres
8 | restart: always
9 | environment:
10 | POSTGRES_PASSWORD: example
11 | ports:
12 | - 5432:5432
13 |
14 | adminer:
15 | image: adminer
16 | restart: always
17 | ports:
18 | - 8080:8080
19 |
--------------------------------------------------------------------------------
/ui/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "goflow-ui",
3 | "version": "1.0.0",
4 | "description": "",
5 | "private": true,
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1",
8 | "build": "webpack"
9 | },
10 | "keywords": [],
11 | "author": "",
12 | "license": "ISC",
13 | "dependencies": {
14 | "d3-color": "^3.1.0",
15 | "dagre-d3": "^0.6.4",
16 | "webpack": "^5.76.0",
17 | "webpack-cli": "^4.6.0"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/.github/workflows/go.yml:
--------------------------------------------------------------------------------
1 | name: Go
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | pull_request:
7 | branches: [ main ]
8 |
9 | jobs:
10 |
11 | build:
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@v2
15 |
16 | - name: Set up Go
17 | uses: actions/setup-go@v4
18 | with:
19 | go-version: '1.20'
20 |
21 | - name: Test
22 | run: go test ./... -coverprofile=coverage.txt -race
23 |
24 | - name: Codecov
25 | uses: codecov/codecov-action@v3
26 |
--------------------------------------------------------------------------------
/utils_test.go:
--------------------------------------------------------------------------------
1 | package goflow
2 |
3 | import (
4 | "testing"
5 | )
6 |
7 | type test struct {
8 | a []string
9 | b []string
10 | out bool
11 | }
12 |
13 | var tests = []test{
14 | {[]string{"a"}, []string{"a"}, true},
15 | {[]string{"a"}, []string{"a", "b"}, false},
16 | {[]string{"a"}, []string{"b"}, false},
17 | }
18 |
19 | func TestEqual(t *testing.T) {
20 | for _, v := range tests {
21 | got := equal(v.a, v.b)
22 | if got != v.out {
23 | t.Errorf("Test failed: got %t, expected %t", got, v.out)
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # To get started with Dependabot version updates, you'll need to specify which
2 | # package ecosystems to update and where the package manifests are located.
3 | # Please see the documentation for all configuration options:
4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
5 |
6 | version: 2
7 | updates:
8 | - package-ecosystem: "gomod" # See documentation for possible values
9 | directory: "/" # Location of package manifests
10 | schedule:
11 | interval: "daily"
12 |
13 |
--------------------------------------------------------------------------------
/log.go:
--------------------------------------------------------------------------------
1 | package goflow
2 |
3 | import (
4 | "fmt"
5 | "time"
6 |
7 | "github.com/gin-gonic/gin"
8 | )
9 |
10 | // DefaultLogger returns the default logging middleware.
11 | func DefaultLogger() gin.HandlerFunc {
12 | return gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
13 | return fmt.Sprintf("%s [GOFLOW] - \"%s %s %s %d %s \"%s\" %s\"\n",
14 | param.TimeStamp.Format(time.RFC3339),
15 | param.Method,
16 | param.Path,
17 | param.Request.Proto,
18 | param.StatusCode,
19 | param.Latency,
20 | param.Request.UserAgent(),
21 | param.ErrorMessage,
22 | )
23 | })
24 | }
25 |
26 | type logWriter struct {
27 | }
28 |
29 | func (writer logWriter) Write(bytes []byte) (int, error) {
30 | return fmt.Print(time.Now().Format(time.RFC3339) + " [GOFLOW] - " + string(bytes))
31 | }
32 |
--------------------------------------------------------------------------------
/example/goflow-example-postgres.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | //import (
4 | // "github.com/fieldryand/goflow/v2"
5 | // "github.com/philippgille/gokv/encoding"
6 | // "github.com/philippgille/gokv/postgresql"
7 | //)
8 | //
9 | //func main() {
10 | // storeOptions := postgresql.Options{
11 | // ConnectionURL: "postgres://postgres:example@0.0.0.0:5432/postgres?sslmode=disable",
12 | // TableName: "Item",
13 | // MaxOpenConnections: 100,
14 | // Codec: encoding.JSON,
15 | // }
16 | //
17 | // client, err := postgresql.NewClient(storeOptions)
18 | // if err != nil {
19 | // panic(err)
20 | // }
21 | // defer client.Close()
22 | //
23 | // options := goflow.Options{
24 | // Store: client,
25 | // Streaming: true,
26 | // ShowExamples: true,
27 | // WithSeconds: true,
28 | // }
29 | // gf := goflow.New(options)
30 | // gf.Use(goflow.DefaultLogger())
31 | // gf.Run(":8181")
32 | //}
33 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Ryan Field
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/stream.go:
--------------------------------------------------------------------------------
1 | package goflow
2 |
3 | import (
4 | "io"
5 | "time"
6 |
7 | "github.com/gin-gonic/gin"
8 | )
9 |
10 | // Set keepOpen to false when testing--one event will be sent and
11 | // then the channel is closed by the server.
12 | func (g *Goflow) stream(keepOpen bool) func(*gin.Context) {
13 |
14 | return func(c *gin.Context) {
15 | job := c.Query("jobname")
16 |
17 | history := make([]*execution, 0)
18 |
19 | // periodically push the list of job runs into the stream
20 | c.Stream(func(w io.Writer) bool {
21 | for jobname := range g.Jobs {
22 | executions, _ := readExecutions(g.Store, jobname)
23 | for _, e := range executions {
24 |
25 | // make sure it wasn't already sent
26 | inHistory := false
27 |
28 | for _, h := range history {
29 | if e.ID == h.ID && e.ModifiedTimestamp == h.ModifiedTimestamp {
30 | inHistory = true
31 | }
32 | }
33 |
34 | if !inHistory {
35 | if (job != "" && job == e.JobName) || job == "" {
36 | c.SSEvent("message", e)
37 | history = append(history, e)
38 | }
39 | }
40 |
41 | }
42 | }
43 |
44 | time.Sleep(time.Second * 1)
45 |
46 | return keepOpen
47 | })
48 | }
49 |
50 | }
51 |
--------------------------------------------------------------------------------
/dag_test.go:
--------------------------------------------------------------------------------
1 | package goflow
2 |
3 | import (
4 | "testing"
5 | )
6 |
7 | func TestDag(t *testing.T) {
8 | d := make(dag)
9 |
10 | d.addNode("a")
11 | d.addNode("b")
12 | d.addNode("c")
13 | d.addNode("d")
14 | d.setDownstream("a", "b")
15 | d.setDownstream("a", "c")
16 | d.setDownstream("b", "d")
17 | d.setDownstream("c", "d")
18 |
19 | if !d.validate() {
20 | t.Errorf("Valid dag failed validation check")
21 | }
22 |
23 | if !equal(d.dependencies("b"), []string{"a"}) {
24 | t.Errorf("d.dependencies() returned %s, expected %s",
25 | d.dependencies("b"),
26 | []string{"a"})
27 | }
28 |
29 | if !equal(d.independentNodes(), []string{"a"}) {
30 | t.Errorf("d.independentNodes() returned %s, expected %s",
31 | d.dependencies("b"),
32 | []string{"a"})
33 | }
34 |
35 | e := make(dag)
36 |
37 | e.addNode("a")
38 | e.addNode("b")
39 | e.addNode("c")
40 | e.setDownstream("c", "a")
41 | e.setDownstream("a", "b")
42 | e.setDownstream("b", "a")
43 |
44 | if e.validate() {
45 | t.Errorf("Invalid dag passed validation check")
46 | }
47 | }
48 |
49 | func TestDagWithSingleNode(t *testing.T) {
50 | d := make(dag)
51 | d.addNode("a")
52 | res := d.isDownstream("a")
53 |
54 | if res {
55 | t.Errorf("isDownstream() returned true for an independent node")
56 | }
57 |
58 | }
59 |
--------------------------------------------------------------------------------
/ui/src/index.js:
--------------------------------------------------------------------------------
1 | var dagreD3 = require("dagre-d3");
2 | var d3 = require("d3");
3 |
4 | async function getDag(jobName) {
5 | const response = await fetch(`/api/jobs/${jobName}`);
6 | const json = await response.json();
7 | return json.dag
8 | }
9 |
10 | export async function graphViz(jobName) {
11 |
12 | const dag = await getDag(jobName);
13 |
14 | // Create a new directed graph
15 | var g = new dagreD3.graphlib.Graph().setGraph({});
16 |
17 | for (var key in dag) {
18 | g.setNode(key, { id: "node-" + key, label: key });
19 | for (var val in dag[key]) {
20 | g.setEdge(key, dag[key][val], {});
21 | }
22 | }
23 |
24 | var svg = d3.select("svg");
25 | var inner = svg.select("g");
26 |
27 | g.nodes().forEach(function(v) {
28 | var node = g.node(v);
29 | node.rx = node.ry = 5;
30 | });
31 |
32 | // Set up zoom support
33 | var zoom = d3.zoom().on("zoom", function() {
34 | inner.attr("transform", d3.event.transform);
35 | });
36 | svg.call(zoom);
37 |
38 | // Create the renderer
39 | var render = new dagreD3.render();
40 |
41 | // Run the renderer. This is what draws the final graph.
42 | render(inner, g);
43 |
44 | // Center the graph
45 | var initialScale = 1.00;
46 | svg.call(zoom.transform, d3.zoomIdentity.translate(20, 20).scale(initialScale));
47 |
48 | svg.attr('height', g.graph().height * initialScale + 40);
49 | }
50 |
--------------------------------------------------------------------------------
/.github/workflows/golangci-lint.yml:
--------------------------------------------------------------------------------
1 | name: golangci-lint
2 | on:
3 | push:
4 | tags:
5 | - v*
6 | branches:
7 | - main
8 | pull_request:
9 | permissions:
10 | contents: read
11 | # Optional: allow read access to pull request. Use with `only-new-issues` option.
12 | # pull-requests: read
13 | jobs:
14 | golangci:
15 | name: lint
16 | runs-on: ubuntu-latest
17 | steps:
18 | - uses: actions/checkout@v2
19 | - name: golangci-lint
20 | uses: golangci/golangci-lint-action@v2
21 | with:
22 | # Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version
23 | version: latest
24 |
25 | # Optional: working directory, useful for monorepos
26 | # working-directory: somedir
27 |
28 | # Optional: golangci-lint command line arguments.
29 | # args: --issues-exit-code=0
30 |
31 | # Optional: show only new issues if it's a pull request. The default value is `false`.
32 | # only-new-issues: true
33 |
34 | # Optional: if set to true then the action will use pre-installed Go.
35 | # skip-go-installation: true
36 |
37 | # Optional: if set to true then the action don't cache or restore ~/go/pkg.
38 | # skip-pkg-cache: true
39 |
40 | # Optional: if set to true then the action don't cache or restore ~/.cache/go-build.
41 | # skip-build-cache: true
42 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/fieldryand/goflow/v2
2 |
3 | go 1.20
4 |
5 | require (
6 | github.com/ef-ds/deque v1.0.4
7 | github.com/gin-gonic/gin v1.9.1
8 | github.com/google/uuid v1.6.0
9 | github.com/philippgille/gokv v0.7.0
10 | github.com/philippgille/gokv/gomap v0.7.0
11 | github.com/robfig/cron/v3 v3.0.1
12 | )
13 |
14 | require (
15 | github.com/bytedance/sonic v1.9.1 // indirect
16 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
17 | github.com/gabriel-vasile/mimetype v1.4.2 // indirect
18 | github.com/gin-contrib/sse v0.1.0 // indirect
19 | github.com/go-playground/locales v0.14.1 // indirect
20 | github.com/go-playground/universal-translator v0.18.1 // indirect
21 | github.com/go-playground/validator/v10 v10.14.0 // indirect
22 | github.com/goccy/go-json v0.10.2 // indirect
23 | github.com/json-iterator/go v1.1.12 // indirect
24 | github.com/klauspost/cpuid/v2 v2.2.4 // indirect
25 | github.com/leodido/go-urn v1.2.4 // indirect
26 | github.com/mattn/go-isatty v0.0.19 // indirect
27 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
28 | github.com/modern-go/reflect2 v1.0.2 // indirect
29 | github.com/pelletier/go-toml/v2 v2.0.8 // indirect
30 | github.com/philippgille/gokv/encoding v0.7.0 // indirect
31 | github.com/philippgille/gokv/util v0.7.0 // indirect
32 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
33 | github.com/ugorji/go/codec v1.2.11 // indirect
34 | golang.org/x/arch v0.3.0 // indirect
35 | golang.org/x/crypto v0.21.0 // indirect
36 | golang.org/x/net v0.23.0 // indirect
37 | golang.org/x/sys v0.18.0 // indirect
38 | golang.org/x/text v0.14.0 // indirect
39 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
40 | google.golang.org/protobuf v1.33.0 // indirect
41 | gopkg.in/yaml.v3 v3.0.1 // indirect
42 | )
43 |
--------------------------------------------------------------------------------
/task.go:
--------------------------------------------------------------------------------
1 | package goflow
2 |
3 | import (
4 | "math"
5 | "time"
6 | )
7 |
8 | // A Task is the unit of work that makes up a job. Whenever a task is executed, it
9 | // calls its associated operator.
10 | type Task struct {
11 | Name string
12 | Operator Operator
13 | TriggerRule triggerRule
14 | Retries int
15 | RetryDelay RetryDelay
16 | remaining int
17 | state state
18 | }
19 |
20 | type triggerRule string
21 |
22 | const (
23 | allDone triggerRule = "allDone"
24 | allSuccessful triggerRule = "allSuccessful"
25 | )
26 |
27 | func (t *Task) run(writes chan writeOp) error {
28 |
29 | _, err := t.Operator.Run()
30 |
31 | // retry
32 | if err != nil && t.remaining > 0 {
33 | writes <- writeOp{t.Name, upForRetry}
34 | return nil
35 | }
36 |
37 | // failed
38 | if err != nil && t.remaining <= 0 {
39 | writes <- writeOp{t.Name, failed}
40 | return err
41 | }
42 |
43 | // success
44 | writes <- writeOp{t.Name, successful}
45 | return nil
46 | }
47 |
48 | func (t *Task) skip(writes chan writeOp) error {
49 | writes <- writeOp{t.Name, skipped}
50 | return nil
51 | }
52 |
53 | // RetryDelay is a type that implements a Wait() method, which is called in between
54 | // task retry attempts.
55 | type RetryDelay interface {
56 | wait(taskName string, attempt int)
57 | }
58 |
59 | // ConstantDelay waits a constant number of seconds between task retries.
60 | type ConstantDelay struct{ Period int }
61 |
62 | func (d ConstantDelay) wait(task string, attempt int) {
63 | time.Sleep(time.Duration(d.Period) * time.Second)
64 | }
65 |
66 | // ExponentialBackoff waits exponentially longer between each retry attempt.
67 | type ExponentialBackoff struct{}
68 |
69 | func (d ExponentialBackoff) wait(task string, attempt int) {
70 | delay := math.Pow(2, float64(attempt))
71 | time.Sleep(time.Duration(delay) * time.Second)
72 | }
73 |
--------------------------------------------------------------------------------
/ui/html/index.html.tmpl:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Goflow
7 |
8 |
9 |
12 |
13 |
Jobs
14 |
15 |
16 |
22 |
23 |
24 |
25 |
26 |
27 |
Job
28 |
Schedule
29 |
State
30 |
31 | {{ range .jobs }}
32 |
33 |
34 |
{{ .Schedule }}
35 |
36 |
37 |
38 |
39 |
40 |
41 | {{ end }}
42 |
43 |
44 |
45 |
46 |
49 |
--------------------------------------------------------------------------------
/operator.go:
--------------------------------------------------------------------------------
1 | package goflow
2 |
3 | import (
4 | "fmt"
5 | "io"
6 | "net/http"
7 | "os/exec"
8 | )
9 |
10 | // An Operator implements a Run() method. When a job executes a task that
11 | // uses the operator, the Run() method is called.
12 | type Operator interface {
13 | Run() (interface{}, error)
14 | }
15 |
16 | // Command executes a shell command.
17 | type Command struct {
18 | Cmd string
19 | Args []string
20 | }
21 |
22 | // Run passes the command and arguments to exec.Command and captures the
23 | // output.
24 | func (o Command) Run() (interface{}, error) {
25 | out, err := exec.Command(o.Cmd, o.Args...).Output()
26 | return string(out), err
27 | }
28 |
29 | // Get makes a GET request.
30 | type Get struct {
31 | Client *http.Client
32 | URL string
33 | }
34 |
35 | // Run sends the request and returns an error if the status code is
36 | // outside the 2xx range.
37 | func (o Get) Run() (interface{}, error) {
38 | res, err := o.Client.Get(o.URL)
39 | if err != nil {
40 | return nil, err
41 | }
42 | defer res.Body.Close()
43 |
44 | if res.StatusCode < 200 || res.StatusCode > 299 {
45 | return nil, fmt.Errorf("Received status code %v", res.StatusCode)
46 | }
47 |
48 | content, err := io.ReadAll(res.Body)
49 | return string(content), err
50 | }
51 |
52 | // Post makes a POST request.
53 | type Post struct {
54 | Client *http.Client
55 | URL string
56 | Body io.Reader
57 | }
58 |
59 | // Run sends the request and returns an error if the status code is
60 | // outside the 2xx range.
61 | func (o Post) Run() (interface{}, error) {
62 | res, err := o.Client.Post(o.URL, "application/json", o.Body)
63 | if err != nil {
64 | return nil, err
65 | }
66 | defer res.Body.Close()
67 |
68 | if res.StatusCode < 200 || res.StatusCode > 299 {
69 | return nil, fmt.Errorf("Received status code %v", res.StatusCode)
70 | }
71 |
72 | content, err := io.ReadAll(res.Body)
73 | return string(content), err
74 | }
75 |
--------------------------------------------------------------------------------
/ui/html/job.html.tmpl:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Goflow
6 |
7 |
8 |
9 |
12 |
13 |
{{ .jobName }}
14 |
15 |
{{ .schedule }}
16 |
17 |
18 |
19 |
20 |
21 |
27 |
28 |
29 |
30 |
31 |
32 |
Task
33 |
State
34 | {{ range $ix, $taskName := .taskNames }}
35 |
{{ $taskName }}
36 |
37 | {{ end }}
38 |
39 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/.github/workflows/deploy-image.yml:
--------------------------------------------------------------------------------
1 | # This workflow uses actions that are not certified by GitHub.
2 | # They are provided by a third-party and are governed by
3 | # separate terms of service, privacy policy, and support
4 | # documentation.
5 |
6 | name: Create and publish a Docker image
7 |
8 | on:
9 | push:
10 | tags:
11 | - "v*.*.*"
12 |
13 | env:
14 | REGISTRY: ghcr.io
15 | IMAGE_NAME: fieldryand/goflow-example
16 |
17 | jobs:
18 | build-and-push-image:
19 | runs-on: ubuntu-latest
20 | permissions:
21 | contents: write
22 | packages: write
23 |
24 | steps:
25 | - name: Checkout repository
26 | uses: actions/checkout@v2
27 |
28 | - name: Set up Go
29 | uses: actions/setup-go@v4
30 | with:
31 | go-version: '1.20'
32 |
33 | - name: Set up Node
34 | uses: actions/setup-node@v2
35 |
36 | - name: Compile UI
37 | run: cd ui && npm install && npm run build
38 |
39 | - name: Build goflow-example
40 | run: go build example/goflow-example.go
41 |
42 | - name: Log in to the Container registry
43 | uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9
44 | with:
45 | registry: ${{ env.REGISTRY }}
46 | username: ${{ github.actor }}
47 | password: ${{ secrets.GITHUB_TOKEN }}
48 |
49 | - name: Extract metadata (tags, labels) for Docker
50 | id: meta
51 | uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
52 | with:
53 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
54 |
55 | - name: Build and push Docker image
56 | uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc
57 | with:
58 | context: .
59 | push: true
60 | tags: ${{ steps.meta.outputs.tags }}
61 | labels: org.opencontainers.image.source=https://github.com/fieldryand/goflow
62 |
63 | - name: Tarball ui
64 | run: tar -czvf goflow-ui.tar.gz ui
65 |
66 | - name: Release
67 | uses: softprops/action-gh-release@v1
68 | with:
69 | files: goflow-ui.tar.gz
70 |
--------------------------------------------------------------------------------
/execution.go:
--------------------------------------------------------------------------------
1 | package goflow
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/google/uuid"
7 | "github.com/philippgille/gokv"
8 | )
9 |
10 | // Execution of a job.
11 | type execution struct {
12 | ID uuid.UUID `json:"id"`
13 | JobName string `json:"job"`
14 | StartedAt string `json:"submitted"`
15 | ModifiedTimestamp string `json:"modifiedTimestamp"`
16 | State state `json:"state"`
17 | TaskExecutions []taskExecution `json:"tasks"`
18 | }
19 |
20 | type taskExecution struct {
21 | Name string `json:"name"`
22 | State state `json:"state"`
23 | }
24 |
25 | func (j *Job) newExecution() *execution {
26 | taskExecutions := make([]taskExecution, 0)
27 | for _, task := range j.Tasks {
28 | taskrun := taskExecution{task.Name, none}
29 | taskExecutions = append(taskExecutions, taskrun)
30 | }
31 | return &execution{
32 | ID: uuid.New(),
33 | JobName: j.Name,
34 | StartedAt: time.Now().UTC().Format(time.RFC3339Nano),
35 | ModifiedTimestamp: time.Now().UTC().Format(time.RFC3339Nano),
36 | State: none,
37 | TaskExecutions: taskExecutions}
38 | }
39 |
40 | // Persist a new execution.
41 | func persistNewExecution(s gokv.Store, e *execution) error {
42 | key := e.ID
43 | return s.Set(key.String(), e)
44 | }
45 |
46 | type executionIndex struct {
47 | ExecutionIDs []string `json:"executions"`
48 | }
49 |
50 | // Index the job runs
51 | func indexExecutions(s gokv.Store, e *execution) error {
52 |
53 | // get the job from the execution
54 | j := e.JobName
55 |
56 | // retrieve the list of executions of that job
57 | i := executionIndex{}
58 | s.Get(j, &i)
59 |
60 | // append to the list
61 | i.ExecutionIDs = append(i.ExecutionIDs, e.ID.String())
62 | return s.Set(e.JobName, i)
63 | }
64 |
65 | // Read all the persisted executions for a given job.
66 | func readExecutions(s gokv.Store, j string) ([]*execution, error) {
67 |
68 | // retrieve the list of executions of the job
69 | i := executionIndex{}
70 | s.Get(j, &i)
71 |
72 | // return the list
73 | executions := make([]*execution, 0)
74 | for _, key := range i.ExecutionIDs {
75 | val := execution{}
76 | s.Get(key, &val)
77 | executions = append(executions, &val)
78 | }
79 |
80 | return executions, nil
81 | }
82 |
83 | // Sync the current state to the persisted execution.
84 | func syncStateToStore(s gokv.Store, e *execution, taskName string, taskState state) error {
85 | key := e.ID
86 | for ix, task := range e.TaskExecutions {
87 | if task.Name == taskName {
88 | e.TaskExecutions[ix].State = taskState
89 | }
90 | }
91 | return s.Set(key.String(), e)
92 | }
93 |
--------------------------------------------------------------------------------
/operator_test.go:
--------------------------------------------------------------------------------
1 | package goflow
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "net/http"
7 | "net/http/httptest"
8 | "testing"
9 | )
10 |
11 | func TestCommand(t *testing.T) {
12 | result, _ := Command{Cmd: "sh", Args: []string{"-c", "echo $((2 + 4))"}}.Run()
13 | resultStr := fmt.Sprintf("%v", result)
14 | expected := "6\n"
15 |
16 | if resultStr != expected {
17 | t.Errorf("Expected %s, got %s", expected, resultStr)
18 | }
19 | }
20 |
21 | func TestGetSuccess(t *testing.T) {
22 | expected := "OK"
23 | srv := httptest.NewServer(
24 | http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
25 | w.WriteHeader(200)
26 | w.Write([]byte(expected))
27 | }))
28 | defer srv.Close()
29 |
30 | client := &http.Client{}
31 | result, _ := Get{client, srv.URL}.Run()
32 |
33 | if result != expected {
34 | t.Errorf("Expected %s, got %s", expected, result)
35 | }
36 | }
37 |
38 | func TestGetNotFound(t *testing.T) {
39 | srv := httptest.NewServer(
40 | http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
41 | w.WriteHeader(404)
42 | w.Write([]byte("Page not found"))
43 | }))
44 | defer srv.Close()
45 |
46 | client := &http.Client{}
47 | _, err := Get{client, srv.URL}.Run()
48 |
49 | if err == nil {
50 | t.Errorf("Expected an error")
51 | }
52 | }
53 |
54 | func TestGetInvalid(t *testing.T) {
55 | client := &http.Client{}
56 | _, err := Get{client, ""}.Run()
57 |
58 | if err == nil {
59 | t.Errorf("Expected an error")
60 | }
61 | }
62 |
63 | func TestPostSuccess(t *testing.T) {
64 | expected := "OK"
65 | srv := httptest.NewServer(
66 | http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
67 | w.WriteHeader(200)
68 | w.Write([]byte(expected))
69 | }))
70 | defer srv.Close()
71 |
72 | client := &http.Client{}
73 | result, _ := Post{client, srv.URL, bytes.NewBuffer([]byte(""))}.Run()
74 |
75 | if result != expected {
76 | t.Errorf("Expected %s, got %s", expected, result)
77 | }
78 | }
79 |
80 | func TestPostNotFound(t *testing.T) {
81 | srv := httptest.NewServer(
82 | http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
83 | w.WriteHeader(404)
84 | w.Write([]byte("Page not found"))
85 | }))
86 | defer srv.Close()
87 |
88 | client := &http.Client{}
89 | _, err := Post{client, srv.URL, bytes.NewBuffer([]byte(""))}.Run()
90 |
91 | if err == nil {
92 | t.Errorf("Expected an error")
93 | }
94 | }
95 |
96 | func TestPostInvalid(t *testing.T) {
97 | client := &http.Client{}
98 | _, err := Post{client, "", bytes.NewBuffer([]byte(""))}.Run()
99 |
100 | if err == nil {
101 | t.Errorf("Expected an error")
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/dag.go:
--------------------------------------------------------------------------------
1 | package goflow
2 |
3 | import (
4 | "github.com/ef-ds/deque"
5 | )
6 |
7 | // Credit: The DAG implementation here is roughly a port of the
8 | // one from this Python project: https://github.com/thieman/dagobah
9 |
10 | // A DAG is a directed acyclic graph represented by a simple map
11 | // where a key is a node in the graph, and a value is a slice of
12 | // immediately downstream dependent nodes.
13 | type dag map[string][]string
14 |
15 | // A node has a name and 0 or more dependent nodes
16 | func (d dag) addNode(name string) {
17 | deps := make([]string, 0)
18 | d[name] = deps
19 | }
20 |
21 | // Create an edge between an independent and dependent node
22 | func (d dag) setDownstream(ind, dep string) {
23 | d[ind] = append(d[ind], dep)
24 | }
25 |
26 | // Returns true if a node is a downstream node, false if it
27 | // is independent.
28 | func (d dag) isDownstream(nodeName string) bool {
29 | ind := d.independentNodes()
30 |
31 | for _, name := range ind {
32 | if nodeName == name {
33 | return false
34 | }
35 | }
36 |
37 | return true
38 | }
39 |
40 | // Ensure the DAG is acyclic
41 | func (d dag) validate() bool {
42 | degree := make(map[string]int)
43 |
44 | for node := range d {
45 | degree[node] = 0
46 | }
47 |
48 | for _, ds := range d {
49 | for _, i := range ds {
50 | degree[i]++
51 | }
52 | }
53 |
54 | var deq deque.Deque
55 |
56 | for node, val := range degree {
57 | if val == 0 {
58 | deq.PushFront(node)
59 | }
60 | }
61 |
62 | l := make([]string, 0)
63 |
64 | for {
65 | popped, ok := deq.PopBack()
66 |
67 | if !ok {
68 | break
69 | } else {
70 | node := popped.(string)
71 | l = append(l, node)
72 | dsNodes := d[node]
73 | for _, dsNode := range dsNodes {
74 | degree[dsNode]--
75 | if degree[dsNode] == 0 {
76 | deq.PushFront(dsNode)
77 | }
78 | }
79 | }
80 | }
81 |
82 | return len(l) == len(d)
83 | }
84 |
85 | // Return the immediately upstream nodes for a given node
86 | func (d dag) dependencies(node string) []string {
87 |
88 | dependencies := make([]string, 0)
89 |
90 | for dep, ds := range d {
91 | for _, i := range ds {
92 | if node == i {
93 | dependencies = append(dependencies, dep)
94 | }
95 | }
96 | }
97 |
98 | return dependencies
99 | }
100 |
101 | // Return all the independent nodes in the graph
102 | func (d dag) independentNodes() []string {
103 |
104 | downstream := make([]string, 0)
105 |
106 | for _, ds := range d {
107 | downstream = append(downstream, ds...)
108 | }
109 |
110 | ind := make([]string, 0)
111 |
112 | for node := range d {
113 | ctr := 0
114 | for _, i := range downstream {
115 | if node == i {
116 | ctr++
117 | }
118 | }
119 | if ctr == 0 {
120 | ind = append(ind, node)
121 | }
122 | }
123 |
124 | return ind
125 |
126 | }
127 |
--------------------------------------------------------------------------------
/example.go:
--------------------------------------------------------------------------------
1 | package goflow
2 |
3 | import (
4 | "errors"
5 | "math/rand"
6 | )
7 |
8 | // Crunch some numbers
9 | func complexAnalyticsJob() *Job {
10 | j := &Job{
11 | Name: "example-complex-analytics",
12 | Schedule: "* * * * * *",
13 | Active: false,
14 | }
15 |
16 | j.Add(&Task{
17 | Name: "sleep-one",
18 | Operator: Command{Cmd: "sleep", Args: []string{"1"}},
19 | })
20 | j.Add(&Task{
21 | Name: "add-one-one",
22 | Operator: Command{Cmd: "sh", Args: []string{"-c", "echo $((1 + 1))"}},
23 | })
24 | j.Add(&Task{
25 | Name: "sleep-two",
26 | Operator: Command{Cmd: "sleep", Args: []string{"2"}},
27 | })
28 | j.Add(&Task{
29 | Name: "add-two-four",
30 | Operator: Command{Cmd: "sh", Args: []string{"-c", "echo $((2 + 4))"}},
31 | })
32 | j.Add(&Task{
33 | Name: "add-three-four",
34 | Operator: Command{Cmd: "sh", Args: []string{"-c", "echo $((3 + 4))"}},
35 | })
36 | j.Add(&Task{
37 | Name: "whoops-with-constant-delay",
38 | Operator: Command{Cmd: "whoops", Args: []string{}},
39 | Retries: 5,
40 | RetryDelay: ConstantDelay{Period: 1},
41 | })
42 | j.Add(&Task{
43 | Name: "whoops-with-exponential-backoff",
44 | Operator: Command{Cmd: "whoops", Args: []string{}},
45 | Retries: 1,
46 | RetryDelay: ExponentialBackoff{},
47 | })
48 | j.Add(&Task{
49 | Name: "totally-skippable",
50 | Operator: Command{Cmd: "sh", Args: []string{"-c", "echo 'everything succeeded'"}},
51 | TriggerRule: "allSuccessful",
52 | })
53 | j.Add(&Task{
54 | Name: "clean-up",
55 | Operator: Command{Cmd: "sh", Args: []string{"-c", "echo 'cleaning up now'"}},
56 | TriggerRule: "allDone",
57 | })
58 |
59 | j.SetDownstream(j.Task("sleep-one"), j.Task("add-one-one"))
60 | j.SetDownstream(j.Task("add-one-one"), j.Task("sleep-two"))
61 | j.SetDownstream(j.Task("sleep-two"), j.Task("add-two-four"))
62 | j.SetDownstream(j.Task("add-one-one"), j.Task("add-three-four"))
63 | j.SetDownstream(j.Task("sleep-one"), j.Task("whoops-with-constant-delay"))
64 | j.SetDownstream(j.Task("sleep-one"), j.Task("whoops-with-exponential-backoff"))
65 | j.SetDownstream(j.Task("whoops-with-constant-delay"), j.Task("totally-skippable"))
66 | j.SetDownstream(j.Task("whoops-with-exponential-backoff"), j.Task("totally-skippable"))
67 | j.SetDownstream(j.Task("totally-skippable"), j.Task("clean-up"))
68 |
69 | return j
70 | }
71 |
72 | // RandomFailure fails randomly. This is a contrived example for demo purposes.
73 | type RandomFailure struct{ n int }
74 |
75 | // rng with seed=1
76 | var r = rand.New(rand.NewSource(1))
77 |
78 | // Run implements failures at random intervals.
79 | func (o RandomFailure) Run() (interface{}, error) {
80 | x := r.Intn(o.n)
81 |
82 | if x == o.n-1 {
83 | return nil, errors.New("unlucky")
84 | }
85 |
86 | return x, nil
87 | }
88 |
89 | // Use our custom operation in a job.
90 | func customOperatorJob() *Job {
91 | j := &Job{Name: "example-custom-operator", Schedule: "* * * * * *", Active: true}
92 | j.Add(&Task{Name: "random-failure", Operator: RandomFailure{4}})
93 | return j
94 | }
95 |
--------------------------------------------------------------------------------
/ui/css/styles.css:
--------------------------------------------------------------------------------
1 | body {
2 | font-family: sans-serif;
3 | }
4 |
5 | .job-table {
6 | display: grid;
7 | grid-template-columns: 1fr 1fr 2fr 0.5fr;
8 | }
9 |
10 | .job-table > div {
11 | padding: .5em
12 | }
13 |
14 | .job-table>div:nth-child(-n+5) {
15 | background-color: mediumslateblue;
16 | color:white;
17 | }
18 |
19 | .job-table>div:nth-child(8n+5), .job-table>div:nth-child(8n+6), .job-table>div:nth-child(8n+7), .job-table>div:nth-child(8n+8) {
20 | background-color: whitesmoke
21 | }
22 |
23 | .task-table {
24 | display: grid;
25 | grid-template-columns: max-content auto;
26 | }
27 |
28 | .task-table > div {
29 | padding: .5em
30 | }
31 |
32 | .task-table>div:nth-child(-n+2) {
33 | background-color: mediumslateblue;
34 | color:white;
35 | }
36 |
37 | .task-table>div:nth-child(4n+3), .task-table>div:nth-child(4n+4) {
38 | background-color: whitesmoke
39 | }
40 |
41 | .button-container {
42 | display: flex;
43 | justify-content: flex-end;
44 | align-items: flex-start;
45 | gap: 0.5em;
46 | }
47 |
48 | .button-container-job-page {
49 | display: flex;
50 | align-items: flex-start;
51 | gap: 0.5em;
52 | }
53 |
54 | .top-nav {
55 | padding-left: 5em;
56 | padding-right: 5em;
57 | padding-top: 1em;
58 | padding-bottom: 1em;
59 | }
60 |
61 | .job-container {
62 | padding-left: 5em;
63 | padding-right: 5em;
64 | }
65 |
66 | .job-info {
67 | padding-left: 5em;
68 | padding-right: 5em;
69 | padding-bottom: 1em;
70 | display: grid;
71 | grid-template-columns: auto;
72 | grid-gap: 1em;
73 | }
74 |
75 | .job-info-title {
76 | font-size: 1.5em;
77 | }
78 |
79 | /* Default select style */
80 | select {
81 | background-color: mediumslateblue;
82 | border-radius: .2em;
83 | color:white;
84 | border-color:mediumslateblue;
85 | border-style: none;
86 | padding:.5em .75em;
87 | white-space: nowrap;
88 | }
89 |
90 | /* Default button style */
91 | button {
92 | background-color: mediumslateblue;
93 | border-radius: .2em;
94 | color:white;
95 | border-color:mediumslateblue;
96 | border-style: none;
97 | padding:.5em .75em;
98 | white-space: nowrap;
99 | }
100 |
101 | /* Style when button is clicked */
102 | button.clicked {
103 | transform: translateY(2px);
104 | box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
105 | }
106 |
107 | a {
108 | color: mediumslateblue;
109 | }
110 |
111 | .node rect {
112 | stroke: #333;
113 | fill: #fff;
114 | }
115 |
116 | .edgePath path {
117 | stroke: #333;
118 | fill: #333;
119 | stroke-width: 1.5px;
120 | }
121 |
122 | .graph {
123 | border: 1px solid #ccc;
124 | overflow: hidden;
125 | margin: 0 auto;
126 | }
127 |
128 | .graph-container {
129 | padding-top: 2em;
130 | }
131 |
132 | .status-indicator {
133 | height: 1em;
134 | width: 1em;
135 | border-radius: 50%;
136 | border: 1px solid #676767;
137 | }
138 |
139 | .status-wrapper {
140 | display: flex;
141 | flex-wrap: wrap;
142 | gap: 0.5em
143 | }
144 |
145 | .schedule-badge-active-true::after {
146 | content: 'Active';
147 | background-color: white;
148 | color:mediumslateblue;
149 | border: 1px solid mediumslateblue;
150 | border-radius: 1em;
151 | font-size: .75rem;
152 | padding: 0.1em 0.75em;
153 | margin-left: 1em;
154 | }
155 |
156 | .schedule-badge-active-false {
157 | }
158 |
--------------------------------------------------------------------------------
/goflow.go:
--------------------------------------------------------------------------------
1 | // Package goflow implements a simple but powerful DAG scheduler and dashboard.
2 | package goflow
3 |
4 | import (
5 | "log"
6 |
7 | "github.com/gin-gonic/gin"
8 | "github.com/google/uuid"
9 | "github.com/philippgille/gokv"
10 | "github.com/philippgille/gokv/gomap"
11 | "github.com/robfig/cron/v3"
12 | )
13 |
14 | // Goflow contains job data and a router.
15 | type Goflow struct {
16 | Store gokv.Store
17 | Options Options
18 | Jobs map[string](func() *Job)
19 | router *gin.Engine
20 | cron *cron.Cron
21 | jobs []string
22 | }
23 |
24 | // Options to control various Goflow behavior.
25 | type Options struct {
26 | Store gokv.Store
27 | UIPath string
28 | Streaming bool
29 | ShowExamples bool
30 | WithSeconds bool
31 | }
32 |
33 | // New returns a Goflow engine.
34 | func New(opts Options) *Goflow {
35 |
36 | // Add a default store if necessary
37 | if opts.Store == nil {
38 | opts.Store = gomap.NewStore(gomap.DefaultOptions)
39 | }
40 |
41 | // Add the cron schedule
42 | var c *cron.Cron
43 | if opts.WithSeconds {
44 | c = cron.New(cron.WithSeconds())
45 | } else {
46 | c = cron.New()
47 | }
48 |
49 | g := &Goflow{
50 | Store: opts.Store,
51 | Options: opts,
52 | Jobs: make(map[string](func() *Job)),
53 | router: gin.New(),
54 | cron: c,
55 | }
56 |
57 | if opts.ShowExamples {
58 | g.AddJob(complexAnalyticsJob)
59 | g.AddJob(customOperatorJob)
60 | }
61 |
62 | return g
63 | }
64 |
65 | // scheduledExecution implements cron.Job
66 | type scheduledExecution struct {
67 | store gokv.Store
68 | jobFunc func() *Job
69 | }
70 |
71 | func (schedExec *scheduledExecution) Run() {
72 |
73 | // create job
74 | job := schedExec.jobFunc()
75 |
76 | // create and persist a new execution
77 | e := job.newExecution()
78 | persistNewExecution(schedExec.store, e)
79 | indexExecutions(schedExec.store, e)
80 |
81 | // start running the job
82 | job.run(schedExec.store, e)
83 | }
84 |
85 | // AddJob takes a job-emitting function and registers it
86 | // with the engine.
87 | func (g *Goflow) AddJob(jobFunc func() *Job) *Goflow {
88 |
89 | j := jobFunc()
90 |
91 | // TODO: change the return type here to error
92 | // "" is not a valid key in the storage layer
93 | //if j.Name == "" {
94 | // return errors.New("\"\" is not a valid job name")
95 | // }
96 |
97 | // Register the job
98 | g.Jobs[j.Name] = jobFunc
99 | g.jobs = append(g.jobs, j.Name)
100 |
101 | // If the job is active by default, add it to the cron schedule
102 | if j.Active {
103 | e := &scheduledExecution{g.Store, jobFunc}
104 | _, err := g.cron.AddJob(j.Schedule, e)
105 |
106 | if err != nil {
107 | panic(err)
108 | }
109 | }
110 |
111 | return g
112 | }
113 |
114 | // toggle flips a job's cron schedule status from active to inactive
115 | // and vice versa. It returns true if the new status is active and false
116 | // if it is inactive.
117 | func (g *Goflow) toggle(jobName string) (bool, error) {
118 |
119 | // if the job is found in the list of entries, remove it
120 | for _, entry := range g.cron.Entries() {
121 | if name := entry.Job.(*scheduledExecution).jobFunc().Name; name == jobName {
122 | g.cron.Remove(entry.ID)
123 | return false, nil
124 | }
125 | }
126 |
127 | // else add a new entry
128 | jobFunc := g.Jobs[jobName]
129 | e := &scheduledExecution{g.Store, jobFunc}
130 | g.cron.AddJob(jobFunc().Schedule, e)
131 | return true, nil
132 | }
133 |
134 | // execute tells the engine to run a given job in a new goroutine.
135 | func (g *Goflow) execute(job string) uuid.UUID {
136 |
137 | // create job
138 | j := g.Jobs[job]()
139 |
140 | // create and persist a new execution
141 | e := j.newExecution()
142 | persistNewExecution(g.Store, e)
143 | indexExecutions(g.Store, e)
144 |
145 | // start running the job
146 | go j.run(g.Store, e)
147 |
148 | return e.ID
149 | }
150 |
151 | // Use middleware in the Gin router.
152 | func (g *Goflow) Use(middleware gin.HandlerFunc) *Goflow {
153 | g.router.Use(middleware)
154 | return g
155 | }
156 |
157 | // Run runs the webserver.
158 | func (g *Goflow) Run(port string) {
159 | log.SetFlags(0)
160 | log.SetOutput(new(logWriter))
161 | g.router.Use(gin.Recovery())
162 | g.addStreamRoute(true)
163 | g.addAPIRoutes()
164 | if g.Options.UIPath != "" {
165 | g.addUIRoutes()
166 | g.addStaticRoutes()
167 | }
168 | g.cron.Start()
169 | g.router.Run(port)
170 | }
171 |
--------------------------------------------------------------------------------
/job_test.go:
--------------------------------------------------------------------------------
1 | package goflow
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/philippgille/gokv/gomap"
7 | )
8 |
9 | func TestJob(t *testing.T) {
10 | j := &Job{Name: "example", Schedule: "* * * * *"}
11 |
12 | j.Add(&Task{
13 | Name: "add-one-one",
14 | Operator: Command{Cmd: "sh", Args: []string{"-c", "echo $((1 + 1))"}},
15 | })
16 | j.Add(&Task{
17 | Name: "sleep-two",
18 | Operator: Command{Cmd: "sleep", Args: []string{"2"}},
19 | })
20 | j.Add(&Task{
21 | Name: "add-two-four",
22 | Operator: Command{Cmd: "sh", Args: []string{"-c", "echo $((2 + 4))"}},
23 | })
24 | j.Add(&Task{
25 | Name: "add-three-four",
26 | Operator: Command{Cmd: "sh", Args: []string{"-c", "echo $((3 + 4))"}},
27 | })
28 | j.Add(&Task{
29 | Name: "whoops-with-constant-delay",
30 | Operator: Command{Cmd: "whoops", Args: []string{}},
31 | Retries: 5,
32 | RetryDelay: ConstantDelay{1},
33 | })
34 | j.Add(&Task{
35 | Name: "whoops-with-exponential-backoff",
36 | Operator: Command{Cmd: "whoops", Args: []string{}},
37 | Retries: 1,
38 | RetryDelay: ExponentialBackoff{},
39 | })
40 | j.Add(&Task{
41 | Name: "totally-skippable",
42 | Operator: Command{Cmd: "sh", Args: []string{"-c", "echo 'everything succeeded'"}},
43 | TriggerRule: "allSuccessful",
44 | })
45 | j.Add(&Task{
46 | Name: "clean-up",
47 | Operator: Command{Cmd: "sh", Args: []string{"-c", "echo 'cleaning up now'"}},
48 | TriggerRule: "allDone",
49 | })
50 | j.Add(&Task{
51 | Name: "failure",
52 | Operator: RandomFailure{1},
53 | })
54 |
55 | j.SetDownstream(j.Task("add-one-one"), j.Task("sleep-two"))
56 | j.SetDownstream(j.Task("sleep-two"), j.Task("add-two-four"))
57 | j.SetDownstream(j.Task("add-one-one"), j.Task("add-three-four"))
58 | j.SetDownstream(j.Task("add-one-one"), j.Task("whoops-with-constant-delay"))
59 | j.SetDownstream(j.Task("add-one-one"), j.Task("whoops-with-exponential-backoff"))
60 | j.SetDownstream(j.Task("whoops-with-constant-delay"), j.Task("totally-skippable"))
61 | j.SetDownstream(j.Task("whoops-with-exponential-backoff"), j.Task("totally-skippable"))
62 | j.SetDownstream(j.Task("totally-skippable"), j.Task("clean-up"))
63 |
64 | store := gomap.NewStore(gomap.DefaultOptions)
65 |
66 | go j.run(store, j.newExecution())
67 |
68 | for {
69 | if j.allDone() {
70 | break
71 | }
72 | }
73 |
74 | if j.loadTaskState("add-one-one") != successful {
75 | t.Errorf("Got status %v, expected %v", j.loadTaskState("add-one-one"), successful)
76 | }
77 | if j.loadTaskState("sleep-two") != successful {
78 | t.Errorf("Got status %v, expected %v", j.loadTaskState("sleep-two"), successful)
79 | }
80 | if j.loadTaskState("add-two-four") != successful {
81 | t.Errorf("Got status %v, expected %v", j.loadTaskState("add-two-four"), successful)
82 | }
83 | if j.loadTaskState("add-three-four") != successful {
84 | t.Errorf("Got status %v, expected %v", j.loadTaskState("add-three-four"), successful)
85 | }
86 | if j.loadTaskState("whoops-with-constant-delay") != failed {
87 | t.Errorf("Got status %v, expected %v", j.loadTaskState("whoops-with-constant-delay"), failed)
88 | }
89 | if j.loadTaskState("whoops-with-exponential-backoff") != failed {
90 | t.Errorf("Got status %v, expected %v", j.loadTaskState("whoops-with-exponential-backoff"), failed)
91 | }
92 | if j.loadTaskState("totally-skippable") != skipped {
93 | t.Errorf("Got status %v, expected %v", j.loadTaskState("totally-skippable"), skipped)
94 | }
95 | if j.loadTaskState("clean-up") != successful {
96 | t.Errorf("Got status %v, expected %v", j.loadTaskState("clean-up"), successful)
97 | }
98 | if j.loadTaskState("failure") != failed {
99 | t.Errorf("Got status %v, expected %v", j.loadTaskState("failure"), failed)
100 | }
101 |
102 | }
103 |
104 | func TestCyclicJob(t *testing.T) {
105 | j := &Job{Name: "cyclic", Schedule: "* * * * *"}
106 |
107 | j.Add(&Task{
108 | Name: "add-two-four",
109 | Operator: Command{Cmd: "sh", Args: []string{"-c", "echo $((2 + 4))"}},
110 | })
111 | j.Add(&Task{
112 | Name: "add-three-four",
113 | Operator: Command{Cmd: "sh", Args: []string{"-c", "echo $((3 + 4))"}},
114 | })
115 |
116 | j.SetDownstream(j.Task("add-two-four"), j.Task("add-three-four"))
117 | j.SetDownstream(j.Task("add-three-four"), j.Task("add-two-four"))
118 |
119 | store := gomap.NewStore(gomap.DefaultOptions)
120 |
121 | j.run(store, j.newExecution())
122 | }
123 |
--------------------------------------------------------------------------------
/ui/src/plain.js:
--------------------------------------------------------------------------------
1 | function getDropdownValue() {
2 | const selectDropdown = document.querySelector('select');
3 | return selectDropdown.value
4 | }
5 |
6 | function indexPageEventListener() {
7 | var stream = new EventSource(`/stream`);
8 | stream.addEventListener("message", indexPageEventHandler)
9 | }
10 |
11 | function jobPageEventListener(job) {
12 | var stream = new EventSource(`/stream?jobname=${job}`);
13 | stream.addEventListener("message", jobPageEventHandler)
14 | }
15 |
16 | function indexPageEventHandler(message) {
17 | const d = JSON.parse(message.data);
18 | const s = stateColor(d.state);
19 | updateStateCircles("job-table", d.id, d.job, s, d.submitted);
20 | }
21 |
22 | function jobPageEventHandler(message) {
23 | const d = JSON.parse(message.data);
24 | updateTaskStateCircles(d);
25 | updateGraphViz(d);
26 | updateLastRunTs(d);
27 | }
28 |
29 | function updateStateCircles(tableName, jobID, wrapperId, color, startTimestamp) {
30 | const options = {
31 | hour: '2-digit',
32 | minute: '2-digit',
33 | second: '2-digit'
34 | };
35 | const limit = getDropdownValue();
36 | const wrapper = document.getElementById(wrapperId);
37 | const startTs = new Date(startTimestamp);
38 | const formattedTs = startTs.toLocaleString(undefined, options);
39 | div = document.createElement("div");
40 | div.setAttribute("id", jobID);
41 | div.setAttribute("class", "status-indicator");
42 | div.setAttribute("style", `background-color:${color}`);
43 | div.setAttribute("title", `ID: ${jobID}\nStarted: ${formattedTs}`);
44 | if (jobID in wrapper.children) {
45 | wrapper.replaceChild(div, document.getElementById(jobID));
46 | } else {
47 | if (wrapper.childElementCount >= limit) {
48 | wrapper.removeChild(wrapper.firstElementChild);
49 | wrapper.appendChild(div);
50 | } else {
51 | wrapper.appendChild(div);
52 | }
53 | }
54 | }
55 |
56 | function updateTaskStateCircles(execution) {
57 | for (i in execution.tasks) {
58 | const t = execution.tasks[i];
59 | const s = stateColor(t.state);
60 | updateStateCircles("task-table", `${execution.id}-${t.name}`, t.name, s, execution.submitted);
61 | }
62 | }
63 |
64 | function updateGraphViz(execution) {
65 | const tasks = execution.tasks
66 | for (i in tasks) {
67 | if (document.getElementsByClassName("output")) {
68 | try {
69 | const rect = document.getElementById("node-" + tasks[i].name).querySelector("rect");
70 | rect.setAttribute("style", "stroke-width: 2; stroke: " + stateColor(tasks[i].state));
71 | }
72 | catch(err) {
73 | console.log(`${err}. This might be a temporary error when the graph is still loading.`)
74 | }
75 | }
76 | }
77 | }
78 |
79 | function updateLastRunTs(execution) {
80 | const lastExecutionTs = execution.submitted;
81 | const lastExecutionTsHTML = document.getElementById("last-execution-ts-wrapper").innerHTML;
82 | const newHTML = lastExecutionTsHTML.replace(/.*/, `Last run: ${lastExecutionTs}`);
83 | document.getElementById("last-execution-ts-wrapper").innerHTML = newHTML;
84 | }
85 |
86 | function updateJobActive(jobName) {
87 | fetch(`/api/jobs/${jobName}`)
88 | .then(response => response.json())
89 | .then(data => {
90 | if (data.active) {
91 | document
92 | .getElementById("schedule-badge-" + jobName)
93 | .setAttribute("class", "schedule-badge-active-true");
94 | } else {
95 | document
96 | .getElementById("schedule-badge-" + jobName)
97 | .setAttribute("class", "schedule-badge-active-false");
98 | }
99 | })
100 | }
101 |
102 | function stateColor(taskState) {
103 | switch (taskState) {
104 | case "running":
105 | var color = "#dffbe3";
106 | break;
107 | case "upforretry":
108 | var color = "#ffc620";
109 | break;
110 | case "successful":
111 | var color = "#39c84e";
112 | break;
113 | case "skipped":
114 | var color = "#abbefb";
115 | break;
116 | case "failed":
117 | var color = "#ff4020";
118 | break;
119 | case "notstarted":
120 | var color = "white";
121 | break;
122 | }
123 |
124 | return color
125 | }
126 |
127 | async function buttonPress(buttonName, jobName) {
128 | var button = document.getElementById(`button-${buttonName}-${jobName}`);
129 | // Add 'clicked' class to apply the style
130 | button.classList.add('clicked');
131 |
132 | const options = {
133 | method: 'POST'
134 | }
135 | await fetch(`/api/jobs/${jobName}/${buttonName}`, options)
136 | .then(updateJobActive(jobName))
137 |
138 | setTimeout(function() {
139 | button.classList.remove('clicked');
140 | }, 200); // 200 milliseconds delay
141 | }
142 |
--------------------------------------------------------------------------------
/job.go:
--------------------------------------------------------------------------------
1 | package goflow
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "sync"
7 | "time"
8 |
9 | "github.com/philippgille/gokv"
10 | )
11 |
12 | // A Job is a workflow consisting of independent and dependent tasks
13 | // organized into a graph.
14 | type Job struct {
15 | Name string
16 | Tasks map[string]*Task
17 | Schedule string
18 | Dag dag
19 | Active bool
20 | state state
21 | tasks []string
22 | sync.RWMutex
23 | }
24 |
25 | // Jobs and tasks are stateful.
26 | type state string
27 |
28 | const (
29 | none state = "notstarted"
30 | running state = "running"
31 | upForRetry state = "upforretry"
32 | skipped state = "skipped"
33 | failed state = "failed"
34 | successful state = "successful"
35 | )
36 |
37 | func (j *Job) loadState() state {
38 | if !j.allDone() {
39 | j.storeState(running)
40 | }
41 | if j.allSuccessful() {
42 | j.storeState(successful)
43 | }
44 | if j.allDone() && j.anyFailed() {
45 | j.storeState(failed)
46 | }
47 | return j.state
48 | }
49 |
50 | func (j *Job) loadTaskState(task string) state {
51 | j.RLock()
52 | result := none
53 | for _, t := range j.Tasks {
54 | if t.Name == task {
55 | result = t.state
56 | break
57 | }
58 | }
59 | j.RUnlock()
60 | return result
61 | }
62 |
63 | func (j *Job) storeState(value state) {
64 | j.Lock()
65 | j.state = value
66 | j.Unlock()
67 | }
68 |
69 | func (j *Job) storeTaskState(task string, value state) {
70 | j.Lock()
71 | for _, t := range j.Tasks {
72 | if t.Name == task {
73 | t.state = value
74 | }
75 | }
76 | j.Unlock()
77 | }
78 |
79 | type writeOp struct {
80 | key string
81 | val state
82 | }
83 |
84 | // Initialize a job.
85 | func (j *Job) initialize() *Job {
86 | j.Dag = make(dag)
87 | j.Tasks = make(map[string]*Task)
88 | j.tasks = make([]string, 0)
89 | j.storeState(none)
90 | return j
91 | }
92 |
93 | // Add a task to a job.
94 | func (j *Job) Add(t *Task) *Job {
95 | if j.Dag == nil {
96 | j.initialize()
97 | }
98 |
99 | if !(t.TriggerRule == allDone || t.TriggerRule == allSuccessful) {
100 | t.TriggerRule = allSuccessful
101 | }
102 |
103 | t.remaining = t.Retries
104 |
105 | j.Tasks[t.Name] = t
106 | j.tasks = append(j.tasks, t.Name)
107 | j.Dag.addNode(t.Name)
108 | j.storeTaskState(t.Name, none)
109 | return j
110 | }
111 |
112 | // Task getter
113 | func (j *Job) Task(name string) *Task {
114 | return j.Tasks[name]
115 | }
116 |
117 | // SetDownstream sets a dependency relationship between two tasks in the job.
118 | // The dependent task is downstream of the independent task and
119 | // waits for the independent task to finish before starting
120 | // execution.
121 | func (j *Job) SetDownstream(ind, dep *Task) *Job {
122 | j.Dag.setDownstream(ind.Name, dep.Name)
123 | return j
124 | }
125 |
126 | func (j *Job) run(store gokv.Store, e *execution) error {
127 |
128 | if !j.Dag.validate() {
129 | return fmt.Errorf("Invalid Dag for job %s", j.Name)
130 | }
131 |
132 | log.Printf("jobID=%v, jobname=%v, msg=starting", e.ID, j.Name)
133 |
134 | writes := make(chan writeOp)
135 |
136 | for {
137 | for _, task := range j.Tasks {
138 |
139 | // Start the independent tasks
140 | v := j.loadTaskState(task.Name)
141 | if v == none && !j.Dag.isDownstream(task.Name) {
142 | j.storeTaskState(task.Name, running)
143 | log.Printf("jobID=%v, job=%v, task=%v, msg=starting", e.ID, j.Name, task.Name)
144 | go task.run(writes)
145 | }
146 |
147 | // Start the tasks that need to be re-tried
148 | if v == upForRetry {
149 | task.RetryDelay.wait(task.Name, task.Retries-task.remaining)
150 | task.remaining = task.remaining - 1
151 | j.storeTaskState(task.Name, running)
152 | log.Printf("jobID=%v, job=%v, task=%v, msg=starting", e.ID, j.Name, task.Name)
153 | go task.run(writes)
154 | }
155 |
156 | // If dependencies are done, start the dependent tasks
157 | if v == none && j.Dag.isDownstream(task.Name) {
158 | upstreamDone := true
159 | upstreamSuccessful := true
160 | for _, us := range j.Dag.dependencies(task.Name) {
161 | w := j.loadTaskState(us)
162 | if w == none || w == running || w == upForRetry {
163 | upstreamDone = false
164 | }
165 | if w != successful {
166 | upstreamSuccessful = false
167 | }
168 | }
169 |
170 | if upstreamDone && task.TriggerRule == allDone {
171 | j.storeTaskState(task.Name, running)
172 | log.Printf("jobID=%v, job=%v, task=%v, msg=starting", e.ID, j.Name, task.Name)
173 | go task.run(writes)
174 | }
175 |
176 | if upstreamSuccessful && task.TriggerRule == allSuccessful {
177 | j.storeTaskState(task.Name, running)
178 | log.Printf("jobID=%v, job=%v, task=%v, msg=starting", e.ID, j.Name, task.Name)
179 | go task.run(writes)
180 | }
181 |
182 | if upstreamDone && !upstreamSuccessful && task.TriggerRule == allSuccessful {
183 | j.storeTaskState(task.Name, skipped)
184 | log.Printf("jobID=%v, job=%v, task=%v, msg=skipping", e.ID, j.Name, task.Name)
185 | go task.skip(writes)
186 | }
187 |
188 | }
189 | }
190 |
191 | // Receive updates on task state
192 | write := <-writes
193 | j.storeTaskState(write.key, write.val)
194 | log.Printf("jobID=%v, job=%v, task=%v, msg=%v", e.ID, j.Name, write.key, write.val)
195 |
196 | // Sync to store
197 | e.State = j.loadState()
198 | e.ModifiedTimestamp = time.Now().UTC().Format(time.RFC3339Nano)
199 | syncStateToStore(store, e, write.key, write.val)
200 |
201 | if j.allDone() {
202 | break
203 | }
204 | }
205 |
206 | log.Printf("jobID=%v, job=%v, msg=%v", e.ID, j.Name, j.loadState())
207 |
208 | return nil
209 | }
210 |
211 | func (j *Job) allDone() bool {
212 | j.RLock()
213 | out := true
214 | for _, t := range j.Tasks {
215 | if t.state == none || t.state == running || t.state == upForRetry {
216 | out = false
217 | }
218 | }
219 | j.RUnlock()
220 | return out
221 | }
222 |
223 | func (j *Job) allSuccessful() bool {
224 | j.RLock()
225 | out := true
226 | for _, t := range j.Tasks {
227 | if t.state != successful {
228 | out = false
229 | }
230 | }
231 | j.RUnlock()
232 | return out
233 | }
234 |
235 | func (j *Job) anyFailed() bool {
236 | j.RLock()
237 | out := false
238 | for _, t := range j.Tasks {
239 | if t.state == failed {
240 | out = true
241 | }
242 | }
243 | j.RUnlock()
244 | return out
245 | }
246 |
--------------------------------------------------------------------------------
/routes.go:
--------------------------------------------------------------------------------
1 | package goflow
2 |
3 | import (
4 | "net/http"
5 | "time"
6 |
7 | "github.com/gin-gonic/gin"
8 | )
9 |
10 | func (g *Goflow) addStaticRoutes() *Goflow {
11 | g.router.Static("/css", g.Options.UIPath+"css")
12 | g.router.Static("/dist", g.Options.UIPath+"dist")
13 | g.router.Static("/src", g.Options.UIPath+"src")
14 | g.router.LoadHTMLGlob(g.Options.UIPath + "html/*.html.tmpl")
15 | return g
16 | }
17 |
18 | func (g *Goflow) addStreamRoute(keepOpen bool) *Goflow {
19 | g.router.GET("/stream", g.stream(keepOpen))
20 | return g
21 | }
22 |
23 | type jobrun struct {
24 | JobName string `json:"job"`
25 | Submitted string `json:"submitted"`
26 | JobState jobstate `json:"state"`
27 | }
28 |
29 | type jobstate struct {
30 | State state `json:"job"`
31 | TaskState taskstate `json:"tasks"`
32 | }
33 |
34 | type taskstate struct {
35 | Taskstate map[string]state `json:"state"`
36 | }
37 |
38 | func (g *Goflow) addAPIRoutes() *Goflow {
39 | api := g.router.Group("/api")
40 | {
41 | api.GET("/health", func(c *gin.Context) {
42 | var msg struct {
43 | Health string `json:"health"`
44 | }
45 | msg.Health = "OK"
46 | c.JSON(http.StatusOK, msg)
47 | })
48 |
49 | api.GET("/jobs", func(c *gin.Context) {
50 | var msg struct {
51 | Jobs []string `json:"jobs"`
52 | }
53 | msg.Jobs = g.jobs
54 | c.JSON(http.StatusOK, msg)
55 | })
56 |
57 | // Deprecated: will be removed in v3.0.0
58 | api.GET("/jobruns", func(c *gin.Context) {
59 | jobName := c.Query("jobname")
60 | stateQuery := c.Query("state")
61 |
62 | jobruns := make([]jobrun, 0)
63 |
64 | for job := range g.Jobs {
65 | stored, _ := readExecutions(g.Store, job)
66 | for _, execution := range stored {
67 | if stateQuery != "" && stateQuery != string(execution.State) {
68 | } else if jobName != "" && jobName != execution.JobName {
69 | } else {
70 |
71 | t := taskstate{make(map[string]state, 0)}
72 |
73 | for _, task := range execution.TaskExecutions {
74 | t.Taskstate[task.Name] = task.State
75 | }
76 |
77 | j := jobrun{
78 | JobName: job,
79 | Submitted: execution.StartedAt,
80 | JobState: jobstate{
81 | State: execution.State,
82 | TaskState: t,
83 | },
84 | }
85 |
86 | jobruns = append(jobruns, j)
87 | }
88 | }
89 | }
90 |
91 | var msg struct {
92 | Jobruns []jobrun `json:"jobruns"`
93 | }
94 | msg.Jobruns = jobruns
95 |
96 | c.JSON(http.StatusOK, msg)
97 | })
98 |
99 | api.GET("/executions", func(c *gin.Context) {
100 | jobName := c.Query("jobname")
101 | stateQuery := c.Query("state")
102 |
103 | executions := make([]*execution, 0)
104 |
105 | for job := range g.Jobs {
106 | stored, _ := readExecutions(g.Store, job)
107 | for _, execution := range stored {
108 | if stateQuery != "" && stateQuery != string(execution.State) {
109 | } else if jobName != "" && jobName != execution.JobName {
110 | } else {
111 | executions = append(executions, execution)
112 | }
113 | }
114 | }
115 |
116 | var msg struct {
117 | Executions []*execution `json:"executions"`
118 | }
119 | msg.Executions = executions
120 |
121 | c.JSON(http.StatusOK, msg)
122 | })
123 |
124 | api.GET("/jobs/:name", func(c *gin.Context) {
125 | name := c.Param("name")
126 | jobFn, ok := g.Jobs[name]
127 |
128 | var msg struct {
129 | JobName string `json:"job"`
130 | TaskNames []string `json:"tasks"`
131 | Dag dag `json:"dag"`
132 | Schedule string `json:"schedule"`
133 | Active bool `json:"active"`
134 | }
135 |
136 | if ok {
137 | msg.JobName = name
138 | msg.TaskNames = jobFn().tasks
139 | msg.Dag = jobFn().Dag
140 | msg.Schedule = g.Jobs[name]().Schedule
141 |
142 | // check if the job is active by looking in the list of cron entries
143 | for _, entry := range g.cron.Entries() {
144 | if jobName := entry.Job.(*scheduledExecution).jobFunc().Name; name == jobName {
145 | msg.Active = true
146 | }
147 | }
148 |
149 | c.JSON(http.StatusOK, msg)
150 | } else {
151 | c.JSON(http.StatusNotFound, msg)
152 | }
153 | })
154 |
155 | api.POST("/jobs/:name/submit", func(c *gin.Context) {
156 | name := c.Param("name")
157 | _, ok := g.Jobs[name]
158 |
159 | var msg struct {
160 | Job string `json:"job"`
161 | Success bool `json:"success"`
162 | Submitted string `json:"submitted"`
163 | }
164 | msg.Job = name
165 |
166 | if ok {
167 | g.execute(name)
168 | msg.Success = true
169 | msg.Submitted = time.Now().UTC().Format(time.RFC3339Nano)
170 | c.JSON(http.StatusOK, msg)
171 | } else {
172 | msg.Success = false
173 | c.JSON(http.StatusNotFound, msg)
174 | }
175 | })
176 |
177 | api.POST("/jobs/:name/toggle", func(c *gin.Context) {
178 | name := c.Param("name")
179 | _, ok := g.Jobs[name]
180 |
181 | var msg struct {
182 | Job string `json:"job"`
183 | Success bool `json:"success"`
184 | Active bool `json:"active"`
185 | }
186 | msg.Job = name
187 |
188 | if ok {
189 | isActive, _ := g.toggle(name)
190 | msg.Success = true
191 | msg.Active = isActive
192 | c.JSON(http.StatusOK, msg)
193 | } else {
194 | msg.Success = false
195 | c.JSON(http.StatusNotFound, msg)
196 | }
197 | })
198 | }
199 |
200 | return g
201 | }
202 |
203 | func (g *Goflow) addUIRoutes() *Goflow {
204 | ui := g.router.Group("/ui")
205 | {
206 | ui.GET("/", func(c *gin.Context) {
207 | jobs := make([]*Job, 0)
208 | for _, job := range g.jobs {
209 |
210 | // create the job, assume it's inactive
211 | j := g.Jobs[job]()
212 | j.Active = false
213 |
214 | // check if the job is active by looking in the list of cron entries
215 | for _, entry := range g.cron.Entries() {
216 | if name := entry.Job.(*scheduledExecution).jobFunc().Name; name == j.Name {
217 | j.Active = true
218 | }
219 | }
220 |
221 | jobs = append(jobs, j)
222 | }
223 | c.HTML(http.StatusOK, "index.html.tmpl", gin.H{"jobs": jobs})
224 | })
225 |
226 | ui.GET("/jobs/:name", func(c *gin.Context) {
227 | name := c.Param("name")
228 | jobFn, ok := g.Jobs[name]
229 |
230 | if ok {
231 | c.HTML(http.StatusOK, "job.html.tmpl", gin.H{
232 | "jobName": name,
233 | "taskNames": jobFn().tasks,
234 | "schedule": g.Jobs[name]().Schedule,
235 | })
236 | } else {
237 | c.String(http.StatusNotFound, "Not found")
238 | }
239 | })
240 | }
241 |
242 | g.router.GET("/", func(c *gin.Context) {
243 | c.Redirect(http.StatusFound, "/ui/")
244 | })
245 |
246 | return g
247 | }
248 |
--------------------------------------------------------------------------------
/goflow_test.go:
--------------------------------------------------------------------------------
1 | package goflow
2 |
3 | import (
4 | "net/http"
5 | "net/http/httptest"
6 | "testing"
7 |
8 | "github.com/gin-gonic/gin"
9 | "github.com/philippgille/gokv/gomap"
10 | )
11 |
12 | var router = exampleRouter()
13 |
14 | type TestResponseRecorder struct {
15 | *httptest.ResponseRecorder
16 | closeChannel chan bool
17 | }
18 |
19 | func (r *TestResponseRecorder) CloseNotify() <-chan bool {
20 | return r.closeChannel
21 | }
22 |
23 | func CreateTestResponseRecorder() *TestResponseRecorder {
24 | return &TestResponseRecorder{
25 | httptest.NewRecorder(),
26 | make(chan bool, 1),
27 | }
28 | }
29 |
30 | func TestIndexRoute(t *testing.T) {
31 | var w = httptest.NewRecorder()
32 | req, _ := http.NewRequest("GET", "/ui/", nil)
33 | router.ServeHTTP(w, req)
34 |
35 | if w.Code != http.StatusOK {
36 | t.Errorf("/ui/ status is %d, expected %d", w.Code, http.StatusOK)
37 | }
38 |
39 | req, _ = http.NewRequest("GET", "/", nil)
40 | router.ServeHTTP(w, req)
41 |
42 | if w.Code != http.StatusOK {
43 | t.Errorf("/ status is %d, expected %d", w.Code, http.StatusOK)
44 | }
45 | }
46 |
47 | func TestHealthRoute(t *testing.T) {
48 | var w = httptest.NewRecorder()
49 | req, _ := http.NewRequest("GET", "/api/health", nil)
50 | router.ServeHTTP(w, req)
51 |
52 | if w.Code != http.StatusOK {
53 | t.Errorf("httpStatus is %d, expected %d", w.Code, http.StatusOK)
54 | }
55 | }
56 |
57 | func TestJobsRoute(t *testing.T) {
58 | var w = httptest.NewRecorder()
59 | req, _ := http.NewRequest("GET", "/api/jobs", nil)
60 | router.ServeHTTP(w, req)
61 |
62 | if w.Code != http.StatusOK {
63 | t.Errorf("httpStatus is %d, expected %d", w.Code, http.StatusOK)
64 | }
65 |
66 | req, _ = http.NewRequest("GET", "/api/jobs/example-complex-analytics", nil)
67 | router.ServeHTTP(w, req)
68 |
69 | if w.Code != http.StatusOK {
70 | t.Errorf("httpStatus is %d, expected %d", w.Code, http.StatusOK)
71 | }
72 | }
73 |
74 | func TestJobRunsRoute(t *testing.T) {
75 | var w = httptest.NewRecorder()
76 | req, _ := http.NewRequest("GET", "/api/jobruns", nil)
77 | router.ServeHTTP(w, req)
78 |
79 | if w.Code != http.StatusOK {
80 | t.Errorf("httpStatus is %d, expected %d", w.Code, http.StatusOK)
81 | }
82 | }
83 |
84 | func TestExecutionsRoute(t *testing.T) {
85 | var w = httptest.NewRecorder()
86 | req, _ := http.NewRequest("GET", "/api/executions", nil)
87 | router.ServeHTTP(w, req)
88 |
89 | if w.Code != http.StatusOK {
90 | t.Errorf("httpStatus is %d, expected %d", w.Code, http.StatusOK)
91 | }
92 | }
93 |
94 | func TestJobSubmitToRouter(t *testing.T) {
95 | var w = httptest.NewRecorder()
96 | req, _ := http.NewRequest("POST", "/api/jobs/example-complex-analytics/submit", nil)
97 |
98 | router.ServeHTTP(w, req)
99 |
100 | if w.Code != http.StatusOK {
101 | t.Errorf("httpStatus is %d, expected %d", w.Code, http.StatusOK)
102 | }
103 |
104 | w = httptest.NewRecorder()
105 | req, _ = http.NewRequest("POST", "/api/jobs/example-custom-operator/submit", nil)
106 | router.ServeHTTP(w, req)
107 |
108 | if w.Code != http.StatusOK {
109 | t.Errorf("httpStatus is %d, expected %d", w.Code, http.StatusOK)
110 | }
111 |
112 | w = httptest.NewRecorder()
113 | req, _ = http.NewRequest("POST", "/api/jobs/bla/submit", nil)
114 | router.ServeHTTP(w, req)
115 |
116 | if w.Code != http.StatusNotFound {
117 | t.Errorf("httpStatus is %d, expected %d", w.Code, http.StatusNotFound)
118 | }
119 | }
120 |
121 | func TestJobToggleActiveRoute(t *testing.T) {
122 | var w = httptest.NewRecorder()
123 | req, _ := http.NewRequest("POST", "/api/jobs/example-complex-analytics/toggle", nil)
124 | router.ServeHTTP(w, req)
125 |
126 | if w.Code != http.StatusOK {
127 | t.Errorf("httpStatus is %d, expected %d", w.Code, http.StatusOK)
128 | }
129 |
130 | req, _ = http.NewRequest("POST", "/api/jobs/example-custom-operator/toggle", nil)
131 | router.ServeHTTP(w, req)
132 |
133 | if w.Code != http.StatusOK {
134 | t.Errorf("httpStatus is %d, expected %d", w.Code, http.StatusOK)
135 | }
136 |
137 | w = httptest.NewRecorder()
138 | req, _ = http.NewRequest("POST", "/api/jobs/bla/toggle", nil)
139 | router.ServeHTTP(w, req)
140 |
141 | if w.Code != http.StatusNotFound {
142 | t.Errorf("httpStatus is %d, expected %d", w.Code, http.StatusNotFound)
143 | }
144 | }
145 |
146 | func TestRouteNotFound(t *testing.T) {
147 | var w = httptest.NewRecorder()
148 | req, _ := http.NewRequest("GET", "/blaaaa", nil)
149 | router.ServeHTTP(w, req)
150 |
151 | if w.Code != http.StatusNotFound {
152 | t.Errorf("httpStatus is %d, expected %d", w.Code, http.StatusNotFound)
153 | }
154 |
155 | req, _ = http.NewRequest("GET", "/api/jobs/blaaaa", nil)
156 | router.ServeHTTP(w, req)
157 |
158 | if w.Code != http.StatusNotFound {
159 | t.Errorf("httpStatus is %d, expected %d", w.Code, http.StatusNotFound)
160 | }
161 |
162 | req, _ = http.NewRequest("GET", "/ui/jobs/blaaaa", nil)
163 | router.ServeHTTP(w, req)
164 |
165 | if w.Code != http.StatusNotFound {
166 | t.Errorf("httpStatus is %d, expected %d", w.Code, http.StatusNotFound)
167 | }
168 | }
169 |
170 | func TestJobOverviewRoute(t *testing.T) {
171 | var w = httptest.NewRecorder()
172 | req, _ := http.NewRequest("GET", "/ui/jobs/example-complex-analytics", nil)
173 | router.ServeHTTP(w, req)
174 |
175 | if w.Code != http.StatusOK {
176 | t.Errorf("httpStatus is %d, expected %d", w.Code, http.StatusOK)
177 | }
178 |
179 | w = httptest.NewRecorder()
180 | req, _ = http.NewRequest("GET", "/jobs/bla", nil)
181 | router.ServeHTTP(w, req)
182 |
183 | if w.Code != http.StatusNotFound {
184 | t.Errorf("httpStatus is %d, expected %d", w.Code, http.StatusNotFound)
185 | }
186 | }
187 |
188 | func TestStreamRoute(t *testing.T) {
189 | var w = CreateTestResponseRecorder()
190 | req, _ := http.NewRequest("GET", "/stream", nil)
191 | router.ServeHTTP(w, req)
192 |
193 | if w.Code != http.StatusOK {
194 | t.Errorf("httpStatus is %d, expected %d", w.Code, http.StatusOK)
195 | }
196 |
197 | w = CreateTestResponseRecorder()
198 | req, _ = http.NewRequest("GET", "/stream?jobname=example-complex-analytics", nil)
199 | router.ServeHTTP(w, req)
200 |
201 | if w.Code != http.StatusOK {
202 | t.Errorf("httpStatus is %d, expected %d", w.Code, http.StatusOK)
203 | }
204 | }
205 |
206 | // check for a race against /stream
207 | func TestToggleRaceCondition(t *testing.T) {
208 | var w = httptest.NewRecorder()
209 | req, _ := http.NewRequest("POST", "/api/jobs/example-complex-analytics/toggle", nil)
210 | router.ServeHTTP(w, req)
211 | }
212 |
213 | func exampleRouter() *gin.Engine {
214 | g := New(Options{UIPath: "ui/", ShowExamples: true, WithSeconds: true})
215 | g.execute("example-custom-operator")
216 | g.Use(DefaultLogger())
217 | g.addStaticRoutes()
218 | g.addStreamRoute(false)
219 | g.addUIRoutes()
220 | g.addAPIRoutes()
221 | return g.router
222 | }
223 |
224 | func TestScheduledExecution(t *testing.T) {
225 | store := gomap.NewStore(gomap.DefaultOptions)
226 | schedExec := scheduledExecution{store, customOperatorJob}
227 | schedExec.Run()
228 | }
229 |
230 | func TestGoflowWithoutOptions(t *testing.T) {
231 | g := New(Options{})
232 | g.Use(DefaultLogger())
233 | }
234 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
2 | github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
3 | github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
4 | github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
5 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
6 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
7 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
8 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
9 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
10 | github.com/ef-ds/deque v1.0.4 h1:iFAZNmveMT9WERAkqLJ+oaABF9AcVQ5AjXem/hroniI=
11 | github.com/ef-ds/deque v1.0.4/go.mod h1:gXDnTC3yqvBcHbq2lcExjtAcVrOnJCbMcZXmuj8Z4tg=
12 | github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
13 | github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
14 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
15 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
16 | github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
17 | github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
18 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
19 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
20 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
21 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
22 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
23 | github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
24 | github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
25 | github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg=
26 | github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
27 | github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
28 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
29 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
30 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
31 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
32 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
33 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
34 | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
35 | github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
36 | github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
37 | github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
38 | github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
39 | github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
40 | github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
41 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
42 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
43 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
44 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
45 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
46 | github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
47 | github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
48 | github.com/philippgille/gokv v0.7.0 h1:rQSIQspete82h78Br7k7rKUZ8JYy/hWlwzm/W5qobPI=
49 | github.com/philippgille/gokv v0.7.0/go.mod h1:OwiTP/3bhEBhSuOmFmq1+rszglfSgjJVxd1HOgOa2N4=
50 | github.com/philippgille/gokv/encoding v0.7.0 h1:2oxepKzzTsi00iLZBCZ7Rmqrallh9zws3iqSrLGfkgo=
51 | github.com/philippgille/gokv/encoding v0.7.0/go.mod h1:yncOBBUciyniPI8t5ECF8XSCwhONE9Rjf3My5IHs3fA=
52 | github.com/philippgille/gokv/gomap v0.7.0 h1:RR+cgJl1aMxw8CkxGczRwCbC42tHJ7cRwaaD4Ycgg9k=
53 | github.com/philippgille/gokv/gomap v0.7.0/go.mod h1:HJ+PC2y/knRG2RrdH81N+BkjDhmbPQMUj+tRHgarvSg=
54 | github.com/philippgille/gokv/test v0.7.0 h1:0wBKnKaFZlSeHxLXcmUJqK//IQGUMeu+o8B876KCiOM=
55 | github.com/philippgille/gokv/util v0.7.0 h1:5avUK/a3aSj/aWjhHv4/FkqgMon2B7k2BqFgLcR+DYg=
56 | github.com/philippgille/gokv/util v0.7.0/go.mod h1:i9KLHbPxGiHLMhkix/CcDQhpPbCkJy5BkW+RKgwDHMo=
57 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
58 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
59 | github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
60 | github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
61 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
62 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
63 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
64 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
65 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
66 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
67 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
68 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
69 | github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
70 | github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
71 | github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
72 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
73 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
74 | github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
75 | github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
76 | golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
77 | golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
78 | golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
79 | golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
80 | golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
81 | golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
82 | golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
83 | golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
84 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
85 | golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
86 | golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
87 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
88 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
89 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
90 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
91 | google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
92 | google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
93 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
94 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
95 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
96 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
97 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
98 | rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
99 |
--------------------------------------------------------------------------------
/swagger.json:
--------------------------------------------------------------------------------
1 | {
2 | "openapi": "3.0.0",
3 | "info": {
4 | "title": "Goflow API",
5 | "version": "2.0.0"
6 | },
7 | "paths": {
8 | "/api/health": {
9 | "get": {
10 | "operationId": "health",
11 | "summary": "check health of the service",
12 | "responses": {
13 | "200": {
14 | "description": "200 response",
15 | "content": {
16 | "application/json": {
17 | "examples": {
18 | "OK": {
19 | "value": {
20 | "health": "OK"
21 | }
22 | }
23 | }
24 | }
25 | }
26 | }
27 | }
28 | }
29 | },
30 | "/api/jobs": {
31 | "get": {
32 | "operationId": "listJobs",
33 | "summary": "list jobs",
34 | "responses": {
35 | "200": {
36 | "description": "200 response",
37 | "content": {
38 | "application/json": {
39 | "examples": {
40 | "exampleJobs": {
41 | "value": {
42 | "jobs": [
43 | "exampleComplexAnalytics",
44 | "exampleCustomOperator"
45 | ]
46 | }
47 | }
48 | }
49 | }
50 | }
51 | }
52 | }
53 | }
54 | },
55 | "/api/jobs/{jobname}": {
56 | "get": {
57 | "operationId": "jobDetails",
58 | "summary": "get the details for a given job",
59 | "parameters": [
60 | {
61 | "in": "path",
62 | "name": "jobname"
63 | }
64 | ],
65 | "responses": {
66 | "200": {
67 | "description": "200 response",
68 | "content": {
69 | "application/json": {
70 | "examples": {
71 | "complexAnalyticsJob": {
72 | "value": {
73 | "job": "exampleComplexAnalytics",
74 | "tasks": [
75 | "sleepOne",
76 | "addTwoFour",
77 | "addThreeFour",
78 | "whoopsWithConstantDelay",
79 | "whoopsWithExponentialBackoff",
80 | "totallySkippable",
81 | "cleanUp",
82 | "addOneOne",
83 | "sleepTwo"
84 | ],
85 | "dag": {
86 | "addOneOne": [
87 | "sleepTwo",
88 | "addThreeFour"
89 | ],
90 | "addThreeFour": [],
91 | "addTwoFour": [],
92 | "cleanUp": [],
93 | "sleepOne": [
94 | "addOneOne",
95 | "whoopsWithConstantDelay",
96 | "whoopsWithExponentialBackoff"
97 | ],
98 | "sleepTwo": [
99 | "addTwoFour"
100 | ],
101 | "totallySkippable": [
102 | "cleanUp"
103 | ],
104 | "whoopsWithConstantDelay": [
105 | "totallySkippable"
106 | ],
107 | "whoopsWithExponentialBackoff": [
108 | "totallySkippable"
109 | ]
110 | },
111 | "schedule": "* * * * *",
112 | "active": false
113 | }
114 | }
115 | }
116 | }
117 | }
118 | }
119 | }
120 | }
121 | },
122 | "/api/jobruns": {
123 | "get": {
124 | "operationId": "listJobRuns",
125 | "summary": "query and list job runs",
126 | "parameters": [
127 | {
128 | "in": "query",
129 | "name": "jobname",
130 | "schema": {
131 | "type": "string"
132 | },
133 | "description": "(optional) the job name"
134 | },
135 | {
136 | "in": "query",
137 | "name": "state",
138 | "schema": {
139 | "type": "string"
140 | },
141 | "description": "(optional) the job state, valid values are [running, failed, successful]"
142 | }
143 | ],
144 | "responses": {
145 | "200": {
146 | "description": "200 response",
147 | "content": {
148 | "application/json": {
149 | "examples": {
150 | "exampleJobRuns": {
151 | "value": {
152 | "jobruns": [
153 | {
154 | "job": "exampleComplexAnalytics",
155 | "submitted": "2023-06-24T07:23:11.208506156Z",
156 | "state": {
157 | "job": "running",
158 | "tasks": {
159 | "state": {
160 | "addOneOne": "running",
161 | "addThreeFour": "notstarted",
162 | "addTwoFour": "notstarted",
163 | "cleanUp": "notstarted",
164 | "sleepOne": "successful",
165 | "sleepTwo": "notstarted",
166 | "totallySkippable": "notstarted",
167 | "whoopsWithConstantDelay": "running",
168 | "whoopsWithExponentialBackoff": "upforretry"
169 | }
170 | }
171 | }
172 | }
173 | ]
174 | }
175 | }
176 | }
177 | }
178 | }
179 | }
180 | }
181 | }
182 | },
183 | "/api/executions": {
184 | "get": {
185 | "operationId": "listExecutions",
186 | "summary": "query and list job executions",
187 | "parameters": [
188 | {
189 | "in": "query",
190 | "name": "jobname",
191 | "schema": {
192 | "type": "string"
193 | },
194 | "description": "(optional) the job name"
195 | },
196 | {
197 | "in": "query",
198 | "name": "state",
199 | "schema": {
200 | "type": "string"
201 | },
202 | "description": "(optional) the job state, valid values are [running, failed, successful]"
203 | }
204 | ],
205 | "responses": {
206 | "200": {
207 | "description": "200 response",
208 | "content": {
209 | "application/json": {
210 | "examples": {
211 | "exampleJobRuns": {
212 | "value": {
213 | "executions": [
214 | {
215 | "id": "b43e5f75-aa2a-4859-b6b9-f551ca258196",
216 | "job": "example-complex-analytics",
217 | "submitted": "2024-02-03T13:26:42.038130297Z",
218 | "state": "failed",
219 | "tasks": [
220 | {
221 | "name": "sleep-one",
222 | "state": "successful"
223 | },
224 | {
225 | "name": "add-one-one",
226 | "state": "successful"
227 | },
228 | {
229 | "name": "sleep-two",
230 | "state": "successful"
231 | },
232 | {
233 | "name": "add-two-four",
234 | "state": "successful"
235 | },
236 | {
237 | "name": "add-three-four",
238 | "state": "successful"
239 | },
240 | {
241 | "name": "whoops-with-constant-delay",
242 | "state": "failed"
243 | },
244 | {
245 | "name": "whoops-with-exponential-backoff",
246 | "state": "failed"
247 | },
248 | {
249 | "name": "totally-skippable",
250 | "state": "skipped"
251 | },
252 | {
253 | "name": "clean-up",
254 | "state": "successful"
255 | }
256 | ]
257 | }
258 | ]
259 | }
260 | }
261 | }
262 | }
263 | }
264 | }
265 | }
266 | }
267 | },
268 | "/api/jobs/{jobname}/submit": {
269 | "post": {
270 | "operationId": "submitJob",
271 | "summary": "submit a job for execution",
272 | "parameters": [
273 | {
274 | "in": "path",
275 | "name": "jobname"
276 | }
277 | ],
278 | "responses": {
279 | "200": {
280 | "description": "200 response",
281 | "content": {
282 | "application/json": {
283 | "examples": {
284 | "customOperator": {
285 | "value": {
286 | "job": "exampleCustomOperator",
287 | "success": true,
288 | "submitted": "2023-06-21T15:02:39.943428403Z"
289 | }
290 | }
291 | }
292 | }
293 | }
294 | }
295 | }
296 | }
297 | },
298 | "/api/jobs/{jobname}/toggle": {
299 | "post": {
300 | "operationId": "toggleJobSchedule",
301 | "summary": "toggle a job schedule on or off",
302 | "parameters": [
303 | {
304 | "in": "path",
305 | "name": "jobname"
306 | }
307 | ],
308 | "responses": {
309 | "200": {
310 | "description": "200 response",
311 | "content": {
312 | "application/json": {
313 | "examples": {
314 | "customOperator": {
315 | "value": {
316 | "job": "exampleCustomOperator",
317 | "success": true,
318 | "active": true
319 | }
320 | }
321 | }
322 | }
323 | }
324 | }
325 | }
326 | }
327 | }
328 | }
329 | }
330 |
--------------------------------------------------------------------------------
/Readme.md:
--------------------------------------------------------------------------------
1 | 
2 | [](https://codecov.io/gh/fieldryand/goflow)
3 | [](https://goreportcard.com/report/github.com/fieldryand/goflow)
4 | [](https://pkg.go.dev/github.com/fieldryand/goflow/v2?tab=doc)
5 | [](https://github.com/fieldryand/goflow/releases)
6 |
7 | # Goflow
8 |
9 | A simple but powerful DAG scheduler and dashboard, written in Go.
10 |
11 | 
12 |
13 | ------
14 |
15 | **Use it if:**
16 | - you need a directed acyclic graph (DAG) scheduler like Apache Airflow, but without the complexity.
17 | - you have a variety of clusters or services performing heavy computations and you want something small and light to orchestrate them.
18 | - you want a monitoring dashboard.
19 | - you want the easiest possible deployment with a single binary or container, saving you time. Volume mounts etc are too much headache.
20 | - you want it to run on a single tiny VM, saving on cloud costs.
21 | - you want to choose your storage technology--embedded, Postgres, Redis, S3, DynamoDB or something else.
22 | - you prefer to define your DAGs with code rather than configuration files. This approach can make it easier to manage complex DAGs.
23 |
24 | **Don't use it if:**
25 | - you need to queue a huge number of tasks. Goflow is not tested at massive scale and does not support horizontal scaling.
26 |
27 | ## Contents
28 |
29 | - [Quick start](#quick-start)
30 | - [With Docker](#with-docker)
31 | - [Without Docker](#without-docker)
32 | - [Development overview](#development-overview)
33 | - [Jobs and tasks](#jobs-and-tasks)
34 | - [Custom Operators](#custom-operators)
35 | - [Retries](#retries)
36 | - [Task dependencies](#task-dependencies)
37 | - [Trigger rules](#trigger-rules)
38 | - [The Goflow engine](#the-goflow-engine)
39 | - [Available operators](#available-operators)
40 | - [Storage](#storage)
41 | - [API and integration](#api-and-integration)
42 |
43 | ## Quick start
44 |
45 | ### With Docker
46 |
47 | ```shell
48 | docker run -p 8181:8181 ghcr.io/fieldryand/goflow-example:latest
49 | ```
50 |
51 | Check out the dashboard at `localhost:8181`.
52 |
53 | ### Without Docker
54 |
55 | In a fresh project directory:
56 |
57 | ```shell
58 | go mod init # create a new module
59 | go get github.com/fieldryand/goflow/v2 # install dependencies
60 | ```
61 |
62 | Create a file `main.go` with contents:
63 | ```go
64 | package main
65 |
66 | import "github.com/fieldryand/goflow/v2"
67 |
68 | func main() {
69 | options := goflow.Options{
70 | UIPath: "ui/",
71 | ShowExamples: true,
72 | WithSeconds: true,
73 | }
74 | gf := goflow.New(options)
75 | gf.Use(goflow.DefaultLogger())
76 | gf.Run(":8181")
77 | }
78 | ```
79 |
80 | Download and untar the dashboard:
81 |
82 | ```shell
83 | wget https://github.com/fieldryand/goflow/releases/latest/download/goflow-ui.tar.gz
84 | tar -xvzf goflow-ui.tar.gz
85 | rm goflow-ui.tar.gz
86 | ```
87 |
88 | Now run the application with `go run main.go` and see it in the browser at localhost:8181.
89 |
90 | ## Development overview
91 |
92 | First a few definitions.
93 |
94 | - `Job`: A Goflow workflow is called a `Job`. Jobs can be scheduled using cron syntax.
95 | - `Task`: Each job consists of one or more tasks organized into a dependency graph. A task can be run under certain conditions; by default, a task runs when all of its dependencies finish successfully.
96 | - Concurrency: Jobs and tasks execute concurrently.
97 | - `Operator`: An `Operator` defines the work done by a `Task`. Goflow comes with a handful of basic operators, and implementing your own `Operator` is straightforward.
98 | - Retries: You can allow a `Task` a given number of retry attempts. Goflow comes with two retry strategies, `ConstantDelay` and `ExponentialBackoff`.
99 | - Streaming: Goflow uses server-sent events to stream the status of jobs and tasks to the dashboard in real time.
100 |
101 | ### Jobs and tasks
102 |
103 | Let's start by creating a function that returns a job called `my-job`. There is a single task in this job that sleeps for one second.
104 |
105 | ```go
106 | package main
107 |
108 | import (
109 | "errors"
110 |
111 | "github.com/fieldryand/goflow/v2"
112 | )
113 |
114 | func myJob() *goflow.Job {
115 | j := &goflow.Job{Name: "my-job", Schedule: "* * * * *", Active: true}
116 | j.Add(&goflow.Task{
117 | Name: "sleep-for-one-second",
118 | Operator: goflow.Command{Cmd: "sleep", Args: []string{"1"}},
119 | })
120 | return j
121 | }
122 | ```
123 |
124 | By setting `Active: true`, we are telling Goflow to apply the provided cron schedule for this job when the application starts.
125 | Job scheduling can be activated and deactivated from the dashboard.
126 |
127 | ### Custom operators
128 |
129 | A custom `Operator` needs to implement the `Run` method. Here's an example of an operator that adds two positive numbers.
130 |
131 | ```go
132 | type PositiveAddition struct{ a, b int }
133 |
134 | func (o PositiveAddition) Run() (interface{}, error) {
135 | if o.a < 0 || o.b < 0 {
136 | return 0, errors.New("Can't add negative numbers")
137 | }
138 | result := o.a + o.b
139 | return result, nil
140 | }
141 | ```
142 |
143 | ### Retries
144 |
145 | Let's add a retry strategy to the `sleep-for-one-second` task:
146 |
147 | ```go
148 | func myJob() *goflow.Job {
149 | j := &goflow.Job{Name: "my-job", Schedule: "* * * * *"}
150 | j.Add(&goflow.Task{
151 | Name: "sleep-for-one-second",
152 | Operator: goflow.Command{Cmd: "sleep", Args: []string{"1"}},
153 | Retries: 5,
154 | RetryDelay: goflow.ConstantDelay{Period: 1},
155 | })
156 | return j
157 | }
158 | ```
159 |
160 | Instead of `ConstantDelay`, we could also use `ExponentialBackoff` (see https://en.wikipedia.org/wiki/Exponential_backoff).
161 |
162 | ### Task dependencies
163 |
164 | A job can define a directed acyclic graph (DAG) of independent and dependent tasks. Let's use the `SetDownstream` method to
165 | define two tasks that are dependent on `sleep-for-one-second`. The tasks will use the `PositiveAddition` operator we defined earlier,
166 | as well as a new operator provided by Goflow, `Get`.
167 |
168 | ```go
169 | func myJob() *goflow.Job {
170 | j := &goflow.Job{Name: "my-job", Schedule: "* * * * *"}
171 | j.Add(&goflow.Task{
172 | Name: "sleep-for-one-second",
173 | Operator: goflow.Command{Cmd: "sleep", Args: []string{"1"}},
174 | Retries: 5,
175 | RetryDelay: goflow.ConstantDelay{Period: 1},
176 | })
177 | j.Add(&goflow.Task{
178 | Name: "get-google",
179 | Operator: goflow.Get{Client: &http.Client{}, URL: "https://www.google.com"},
180 | })
181 | j.Add(&goflow.Task{
182 | Name: "add-two-plus-three",
183 | Operator: PositiveAddition{a: 2, b: 3},
184 | })
185 | j.SetDownstream(j.Task("sleep-for-one-second"), j.Task("get-google"))
186 | j.SetDownstream(j.Task("sleep-for-one-second"), j.Task("add-two-plus-three"))
187 | return j
188 | }
189 | ```
190 |
191 | ### Trigger rules
192 |
193 | By default, a task has the trigger rule `allSuccessful`, meaning the task starts executing when all the tasks directly
194 | upstream exit successfully. If any dependency exits with an error, all downstream tasks are skipped, and the job exits with an error.
195 |
196 | Sometimes you want a downstream task to execute even if there are upstream failures. Often these are situations where you want
197 | to perform some cleanup task, such as shutting down a server. In such cases, you can give a task the trigger rule `allDone`.
198 |
199 | Let's modify `sleep-for-one-second` to have the trigger rule `allDone`.
200 |
201 |
202 | ```go
203 | func myJob() *goflow.Job {
204 | // other stuff
205 | j.Add(&goflow.Task{
206 | Name: "sleep-for-one-second",
207 | Operator: goflow.Command{Cmd: "sleep", Args: []string{"1"}},
208 | Retries: 5,
209 | RetryDelay: goflow.ConstantDelay{Period: 1},
210 | TriggerRule: "allDone",
211 | })
212 | // other stuff
213 | }
214 | ```
215 |
216 | ### The Goflow Engine
217 |
218 | Finally, let's create a Goflow engine, register our job, attach a logger, and run the application.
219 |
220 | ```go
221 | func main() {
222 | gf := goflow.New(goflow.Options{Streaming: true})
223 | gf.AddJob(myJob)
224 | gf.Use(goflow.DefaultLogger())
225 | gf.Run(":8181")
226 | }
227 | ```
228 |
229 | You can pass different options to the engine. Options currently supported:
230 | - `Store`: This is [described in more detail below.](#storage)
231 | - `UIPath`: The path to the dashboard code. The default value is an empty string, meaning Goflow serves only the API and not the dashboard. Suggested value if you want the dashboard: `ui/`
232 | - `ShowExamples`: Whether to show the example jobs. Default value: `false`
233 | - `WithSeconds`: Whether to include the seconds field in the cron spec. See the [cron package documentation](https://github.com/robfig/cron) for details. Default value: `false`
234 |
235 | Goflow is built on the [Gin framework](https://github.com/gin-gonic/gin), so you can pass any Gin handler to `Use`.
236 |
237 | ### Available operators
238 |
239 | Goflow provides several operators for common tasks. [See the package documentation](https://pkg.go.dev/github.com/fieldryand/goflow) for details on each.
240 |
241 | - `Command` executes a shell command.
242 | - `Get` makes a GET request.
243 | - `Post` makes a POST request.
244 |
245 | ## Storage
246 |
247 | For persisting your job execution history, Goflow allows you to plug in many different key-value stores thanks to the [excellent gokv package](https://github.com/philippgille/gokv/). This way you can recover from a crash or deploy a new version of your app without losing your data.
248 |
249 | > Note: the gokv API is not yet stable. Goflow has been tested against v0.6.0.
250 |
251 | By default, Goflow uses an in-memory database, but you can easily replace it with Postgres, Redis, S3 or any other `gokv.Store`. Here is an example:
252 |
253 | ```go
254 | package main
255 |
256 | import "github.com/fieldryand/goflow/v2"
257 | import "github.com/philippgille/gokv/redis"
258 |
259 | func main() {
260 | // create a storage client
261 | client, err := redis.NewClient(redis.DefaultOptions)
262 | if err != nil {
263 | panic(err)
264 | }
265 | defer client.Close()
266 |
267 | // pass the client as a Goflow option
268 | options := goflow.Options{
269 | Store: client,
270 | UIPath: "ui/",
271 | Streaming: true,
272 | ShowExamples: true,
273 | }
274 | gf := goflow.New(options)
275 | gf.Use(goflow.DefaultLogger())
276 | gf.Run(":8181")
277 | }
278 | ```
279 |
280 |
281 | ## API and integration
282 |
283 | You can use the API to integrate Goflow with other applications, such as an existing dashboard. Here is an overview of available endpoints:
284 | - `GET /api/health`: Check health of the service
285 | - `GET /api/jobs`: List registered jobs
286 | - `GET /api/jobs/{jobname}`: Get the details for a given job
287 | - `GET /api/executions`: Query and list job executions
288 | - `POST /api/jobs/{jobname}/submit`: Submit a job for execution
289 | - `POST /api/jobs/{jobname}/toggle`: Toggle a job schedule on or off
290 | - `/stream`: This endpoint returns Server-Sent Events with a `data` payload matching the one returned by `/api/executions`. The dashboard that ships with Goflow uses this endpoint.
291 |
292 | Check out the OpenAPI spec for more details. Easiest way is to clone the repo, then within the repo use Swagger as in the following:
293 |
294 | ```shell
295 | docker run -p 8080:8080 -e SWAGGER_JSON=/app/swagger.json -v $(pwd):/app swaggerapi/swagger-ui
296 | ```
297 |
--------------------------------------------------------------------------------
/ui/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "goflow-ui",
3 | "version": "1.0.0",
4 | "lockfileVersion": 3,
5 | "requires": true,
6 | "packages": {
7 | "": {
8 | "name": "goflow-ui",
9 | "version": "1.0.0",
10 | "license": "ISC",
11 | "dependencies": {
12 | "d3-color": "^3.1.0",
13 | "dagre-d3": "^0.6.4",
14 | "webpack": "^5.76.0",
15 | "webpack-cli": "^4.6.0"
16 | }
17 | },
18 | "node_modules/@discoveryjs/json-ext": {
19 | "version": "0.5.2",
20 | "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.2.tgz",
21 | "integrity": "sha512-HyYEUDeIj5rRQU2Hk5HTB2uHsbRQpF70nvMhVzi+VJR0X+xNEhjPui4/kBf3VeH/wqD28PT4sVOm8qqLjBrSZg==",
22 | "engines": {
23 | "node": ">=10.0.0"
24 | }
25 | },
26 | "node_modules/@jridgewell/gen-mapping": {
27 | "version": "0.3.2",
28 | "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz",
29 | "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==",
30 | "dependencies": {
31 | "@jridgewell/set-array": "^1.0.1",
32 | "@jridgewell/sourcemap-codec": "^1.4.10",
33 | "@jridgewell/trace-mapping": "^0.3.9"
34 | },
35 | "engines": {
36 | "node": ">=6.0.0"
37 | }
38 | },
39 | "node_modules/@jridgewell/resolve-uri": {
40 | "version": "3.1.0",
41 | "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz",
42 | "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==",
43 | "engines": {
44 | "node": ">=6.0.0"
45 | }
46 | },
47 | "node_modules/@jridgewell/set-array": {
48 | "version": "1.1.2",
49 | "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz",
50 | "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==",
51 | "engines": {
52 | "node": ">=6.0.0"
53 | }
54 | },
55 | "node_modules/@jridgewell/source-map": {
56 | "version": "0.3.2",
57 | "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz",
58 | "integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==",
59 | "dependencies": {
60 | "@jridgewell/gen-mapping": "^0.3.0",
61 | "@jridgewell/trace-mapping": "^0.3.9"
62 | }
63 | },
64 | "node_modules/@jridgewell/sourcemap-codec": {
65 | "version": "1.4.14",
66 | "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz",
67 | "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw=="
68 | },
69 | "node_modules/@jridgewell/trace-mapping": {
70 | "version": "0.3.17",
71 | "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz",
72 | "integrity": "sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==",
73 | "dependencies": {
74 | "@jridgewell/resolve-uri": "3.1.0",
75 | "@jridgewell/sourcemap-codec": "1.4.14"
76 | }
77 | },
78 | "node_modules/@types/eslint": {
79 | "version": "8.21.2",
80 | "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.21.2.tgz",
81 | "integrity": "sha512-EMpxUyystd3uZVByZap1DACsMXvb82ypQnGn89e1Y0a+LYu3JJscUd/gqhRsVFDkaD2MIiWo0MT8EfXr3DGRKw==",
82 | "dependencies": {
83 | "@types/estree": "*",
84 | "@types/json-schema": "*"
85 | }
86 | },
87 | "node_modules/@types/eslint-scope": {
88 | "version": "3.7.4",
89 | "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.4.tgz",
90 | "integrity": "sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==",
91 | "dependencies": {
92 | "@types/eslint": "*",
93 | "@types/estree": "*"
94 | }
95 | },
96 | "node_modules/@types/estree": {
97 | "version": "0.0.51",
98 | "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.51.tgz",
99 | "integrity": "sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ=="
100 | },
101 | "node_modules/@types/json-schema": {
102 | "version": "7.0.11",
103 | "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz",
104 | "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ=="
105 | },
106 | "node_modules/@types/node": {
107 | "version": "18.15.3",
108 | "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.3.tgz",
109 | "integrity": "sha512-p6ua9zBxz5otCmbpb5D3U4B5Nanw6Pk3PPyX05xnxbB/fRv71N7CPmORg7uAD5P70T0xmx1pzAx/FUfa5X+3cw=="
110 | },
111 | "node_modules/@webassemblyjs/ast": {
112 | "version": "1.11.1",
113 | "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz",
114 | "integrity": "sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==",
115 | "dependencies": {
116 | "@webassemblyjs/helper-numbers": "1.11.1",
117 | "@webassemblyjs/helper-wasm-bytecode": "1.11.1"
118 | }
119 | },
120 | "node_modules/@webassemblyjs/floating-point-hex-parser": {
121 | "version": "1.11.1",
122 | "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz",
123 | "integrity": "sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ=="
124 | },
125 | "node_modules/@webassemblyjs/helper-api-error": {
126 | "version": "1.11.1",
127 | "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz",
128 | "integrity": "sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg=="
129 | },
130 | "node_modules/@webassemblyjs/helper-buffer": {
131 | "version": "1.11.1",
132 | "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz",
133 | "integrity": "sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA=="
134 | },
135 | "node_modules/@webassemblyjs/helper-numbers": {
136 | "version": "1.11.1",
137 | "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz",
138 | "integrity": "sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ==",
139 | "dependencies": {
140 | "@webassemblyjs/floating-point-hex-parser": "1.11.1",
141 | "@webassemblyjs/helper-api-error": "1.11.1",
142 | "@xtuc/long": "4.2.2"
143 | }
144 | },
145 | "node_modules/@webassemblyjs/helper-wasm-bytecode": {
146 | "version": "1.11.1",
147 | "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz",
148 | "integrity": "sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q=="
149 | },
150 | "node_modules/@webassemblyjs/helper-wasm-section": {
151 | "version": "1.11.1",
152 | "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz",
153 | "integrity": "sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg==",
154 | "dependencies": {
155 | "@webassemblyjs/ast": "1.11.1",
156 | "@webassemblyjs/helper-buffer": "1.11.1",
157 | "@webassemblyjs/helper-wasm-bytecode": "1.11.1",
158 | "@webassemblyjs/wasm-gen": "1.11.1"
159 | }
160 | },
161 | "node_modules/@webassemblyjs/ieee754": {
162 | "version": "1.11.1",
163 | "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz",
164 | "integrity": "sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ==",
165 | "dependencies": {
166 | "@xtuc/ieee754": "^1.2.0"
167 | }
168 | },
169 | "node_modules/@webassemblyjs/leb128": {
170 | "version": "1.11.1",
171 | "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.1.tgz",
172 | "integrity": "sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw==",
173 | "dependencies": {
174 | "@xtuc/long": "4.2.2"
175 | }
176 | },
177 | "node_modules/@webassemblyjs/utf8": {
178 | "version": "1.11.1",
179 | "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.1.tgz",
180 | "integrity": "sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ=="
181 | },
182 | "node_modules/@webassemblyjs/wasm-edit": {
183 | "version": "1.11.1",
184 | "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz",
185 | "integrity": "sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA==",
186 | "dependencies": {
187 | "@webassemblyjs/ast": "1.11.1",
188 | "@webassemblyjs/helper-buffer": "1.11.1",
189 | "@webassemblyjs/helper-wasm-bytecode": "1.11.1",
190 | "@webassemblyjs/helper-wasm-section": "1.11.1",
191 | "@webassemblyjs/wasm-gen": "1.11.1",
192 | "@webassemblyjs/wasm-opt": "1.11.1",
193 | "@webassemblyjs/wasm-parser": "1.11.1",
194 | "@webassemblyjs/wast-printer": "1.11.1"
195 | }
196 | },
197 | "node_modules/@webassemblyjs/wasm-gen": {
198 | "version": "1.11.1",
199 | "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz",
200 | "integrity": "sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA==",
201 | "dependencies": {
202 | "@webassemblyjs/ast": "1.11.1",
203 | "@webassemblyjs/helper-wasm-bytecode": "1.11.1",
204 | "@webassemblyjs/ieee754": "1.11.1",
205 | "@webassemblyjs/leb128": "1.11.1",
206 | "@webassemblyjs/utf8": "1.11.1"
207 | }
208 | },
209 | "node_modules/@webassemblyjs/wasm-opt": {
210 | "version": "1.11.1",
211 | "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz",
212 | "integrity": "sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw==",
213 | "dependencies": {
214 | "@webassemblyjs/ast": "1.11.1",
215 | "@webassemblyjs/helper-buffer": "1.11.1",
216 | "@webassemblyjs/wasm-gen": "1.11.1",
217 | "@webassemblyjs/wasm-parser": "1.11.1"
218 | }
219 | },
220 | "node_modules/@webassemblyjs/wasm-parser": {
221 | "version": "1.11.1",
222 | "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz",
223 | "integrity": "sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA==",
224 | "dependencies": {
225 | "@webassemblyjs/ast": "1.11.1",
226 | "@webassemblyjs/helper-api-error": "1.11.1",
227 | "@webassemblyjs/helper-wasm-bytecode": "1.11.1",
228 | "@webassemblyjs/ieee754": "1.11.1",
229 | "@webassemblyjs/leb128": "1.11.1",
230 | "@webassemblyjs/utf8": "1.11.1"
231 | }
232 | },
233 | "node_modules/@webassemblyjs/wast-printer": {
234 | "version": "1.11.1",
235 | "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz",
236 | "integrity": "sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg==",
237 | "dependencies": {
238 | "@webassemblyjs/ast": "1.11.1",
239 | "@xtuc/long": "4.2.2"
240 | }
241 | },
242 | "node_modules/@webpack-cli/configtest": {
243 | "version": "1.0.2",
244 | "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-1.0.2.tgz",
245 | "integrity": "sha512-3OBzV2fBGZ5TBfdW50cha1lHDVf9vlvRXnjpVbJBa20pSZQaSkMJZiwA8V2vD9ogyeXn8nU5s5A6mHyf5jhMzA==",
246 | "peerDependencies": {
247 | "webpack": "4.x.x || 5.x.x",
248 | "webpack-cli": "4.x.x"
249 | }
250 | },
251 | "node_modules/@webpack-cli/info": {
252 | "version": "1.2.3",
253 | "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-1.2.3.tgz",
254 | "integrity": "sha512-lLek3/T7u40lTqzCGpC6CAbY6+vXhdhmwFRxZLMnRm6/sIF/7qMpT8MocXCRQfz0JAh63wpbXLMnsQ5162WS7Q==",
255 | "dependencies": {
256 | "envinfo": "^7.7.3"
257 | },
258 | "peerDependencies": {
259 | "webpack-cli": "4.x.x"
260 | }
261 | },
262 | "node_modules/@webpack-cli/serve": {
263 | "version": "1.3.1",
264 | "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-1.3.1.tgz",
265 | "integrity": "sha512-0qXvpeYO6vaNoRBI52/UsbcaBydJCggoBBnIo/ovQQdn6fug0BgwsjorV1hVS7fMqGVTZGcVxv8334gjmbj5hw==",
266 | "peerDependencies": {
267 | "webpack-cli": "4.x.x"
268 | },
269 | "peerDependenciesMeta": {
270 | "webpack-dev-server": {
271 | "optional": true
272 | }
273 | }
274 | },
275 | "node_modules/@xtuc/ieee754": {
276 | "version": "1.2.0",
277 | "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
278 | "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA=="
279 | },
280 | "node_modules/@xtuc/long": {
281 | "version": "4.2.2",
282 | "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz",
283 | "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ=="
284 | },
285 | "node_modules/acorn": {
286 | "version": "8.8.2",
287 | "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz",
288 | "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==",
289 | "bin": {
290 | "acorn": "bin/acorn"
291 | },
292 | "engines": {
293 | "node": ">=0.4.0"
294 | }
295 | },
296 | "node_modules/acorn-import-assertions": {
297 | "version": "1.8.0",
298 | "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz",
299 | "integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==",
300 | "peerDependencies": {
301 | "acorn": "^8"
302 | }
303 | },
304 | "node_modules/ajv": {
305 | "version": "6.12.6",
306 | "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
307 | "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
308 | "dependencies": {
309 | "fast-deep-equal": "^3.1.1",
310 | "fast-json-stable-stringify": "^2.0.0",
311 | "json-schema-traverse": "^0.4.1",
312 | "uri-js": "^4.2.2"
313 | },
314 | "funding": {
315 | "type": "github",
316 | "url": "https://github.com/sponsors/epoberezkin"
317 | }
318 | },
319 | "node_modules/ajv-keywords": {
320 | "version": "3.5.2",
321 | "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
322 | "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
323 | "peerDependencies": {
324 | "ajv": "^6.9.1"
325 | }
326 | },
327 | "node_modules/ansi-colors": {
328 | "version": "4.1.1",
329 | "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz",
330 | "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==",
331 | "engines": {
332 | "node": ">=6"
333 | }
334 | },
335 | "node_modules/browserslist": {
336 | "version": "4.21.5",
337 | "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.5.tgz",
338 | "integrity": "sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w==",
339 | "funding": [
340 | {
341 | "type": "opencollective",
342 | "url": "https://opencollective.com/browserslist"
343 | },
344 | {
345 | "type": "tidelift",
346 | "url": "https://tidelift.com/funding/github/npm/browserslist"
347 | }
348 | ],
349 | "dependencies": {
350 | "caniuse-lite": "^1.0.30001449",
351 | "electron-to-chromium": "^1.4.284",
352 | "node-releases": "^2.0.8",
353 | "update-browserslist-db": "^1.0.10"
354 | },
355 | "bin": {
356 | "browserslist": "cli.js"
357 | },
358 | "engines": {
359 | "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
360 | }
361 | },
362 | "node_modules/buffer-from": {
363 | "version": "1.1.2",
364 | "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
365 | "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="
366 | },
367 | "node_modules/caniuse-lite": {
368 | "version": "1.0.30001466",
369 | "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001466.tgz",
370 | "integrity": "sha512-ewtFBSfWjEmxUgNBSZItFSmVtvk9zkwkl1OfRZlKA8slltRN+/C/tuGVrF9styXkN36Yu3+SeJ1qkXxDEyNZ5w==",
371 | "funding": [
372 | {
373 | "type": "opencollective",
374 | "url": "https://opencollective.com/browserslist"
375 | },
376 | {
377 | "type": "tidelift",
378 | "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
379 | }
380 | ]
381 | },
382 | "node_modules/chrome-trace-event": {
383 | "version": "1.0.3",
384 | "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz",
385 | "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==",
386 | "engines": {
387 | "node": ">=6.0"
388 | }
389 | },
390 | "node_modules/clone-deep": {
391 | "version": "4.0.1",
392 | "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz",
393 | "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==",
394 | "dependencies": {
395 | "is-plain-object": "^2.0.4",
396 | "kind-of": "^6.0.2",
397 | "shallow-clone": "^3.0.0"
398 | },
399 | "engines": {
400 | "node": ">=6"
401 | }
402 | },
403 | "node_modules/colorette": {
404 | "version": "1.2.2",
405 | "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz",
406 | "integrity": "sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w=="
407 | },
408 | "node_modules/commander": {
409 | "version": "2.20.3",
410 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
411 | "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
412 | },
413 | "node_modules/cross-spawn": {
414 | "version": "7.0.3",
415 | "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
416 | "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
417 | "dependencies": {
418 | "path-key": "^3.1.0",
419 | "shebang-command": "^2.0.0",
420 | "which": "^2.0.1"
421 | },
422 | "engines": {
423 | "node": ">= 8"
424 | }
425 | },
426 | "node_modules/d3": {
427 | "version": "5.16.0",
428 | "resolved": "https://registry.npmjs.org/d3/-/d3-5.16.0.tgz",
429 | "integrity": "sha512-4PL5hHaHwX4m7Zr1UapXW23apo6pexCgdetdJ5kTmADpG/7T9Gkxw0M0tf/pjoB63ezCCm0u5UaFYy2aMt0Mcw==",
430 | "dependencies": {
431 | "d3-array": "1",
432 | "d3-axis": "1",
433 | "d3-brush": "1",
434 | "d3-chord": "1",
435 | "d3-collection": "1",
436 | "d3-color": "1",
437 | "d3-contour": "1",
438 | "d3-dispatch": "1",
439 | "d3-drag": "1",
440 | "d3-dsv": "1",
441 | "d3-ease": "1",
442 | "d3-fetch": "1",
443 | "d3-force": "1",
444 | "d3-format": "1",
445 | "d3-geo": "1",
446 | "d3-hierarchy": "1",
447 | "d3-interpolate": "1",
448 | "d3-path": "1",
449 | "d3-polygon": "1",
450 | "d3-quadtree": "1",
451 | "d3-random": "1",
452 | "d3-scale": "2",
453 | "d3-scale-chromatic": "1",
454 | "d3-selection": "1",
455 | "d3-shape": "1",
456 | "d3-time": "1",
457 | "d3-time-format": "2",
458 | "d3-timer": "1",
459 | "d3-transition": "1",
460 | "d3-voronoi": "1",
461 | "d3-zoom": "1"
462 | }
463 | },
464 | "node_modules/d3-array": {
465 | "version": "1.2.4",
466 | "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-1.2.4.tgz",
467 | "integrity": "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw=="
468 | },
469 | "node_modules/d3-axis": {
470 | "version": "1.0.12",
471 | "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-1.0.12.tgz",
472 | "integrity": "sha512-ejINPfPSNdGFKEOAtnBtdkpr24c4d4jsei6Lg98mxf424ivoDP2956/5HDpIAtmHo85lqT4pruy+zEgvRUBqaQ=="
473 | },
474 | "node_modules/d3-brush": {
475 | "version": "1.1.6",
476 | "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-1.1.6.tgz",
477 | "integrity": "sha512-7RW+w7HfMCPyZLifTz/UnJmI5kdkXtpCbombUSs8xniAyo0vIbrDzDwUJB6eJOgl9u5DQOt2TQlYumxzD1SvYA==",
478 | "dependencies": {
479 | "d3-dispatch": "1",
480 | "d3-drag": "1",
481 | "d3-interpolate": "1",
482 | "d3-selection": "1",
483 | "d3-transition": "1"
484 | }
485 | },
486 | "node_modules/d3-chord": {
487 | "version": "1.0.6",
488 | "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-1.0.6.tgz",
489 | "integrity": "sha512-JXA2Dro1Fxw9rJe33Uv+Ckr5IrAa74TlfDEhE/jfLOaXegMQFQTAgAw9WnZL8+HxVBRXaRGCkrNU7pJeylRIuA==",
490 | "dependencies": {
491 | "d3-array": "1",
492 | "d3-path": "1"
493 | }
494 | },
495 | "node_modules/d3-collection": {
496 | "version": "1.0.7",
497 | "resolved": "https://registry.npmjs.org/d3-collection/-/d3-collection-1.0.7.tgz",
498 | "integrity": "sha512-ii0/r5f4sjKNTfh84Di+DpztYwqKhEyUlKoPrzUFfeSkWxjW49xU2QzO9qrPrNkpdI0XJkfzvmTu8V2Zylln6A=="
499 | },
500 | "node_modules/d3-color": {
501 | "version": "3.1.0",
502 | "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
503 | "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
504 | "engines": {
505 | "node": ">=12"
506 | }
507 | },
508 | "node_modules/d3-contour": {
509 | "version": "1.3.2",
510 | "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-1.3.2.tgz",
511 | "integrity": "sha512-hoPp4K/rJCu0ladiH6zmJUEz6+u3lgR+GSm/QdM2BBvDraU39Vr7YdDCicJcxP1z8i9B/2dJLgDC1NcvlF8WCg==",
512 | "dependencies": {
513 | "d3-array": "^1.1.1"
514 | }
515 | },
516 | "node_modules/d3-dispatch": {
517 | "version": "1.0.6",
518 | "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-1.0.6.tgz",
519 | "integrity": "sha512-fVjoElzjhCEy+Hbn8KygnmMS7Or0a9sI2UzGwoB7cCtvI1XpVN9GpoYlnb3xt2YV66oXYb1fLJ8GMvP4hdU1RA=="
520 | },
521 | "node_modules/d3-drag": {
522 | "version": "1.2.5",
523 | "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-1.2.5.tgz",
524 | "integrity": "sha512-rD1ohlkKQwMZYkQlYVCrSFxsWPzI97+W+PaEIBNTMxRuxz9RF0Hi5nJWHGVJ3Om9d2fRTe1yOBINJyy/ahV95w==",
525 | "dependencies": {
526 | "d3-dispatch": "1",
527 | "d3-selection": "1"
528 | }
529 | },
530 | "node_modules/d3-dsv": {
531 | "version": "1.2.0",
532 | "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-1.2.0.tgz",
533 | "integrity": "sha512-9yVlqvZcSOMhCYzniHE7EVUws7Fa1zgw+/EAV2BxJoG3ME19V6BQFBwI855XQDsxyOuG7NibqRMTtiF/Qup46g==",
534 | "dependencies": {
535 | "commander": "2",
536 | "iconv-lite": "0.4",
537 | "rw": "1"
538 | },
539 | "bin": {
540 | "csv2json": "bin/dsv2json",
541 | "csv2tsv": "bin/dsv2dsv",
542 | "dsv2dsv": "bin/dsv2dsv",
543 | "dsv2json": "bin/dsv2json",
544 | "json2csv": "bin/json2dsv",
545 | "json2dsv": "bin/json2dsv",
546 | "json2tsv": "bin/json2dsv",
547 | "tsv2csv": "bin/dsv2dsv",
548 | "tsv2json": "bin/dsv2json"
549 | }
550 | },
551 | "node_modules/d3-ease": {
552 | "version": "1.0.7",
553 | "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-1.0.7.tgz",
554 | "integrity": "sha512-lx14ZPYkhNx0s/2HX5sLFUI3mbasHjSSpwO/KaaNACweVwxUruKyWVcb293wMv1RqTPZyZ8kSZ2NogUZNcLOFQ=="
555 | },
556 | "node_modules/d3-fetch": {
557 | "version": "1.2.0",
558 | "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-1.2.0.tgz",
559 | "integrity": "sha512-yC78NBVcd2zFAyR/HnUiBS7Lf6inSCoWcSxFfw8FYL7ydiqe80SazNwoffcqOfs95XaLo7yebsmQqDKSsXUtvA==",
560 | "dependencies": {
561 | "d3-dsv": "1"
562 | }
563 | },
564 | "node_modules/d3-force": {
565 | "version": "1.2.1",
566 | "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-1.2.1.tgz",
567 | "integrity": "sha512-HHvehyaiUlVo5CxBJ0yF/xny4xoaxFxDnBXNvNcfW9adORGZfyNF1dj6DGLKyk4Yh3brP/1h3rnDzdIAwL08zg==",
568 | "dependencies": {
569 | "d3-collection": "1",
570 | "d3-dispatch": "1",
571 | "d3-quadtree": "1",
572 | "d3-timer": "1"
573 | }
574 | },
575 | "node_modules/d3-format": {
576 | "version": "1.4.5",
577 | "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-1.4.5.tgz",
578 | "integrity": "sha512-J0piedu6Z8iB6TbIGfZgDzfXxUFN3qQRMofy2oPdXzQibYGqPB/9iMcxr/TGalU+2RsyDO+U4f33id8tbnSRMQ=="
579 | },
580 | "node_modules/d3-geo": {
581 | "version": "1.12.1",
582 | "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-1.12.1.tgz",
583 | "integrity": "sha512-XG4d1c/UJSEX9NfU02KwBL6BYPj8YKHxgBEw5om2ZnTRSbIcego6dhHwcxuSR3clxh0EpE38os1DVPOmnYtTPg==",
584 | "dependencies": {
585 | "d3-array": "1"
586 | }
587 | },
588 | "node_modules/d3-hierarchy": {
589 | "version": "1.1.9",
590 | "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-1.1.9.tgz",
591 | "integrity": "sha512-j8tPxlqh1srJHAtxfvOUwKNYJkQuBFdM1+JAUfq6xqH5eAqf93L7oG1NVqDa4CpFZNvnNKtCYEUC8KY9yEn9lQ=="
592 | },
593 | "node_modules/d3-interpolate": {
594 | "version": "1.4.0",
595 | "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.4.0.tgz",
596 | "integrity": "sha512-V9znK0zc3jOPV4VD2zZn0sDhZU3WAE2bmlxdIwwQPPzPjvyLkd8B3JUVdS1IDUFDkWZ72c9qnv1GK2ZagTZ8EA==",
597 | "dependencies": {
598 | "d3-color": "1"
599 | }
600 | },
601 | "node_modules/d3-interpolate/node_modules/d3-color": {
602 | "version": "1.4.1",
603 | "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-1.4.1.tgz",
604 | "integrity": "sha512-p2sTHSLCJI2QKunbGb7ocOh7DgTAn8IrLx21QRc/BSnodXM4sv6aLQlnfpvehFMLZEfBc6g9pH9SWQccFYfJ9Q=="
605 | },
606 | "node_modules/d3-path": {
607 | "version": "1.0.9",
608 | "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz",
609 | "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="
610 | },
611 | "node_modules/d3-polygon": {
612 | "version": "1.0.6",
613 | "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-1.0.6.tgz",
614 | "integrity": "sha512-k+RF7WvI08PC8reEoXa/w2nSg5AUMTi+peBD9cmFc+0ixHfbs4QmxxkarVal1IkVkgxVuk9JSHhJURHiyHKAuQ=="
615 | },
616 | "node_modules/d3-quadtree": {
617 | "version": "1.0.7",
618 | "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-1.0.7.tgz",
619 | "integrity": "sha512-RKPAeXnkC59IDGD0Wu5mANy0Q2V28L+fNe65pOCXVdVuTJS3WPKaJlFHer32Rbh9gIo9qMuJXio8ra4+YmIymA=="
620 | },
621 | "node_modules/d3-random": {
622 | "version": "1.1.2",
623 | "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-1.1.2.tgz",
624 | "integrity": "sha512-6AK5BNpIFqP+cx/sreKzNjWbwZQCSUatxq+pPRmFIQaWuoD+NrbVWw7YWpHiXpCQ/NanKdtGDuB+VQcZDaEmYQ=="
625 | },
626 | "node_modules/d3-scale": {
627 | "version": "2.2.2",
628 | "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-2.2.2.tgz",
629 | "integrity": "sha512-LbeEvGgIb8UMcAa0EATLNX0lelKWGYDQiPdHj+gLblGVhGLyNbaCn3EvrJf0A3Y/uOOU5aD6MTh5ZFCdEwGiCw==",
630 | "dependencies": {
631 | "d3-array": "^1.2.0",
632 | "d3-collection": "1",
633 | "d3-format": "1",
634 | "d3-interpolate": "1",
635 | "d3-time": "1",
636 | "d3-time-format": "2"
637 | }
638 | },
639 | "node_modules/d3-scale-chromatic": {
640 | "version": "1.5.0",
641 | "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-1.5.0.tgz",
642 | "integrity": "sha512-ACcL46DYImpRFMBcpk9HhtIyC7bTBR4fNOPxwVSl0LfulDAwyiHyPOTqcDG1+t5d4P9W7t/2NAuWu59aKko/cg==",
643 | "dependencies": {
644 | "d3-color": "1",
645 | "d3-interpolate": "1"
646 | }
647 | },
648 | "node_modules/d3-scale-chromatic/node_modules/d3-color": {
649 | "version": "1.4.1",
650 | "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-1.4.1.tgz",
651 | "integrity": "sha512-p2sTHSLCJI2QKunbGb7ocOh7DgTAn8IrLx21QRc/BSnodXM4sv6aLQlnfpvehFMLZEfBc6g9pH9SWQccFYfJ9Q=="
652 | },
653 | "node_modules/d3-selection": {
654 | "version": "1.4.2",
655 | "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-1.4.2.tgz",
656 | "integrity": "sha512-SJ0BqYihzOjDnnlfyeHT0e30k0K1+5sR3d5fNueCNeuhZTnGw4M4o8mqJchSwgKMXCNFo+e2VTChiSJ0vYtXkg=="
657 | },
658 | "node_modules/d3-shape": {
659 | "version": "1.3.7",
660 | "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz",
661 | "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==",
662 | "dependencies": {
663 | "d3-path": "1"
664 | }
665 | },
666 | "node_modules/d3-time": {
667 | "version": "1.1.0",
668 | "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-1.1.0.tgz",
669 | "integrity": "sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA=="
670 | },
671 | "node_modules/d3-time-format": {
672 | "version": "2.3.0",
673 | "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.3.0.tgz",
674 | "integrity": "sha512-guv6b2H37s2Uq/GefleCDtbe0XZAuy7Wa49VGkPVPMfLL9qObgBST3lEHJBMUp8S7NdLQAGIvr2KXk8Hc98iKQ==",
675 | "dependencies": {
676 | "d3-time": "1"
677 | }
678 | },
679 | "node_modules/d3-timer": {
680 | "version": "1.0.10",
681 | "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-1.0.10.tgz",
682 | "integrity": "sha512-B1JDm0XDaQC+uvo4DT79H0XmBskgS3l6Ve+1SBCfxgmtIb1AVrPIoqd+nPSv+loMX8szQ0sVUhGngL7D5QPiXw=="
683 | },
684 | "node_modules/d3-transition": {
685 | "version": "1.3.2",
686 | "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-1.3.2.tgz",
687 | "integrity": "sha512-sc0gRU4PFqZ47lPVHloMn9tlPcv8jxgOQg+0zjhfZXMQuvppjG6YuwdMBE0TuqCZjeJkLecku/l9R0JPcRhaDA==",
688 | "dependencies": {
689 | "d3-color": "1",
690 | "d3-dispatch": "1",
691 | "d3-ease": "1",
692 | "d3-interpolate": "1",
693 | "d3-selection": "^1.1.0",
694 | "d3-timer": "1"
695 | }
696 | },
697 | "node_modules/d3-transition/node_modules/d3-color": {
698 | "version": "1.4.1",
699 | "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-1.4.1.tgz",
700 | "integrity": "sha512-p2sTHSLCJI2QKunbGb7ocOh7DgTAn8IrLx21QRc/BSnodXM4sv6aLQlnfpvehFMLZEfBc6g9pH9SWQccFYfJ9Q=="
701 | },
702 | "node_modules/d3-voronoi": {
703 | "version": "1.1.4",
704 | "resolved": "https://registry.npmjs.org/d3-voronoi/-/d3-voronoi-1.1.4.tgz",
705 | "integrity": "sha512-dArJ32hchFsrQ8uMiTBLq256MpnZjeuBtdHpaDlYuQyjU0CVzCJl/BVW+SkszaAeH95D/8gxqAhgx0ouAWAfRg=="
706 | },
707 | "node_modules/d3-zoom": {
708 | "version": "1.8.3",
709 | "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-1.8.3.tgz",
710 | "integrity": "sha512-VoLXTK4wvy1a0JpH2Il+F2CiOhVu7VRXWF5M/LroMIh3/zBAC3WAt7QoIvPibOavVo20hN6/37vwAsdBejLyKQ==",
711 | "dependencies": {
712 | "d3-dispatch": "1",
713 | "d3-drag": "1",
714 | "d3-interpolate": "1",
715 | "d3-selection": "1",
716 | "d3-transition": "1"
717 | }
718 | },
719 | "node_modules/d3/node_modules/d3-color": {
720 | "version": "1.4.1",
721 | "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-1.4.1.tgz",
722 | "integrity": "sha512-p2sTHSLCJI2QKunbGb7ocOh7DgTAn8IrLx21QRc/BSnodXM4sv6aLQlnfpvehFMLZEfBc6g9pH9SWQccFYfJ9Q=="
723 | },
724 | "node_modules/dagre": {
725 | "version": "0.8.5",
726 | "resolved": "https://registry.npmjs.org/dagre/-/dagre-0.8.5.tgz",
727 | "integrity": "sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw==",
728 | "dependencies": {
729 | "graphlib": "^2.1.8",
730 | "lodash": "^4.17.15"
731 | }
732 | },
733 | "node_modules/dagre-d3": {
734 | "version": "0.6.4",
735 | "resolved": "https://registry.npmjs.org/dagre-d3/-/dagre-d3-0.6.4.tgz",
736 | "integrity": "sha512-e/6jXeCP7/ptlAM48clmX4xTZc5Ek6T6kagS7Oz2HrYSdqcLZFLqpAfh7ldbZRFfxCZVyh61NEPR08UQRVxJzQ==",
737 | "dependencies": {
738 | "d3": "^5.14",
739 | "dagre": "^0.8.5",
740 | "graphlib": "^2.1.8",
741 | "lodash": "^4.17.15"
742 | }
743 | },
744 | "node_modules/electron-to-chromium": {
745 | "version": "1.4.328",
746 | "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.328.tgz",
747 | "integrity": "sha512-DE9tTy2PNmy1v55AZAO542ui+MLC2cvINMK4P2LXGsJdput/ThVG9t+QGecPuAZZSgC8XoI+Jh9M1OG9IoNSCw=="
748 | },
749 | "node_modules/enhanced-resolve": {
750 | "version": "5.12.0",
751 | "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz",
752 | "integrity": "sha512-QHTXI/sZQmko1cbDoNAa3mJ5qhWUUNAq3vR0/YiD379fWQrcfuoX1+HW2S0MTt7XmoPLapdaDKUtelUSPic7hQ==",
753 | "dependencies": {
754 | "graceful-fs": "^4.2.4",
755 | "tapable": "^2.2.0"
756 | },
757 | "engines": {
758 | "node": ">=10.13.0"
759 | }
760 | },
761 | "node_modules/enquirer": {
762 | "version": "2.3.6",
763 | "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz",
764 | "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==",
765 | "dependencies": {
766 | "ansi-colors": "^4.1.1"
767 | },
768 | "engines": {
769 | "node": ">=8.6"
770 | }
771 | },
772 | "node_modules/envinfo": {
773 | "version": "7.8.1",
774 | "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.8.1.tgz",
775 | "integrity": "sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw==",
776 | "bin": {
777 | "envinfo": "dist/cli.js"
778 | },
779 | "engines": {
780 | "node": ">=4"
781 | }
782 | },
783 | "node_modules/es-module-lexer": {
784 | "version": "0.9.3",
785 | "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.9.3.tgz",
786 | "integrity": "sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ=="
787 | },
788 | "node_modules/escalade": {
789 | "version": "3.1.1",
790 | "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
791 | "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==",
792 | "engines": {
793 | "node": ">=6"
794 | }
795 | },
796 | "node_modules/eslint-scope": {
797 | "version": "5.1.1",
798 | "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
799 | "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==",
800 | "dependencies": {
801 | "esrecurse": "^4.3.0",
802 | "estraverse": "^4.1.1"
803 | },
804 | "engines": {
805 | "node": ">=8.0.0"
806 | }
807 | },
808 | "node_modules/esrecurse": {
809 | "version": "4.3.0",
810 | "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
811 | "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
812 | "dependencies": {
813 | "estraverse": "^5.2.0"
814 | },
815 | "engines": {
816 | "node": ">=4.0"
817 | }
818 | },
819 | "node_modules/esrecurse/node_modules/estraverse": {
820 | "version": "5.3.0",
821 | "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
822 | "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
823 | "engines": {
824 | "node": ">=4.0"
825 | }
826 | },
827 | "node_modules/estraverse": {
828 | "version": "4.3.0",
829 | "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
830 | "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
831 | "engines": {
832 | "node": ">=4.0"
833 | }
834 | },
835 | "node_modules/events": {
836 | "version": "3.3.0",
837 | "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
838 | "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
839 | "engines": {
840 | "node": ">=0.8.x"
841 | }
842 | },
843 | "node_modules/execa": {
844 | "version": "5.0.0",
845 | "resolved": "https://registry.npmjs.org/execa/-/execa-5.0.0.tgz",
846 | "integrity": "sha512-ov6w/2LCiuyO4RLYGdpFGjkcs0wMTgGE8PrkTHikeUy5iJekXyPIKUjifk5CsE0pt7sMCrMZ3YNqoCj6idQOnQ==",
847 | "dependencies": {
848 | "cross-spawn": "^7.0.3",
849 | "get-stream": "^6.0.0",
850 | "human-signals": "^2.1.0",
851 | "is-stream": "^2.0.0",
852 | "merge-stream": "^2.0.0",
853 | "npm-run-path": "^4.0.1",
854 | "onetime": "^5.1.2",
855 | "signal-exit": "^3.0.3",
856 | "strip-final-newline": "^2.0.0"
857 | },
858 | "engines": {
859 | "node": ">=10"
860 | },
861 | "funding": {
862 | "url": "https://github.com/sindresorhus/execa?sponsor=1"
863 | }
864 | },
865 | "node_modules/fast-deep-equal": {
866 | "version": "3.1.3",
867 | "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
868 | "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
869 | },
870 | "node_modules/fast-json-stable-stringify": {
871 | "version": "2.1.0",
872 | "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
873 | "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="
874 | },
875 | "node_modules/fastest-levenshtein": {
876 | "version": "1.0.12",
877 | "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz",
878 | "integrity": "sha512-On2N+BpYJ15xIC974QNVuYGMOlEVt4s0EOI3wwMqOmK1fdDY+FN/zltPV8vosq4ad4c/gJ1KHScUn/6AWIgiow=="
879 | },
880 | "node_modules/find-up": {
881 | "version": "4.1.0",
882 | "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
883 | "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
884 | "dependencies": {
885 | "locate-path": "^5.0.0",
886 | "path-exists": "^4.0.0"
887 | },
888 | "engines": {
889 | "node": ">=8"
890 | }
891 | },
892 | "node_modules/function-bind": {
893 | "version": "1.1.1",
894 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
895 | "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
896 | },
897 | "node_modules/get-stream": {
898 | "version": "6.0.1",
899 | "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
900 | "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==",
901 | "engines": {
902 | "node": ">=10"
903 | },
904 | "funding": {
905 | "url": "https://github.com/sponsors/sindresorhus"
906 | }
907 | },
908 | "node_modules/glob-to-regexp": {
909 | "version": "0.4.1",
910 | "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz",
911 | "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw=="
912 | },
913 | "node_modules/graceful-fs": {
914 | "version": "4.2.10",
915 | "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz",
916 | "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA=="
917 | },
918 | "node_modules/graphlib": {
919 | "version": "2.1.8",
920 | "resolved": "https://registry.npmjs.org/graphlib/-/graphlib-2.1.8.tgz",
921 | "integrity": "sha512-jcLLfkpoVGmH7/InMC/1hIvOPSUh38oJtGhvrOFGzioE1DZ+0YW16RgmOJhHiuWTvGiJQ9Z1Ik43JvkRPRvE+A==",
922 | "dependencies": {
923 | "lodash": "^4.17.15"
924 | }
925 | },
926 | "node_modules/has": {
927 | "version": "1.0.3",
928 | "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
929 | "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
930 | "dependencies": {
931 | "function-bind": "^1.1.1"
932 | },
933 | "engines": {
934 | "node": ">= 0.4.0"
935 | }
936 | },
937 | "node_modules/has-flag": {
938 | "version": "4.0.0",
939 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
940 | "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
941 | "engines": {
942 | "node": ">=8"
943 | }
944 | },
945 | "node_modules/human-signals": {
946 | "version": "2.1.0",
947 | "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz",
948 | "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==",
949 | "engines": {
950 | "node": ">=10.17.0"
951 | }
952 | },
953 | "node_modules/iconv-lite": {
954 | "version": "0.4.24",
955 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
956 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
957 | "dependencies": {
958 | "safer-buffer": ">= 2.1.2 < 3"
959 | },
960 | "engines": {
961 | "node": ">=0.10.0"
962 | }
963 | },
964 | "node_modules/import-local": {
965 | "version": "3.0.2",
966 | "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.0.2.tgz",
967 | "integrity": "sha512-vjL3+w0oulAVZ0hBHnxa/Nm5TAurf9YLQJDhqRZyqb+VKGOB6LU8t9H1Nr5CIo16vh9XfJTOoHwU0B71S557gA==",
968 | "dependencies": {
969 | "pkg-dir": "^4.2.0",
970 | "resolve-cwd": "^3.0.0"
971 | },
972 | "bin": {
973 | "import-local-fixture": "fixtures/cli.js"
974 | },
975 | "engines": {
976 | "node": ">=8"
977 | }
978 | },
979 | "node_modules/interpret": {
980 | "version": "2.2.0",
981 | "resolved": "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz",
982 | "integrity": "sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==",
983 | "engines": {
984 | "node": ">= 0.10"
985 | }
986 | },
987 | "node_modules/is-core-module": {
988 | "version": "2.2.0",
989 | "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.2.0.tgz",
990 | "integrity": "sha512-XRAfAdyyY5F5cOXn7hYQDqh2Xmii+DEfIcQGxK/uNwMHhIkPWO0g8msXcbzLe+MpGoR951MlqM/2iIlU4vKDdQ==",
991 | "dependencies": {
992 | "has": "^1.0.3"
993 | },
994 | "funding": {
995 | "url": "https://github.com/sponsors/ljharb"
996 | }
997 | },
998 | "node_modules/is-plain-object": {
999 | "version": "2.0.4",
1000 | "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz",
1001 | "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==",
1002 | "dependencies": {
1003 | "isobject": "^3.0.1"
1004 | },
1005 | "engines": {
1006 | "node": ">=0.10.0"
1007 | }
1008 | },
1009 | "node_modules/is-stream": {
1010 | "version": "2.0.0",
1011 | "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz",
1012 | "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==",
1013 | "engines": {
1014 | "node": ">=8"
1015 | }
1016 | },
1017 | "node_modules/isexe": {
1018 | "version": "2.0.0",
1019 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
1020 | "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA="
1021 | },
1022 | "node_modules/isobject": {
1023 | "version": "3.0.1",
1024 | "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
1025 | "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
1026 | "engines": {
1027 | "node": ">=0.10.0"
1028 | }
1029 | },
1030 | "node_modules/jest-worker": {
1031 | "version": "27.5.1",
1032 | "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz",
1033 | "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==",
1034 | "dependencies": {
1035 | "@types/node": "*",
1036 | "merge-stream": "^2.0.0",
1037 | "supports-color": "^8.0.0"
1038 | },
1039 | "engines": {
1040 | "node": ">= 10.13.0"
1041 | }
1042 | },
1043 | "node_modules/json-parse-even-better-errors": {
1044 | "version": "2.3.1",
1045 | "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
1046 | "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="
1047 | },
1048 | "node_modules/json-schema-traverse": {
1049 | "version": "0.4.1",
1050 | "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
1051 | "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
1052 | },
1053 | "node_modules/kind-of": {
1054 | "version": "6.0.3",
1055 | "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
1056 | "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
1057 | "engines": {
1058 | "node": ">=0.10.0"
1059 | }
1060 | },
1061 | "node_modules/loader-runner": {
1062 | "version": "4.3.0",
1063 | "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz",
1064 | "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==",
1065 | "engines": {
1066 | "node": ">=6.11.5"
1067 | }
1068 | },
1069 | "node_modules/locate-path": {
1070 | "version": "5.0.0",
1071 | "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
1072 | "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
1073 | "dependencies": {
1074 | "p-locate": "^4.1.0"
1075 | },
1076 | "engines": {
1077 | "node": ">=8"
1078 | }
1079 | },
1080 | "node_modules/lodash": {
1081 | "version": "4.17.21",
1082 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
1083 | "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
1084 | },
1085 | "node_modules/merge-stream": {
1086 | "version": "2.0.0",
1087 | "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
1088 | "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="
1089 | },
1090 | "node_modules/mime-db": {
1091 | "version": "1.52.0",
1092 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
1093 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
1094 | "engines": {
1095 | "node": ">= 0.6"
1096 | }
1097 | },
1098 | "node_modules/mime-types": {
1099 | "version": "2.1.35",
1100 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
1101 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
1102 | "dependencies": {
1103 | "mime-db": "1.52.0"
1104 | },
1105 | "engines": {
1106 | "node": ">= 0.6"
1107 | }
1108 | },
1109 | "node_modules/mimic-fn": {
1110 | "version": "2.1.0",
1111 | "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
1112 | "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
1113 | "engines": {
1114 | "node": ">=6"
1115 | }
1116 | },
1117 | "node_modules/neo-async": {
1118 | "version": "2.6.2",
1119 | "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
1120 | "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="
1121 | },
1122 | "node_modules/node-releases": {
1123 | "version": "2.0.10",
1124 | "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.10.tgz",
1125 | "integrity": "sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w=="
1126 | },
1127 | "node_modules/npm-run-path": {
1128 | "version": "4.0.1",
1129 | "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
1130 | "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==",
1131 | "dependencies": {
1132 | "path-key": "^3.0.0"
1133 | },
1134 | "engines": {
1135 | "node": ">=8"
1136 | }
1137 | },
1138 | "node_modules/onetime": {
1139 | "version": "5.1.2",
1140 | "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
1141 | "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
1142 | "dependencies": {
1143 | "mimic-fn": "^2.1.0"
1144 | },
1145 | "engines": {
1146 | "node": ">=6"
1147 | },
1148 | "funding": {
1149 | "url": "https://github.com/sponsors/sindresorhus"
1150 | }
1151 | },
1152 | "node_modules/p-locate": {
1153 | "version": "4.1.0",
1154 | "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
1155 | "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
1156 | "dependencies": {
1157 | "p-limit": "^2.2.0"
1158 | },
1159 | "engines": {
1160 | "node": ">=8"
1161 | }
1162 | },
1163 | "node_modules/p-locate/node_modules/p-limit": {
1164 | "version": "2.3.0",
1165 | "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
1166 | "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
1167 | "dependencies": {
1168 | "p-try": "^2.0.0"
1169 | },
1170 | "engines": {
1171 | "node": ">=6"
1172 | },
1173 | "funding": {
1174 | "url": "https://github.com/sponsors/sindresorhus"
1175 | }
1176 | },
1177 | "node_modules/p-try": {
1178 | "version": "2.2.0",
1179 | "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
1180 | "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
1181 | "engines": {
1182 | "node": ">=6"
1183 | }
1184 | },
1185 | "node_modules/path-exists": {
1186 | "version": "4.0.0",
1187 | "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
1188 | "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
1189 | "engines": {
1190 | "node": ">=8"
1191 | }
1192 | },
1193 | "node_modules/path-key": {
1194 | "version": "3.1.1",
1195 | "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
1196 | "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
1197 | "engines": {
1198 | "node": ">=8"
1199 | }
1200 | },
1201 | "node_modules/path-parse": {
1202 | "version": "1.0.7",
1203 | "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
1204 | "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
1205 | },
1206 | "node_modules/picocolors": {
1207 | "version": "1.0.0",
1208 | "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
1209 | "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="
1210 | },
1211 | "node_modules/pkg-dir": {
1212 | "version": "4.2.0",
1213 | "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz",
1214 | "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==",
1215 | "dependencies": {
1216 | "find-up": "^4.0.0"
1217 | },
1218 | "engines": {
1219 | "node": ">=8"
1220 | }
1221 | },
1222 | "node_modules/punycode": {
1223 | "version": "2.3.0",
1224 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz",
1225 | "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==",
1226 | "engines": {
1227 | "node": ">=6"
1228 | }
1229 | },
1230 | "node_modules/randombytes": {
1231 | "version": "2.1.0",
1232 | "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
1233 | "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
1234 | "dependencies": {
1235 | "safe-buffer": "^5.1.0"
1236 | }
1237 | },
1238 | "node_modules/rechoir": {
1239 | "version": "0.7.0",
1240 | "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.7.0.tgz",
1241 | "integrity": "sha512-ADsDEH2bvbjltXEP+hTIAmeFekTFK0V2BTxMkok6qILyAJEXV0AFfoWcAq4yfll5VdIMd/RVXq0lR+wQi5ZU3Q==",
1242 | "dependencies": {
1243 | "resolve": "^1.9.0"
1244 | },
1245 | "engines": {
1246 | "node": ">= 0.10"
1247 | }
1248 | },
1249 | "node_modules/resolve": {
1250 | "version": "1.20.0",
1251 | "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz",
1252 | "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==",
1253 | "dependencies": {
1254 | "is-core-module": "^2.2.0",
1255 | "path-parse": "^1.0.6"
1256 | },
1257 | "funding": {
1258 | "url": "https://github.com/sponsors/ljharb"
1259 | }
1260 | },
1261 | "node_modules/resolve-cwd": {
1262 | "version": "3.0.0",
1263 | "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz",
1264 | "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==",
1265 | "dependencies": {
1266 | "resolve-from": "^5.0.0"
1267 | },
1268 | "engines": {
1269 | "node": ">=8"
1270 | }
1271 | },
1272 | "node_modules/resolve-from": {
1273 | "version": "5.0.0",
1274 | "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz",
1275 | "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==",
1276 | "engines": {
1277 | "node": ">=8"
1278 | }
1279 | },
1280 | "node_modules/rw": {
1281 | "version": "1.3.3",
1282 | "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz",
1283 | "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ=="
1284 | },
1285 | "node_modules/safe-buffer": {
1286 | "version": "5.2.1",
1287 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
1288 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
1289 | "funding": [
1290 | {
1291 | "type": "github",
1292 | "url": "https://github.com/sponsors/feross"
1293 | },
1294 | {
1295 | "type": "patreon",
1296 | "url": "https://www.patreon.com/feross"
1297 | },
1298 | {
1299 | "type": "consulting",
1300 | "url": "https://feross.org/support"
1301 | }
1302 | ]
1303 | },
1304 | "node_modules/safer-buffer": {
1305 | "version": "2.1.2",
1306 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
1307 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
1308 | },
1309 | "node_modules/schema-utils": {
1310 | "version": "3.1.1",
1311 | "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz",
1312 | "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==",
1313 | "dependencies": {
1314 | "@types/json-schema": "^7.0.8",
1315 | "ajv": "^6.12.5",
1316 | "ajv-keywords": "^3.5.2"
1317 | },
1318 | "engines": {
1319 | "node": ">= 10.13.0"
1320 | },
1321 | "funding": {
1322 | "type": "opencollective",
1323 | "url": "https://opencollective.com/webpack"
1324 | }
1325 | },
1326 | "node_modules/serialize-javascript": {
1327 | "version": "6.0.1",
1328 | "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz",
1329 | "integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==",
1330 | "dependencies": {
1331 | "randombytes": "^2.1.0"
1332 | }
1333 | },
1334 | "node_modules/shallow-clone": {
1335 | "version": "3.0.1",
1336 | "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz",
1337 | "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==",
1338 | "dependencies": {
1339 | "kind-of": "^6.0.2"
1340 | },
1341 | "engines": {
1342 | "node": ">=8"
1343 | }
1344 | },
1345 | "node_modules/shebang-command": {
1346 | "version": "2.0.0",
1347 | "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
1348 | "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
1349 | "dependencies": {
1350 | "shebang-regex": "^3.0.0"
1351 | },
1352 | "engines": {
1353 | "node": ">=8"
1354 | }
1355 | },
1356 | "node_modules/shebang-regex": {
1357 | "version": "3.0.0",
1358 | "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
1359 | "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
1360 | "engines": {
1361 | "node": ">=8"
1362 | }
1363 | },
1364 | "node_modules/signal-exit": {
1365 | "version": "3.0.3",
1366 | "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz",
1367 | "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA=="
1368 | },
1369 | "node_modules/source-map": {
1370 | "version": "0.6.1",
1371 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
1372 | "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
1373 | "engines": {
1374 | "node": ">=0.10.0"
1375 | }
1376 | },
1377 | "node_modules/source-map-support": {
1378 | "version": "0.5.21",
1379 | "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
1380 | "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
1381 | "dependencies": {
1382 | "buffer-from": "^1.0.0",
1383 | "source-map": "^0.6.0"
1384 | }
1385 | },
1386 | "node_modules/strip-final-newline": {
1387 | "version": "2.0.0",
1388 | "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz",
1389 | "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==",
1390 | "engines": {
1391 | "node": ">=6"
1392 | }
1393 | },
1394 | "node_modules/supports-color": {
1395 | "version": "8.1.1",
1396 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
1397 | "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
1398 | "dependencies": {
1399 | "has-flag": "^4.0.0"
1400 | },
1401 | "engines": {
1402 | "node": ">=10"
1403 | },
1404 | "funding": {
1405 | "url": "https://github.com/chalk/supports-color?sponsor=1"
1406 | }
1407 | },
1408 | "node_modules/tapable": {
1409 | "version": "2.2.1",
1410 | "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
1411 | "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==",
1412 | "engines": {
1413 | "node": ">=6"
1414 | }
1415 | },
1416 | "node_modules/terser": {
1417 | "version": "5.16.6",
1418 | "resolved": "https://registry.npmjs.org/terser/-/terser-5.16.6.tgz",
1419 | "integrity": "sha512-IBZ+ZQIA9sMaXmRZCUMDjNH0D5AQQfdn4WUjHL0+1lF4TP1IHRJbrhb6fNaXWikrYQTSkb7SLxkeXAiy1p7mbg==",
1420 | "dependencies": {
1421 | "@jridgewell/source-map": "^0.3.2",
1422 | "acorn": "^8.5.0",
1423 | "commander": "^2.20.0",
1424 | "source-map-support": "~0.5.20"
1425 | },
1426 | "bin": {
1427 | "terser": "bin/terser"
1428 | },
1429 | "engines": {
1430 | "node": ">=10"
1431 | }
1432 | },
1433 | "node_modules/terser-webpack-plugin": {
1434 | "version": "5.3.7",
1435 | "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.7.tgz",
1436 | "integrity": "sha512-AfKwIktyP7Cu50xNjXF/6Qb5lBNzYaWpU6YfoX3uZicTx0zTy0stDDCsvjDapKsSDvOeWo5MEq4TmdBy2cNoHw==",
1437 | "dependencies": {
1438 | "@jridgewell/trace-mapping": "^0.3.17",
1439 | "jest-worker": "^27.4.5",
1440 | "schema-utils": "^3.1.1",
1441 | "serialize-javascript": "^6.0.1",
1442 | "terser": "^5.16.5"
1443 | },
1444 | "engines": {
1445 | "node": ">= 10.13.0"
1446 | },
1447 | "funding": {
1448 | "type": "opencollective",
1449 | "url": "https://opencollective.com/webpack"
1450 | },
1451 | "peerDependencies": {
1452 | "webpack": "^5.1.0"
1453 | },
1454 | "peerDependenciesMeta": {
1455 | "@swc/core": {
1456 | "optional": true
1457 | },
1458 | "esbuild": {
1459 | "optional": true
1460 | },
1461 | "uglify-js": {
1462 | "optional": true
1463 | }
1464 | }
1465 | },
1466 | "node_modules/update-browserslist-db": {
1467 | "version": "1.0.10",
1468 | "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz",
1469 | "integrity": "sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==",
1470 | "funding": [
1471 | {
1472 | "type": "opencollective",
1473 | "url": "https://opencollective.com/browserslist"
1474 | },
1475 | {
1476 | "type": "tidelift",
1477 | "url": "https://tidelift.com/funding/github/npm/browserslist"
1478 | }
1479 | ],
1480 | "dependencies": {
1481 | "escalade": "^3.1.1",
1482 | "picocolors": "^1.0.0"
1483 | },
1484 | "bin": {
1485 | "browserslist-lint": "cli.js"
1486 | },
1487 | "peerDependencies": {
1488 | "browserslist": ">= 4.21.0"
1489 | }
1490 | },
1491 | "node_modules/uri-js": {
1492 | "version": "4.4.1",
1493 | "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
1494 | "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
1495 | "dependencies": {
1496 | "punycode": "^2.1.0"
1497 | }
1498 | },
1499 | "node_modules/v8-compile-cache": {
1500 | "version": "2.3.0",
1501 | "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz",
1502 | "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA=="
1503 | },
1504 | "node_modules/watchpack": {
1505 | "version": "2.4.0",
1506 | "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz",
1507 | "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==",
1508 | "dependencies": {
1509 | "glob-to-regexp": "^0.4.1",
1510 | "graceful-fs": "^4.1.2"
1511 | },
1512 | "engines": {
1513 | "node": ">=10.13.0"
1514 | }
1515 | },
1516 | "node_modules/webpack": {
1517 | "version": "5.76.0",
1518 | "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.76.0.tgz",
1519 | "integrity": "sha512-l5sOdYBDunyf72HW8dF23rFtWq/7Zgvt/9ftMof71E/yUb1YLOBmTgA2K4vQthB3kotMrSj609txVE0dnr2fjA==",
1520 | "dependencies": {
1521 | "@types/eslint-scope": "^3.7.3",
1522 | "@types/estree": "^0.0.51",
1523 | "@webassemblyjs/ast": "1.11.1",
1524 | "@webassemblyjs/wasm-edit": "1.11.1",
1525 | "@webassemblyjs/wasm-parser": "1.11.1",
1526 | "acorn": "^8.7.1",
1527 | "acorn-import-assertions": "^1.7.6",
1528 | "browserslist": "^4.14.5",
1529 | "chrome-trace-event": "^1.0.2",
1530 | "enhanced-resolve": "^5.10.0",
1531 | "es-module-lexer": "^0.9.0",
1532 | "eslint-scope": "5.1.1",
1533 | "events": "^3.2.0",
1534 | "glob-to-regexp": "^0.4.1",
1535 | "graceful-fs": "^4.2.9",
1536 | "json-parse-even-better-errors": "^2.3.1",
1537 | "loader-runner": "^4.2.0",
1538 | "mime-types": "^2.1.27",
1539 | "neo-async": "^2.6.2",
1540 | "schema-utils": "^3.1.0",
1541 | "tapable": "^2.1.1",
1542 | "terser-webpack-plugin": "^5.1.3",
1543 | "watchpack": "^2.4.0",
1544 | "webpack-sources": "^3.2.3"
1545 | },
1546 | "bin": {
1547 | "webpack": "bin/webpack.js"
1548 | },
1549 | "engines": {
1550 | "node": ">=10.13.0"
1551 | },
1552 | "funding": {
1553 | "type": "opencollective",
1554 | "url": "https://opencollective.com/webpack"
1555 | },
1556 | "peerDependenciesMeta": {
1557 | "webpack-cli": {
1558 | "optional": true
1559 | }
1560 | }
1561 | },
1562 | "node_modules/webpack-cli": {
1563 | "version": "4.6.0",
1564 | "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-4.6.0.tgz",
1565 | "integrity": "sha512-9YV+qTcGMjQFiY7Nb1kmnupvb1x40lfpj8pwdO/bom+sQiP4OBMKjHq29YQrlDWDPZO9r/qWaRRywKaRDKqBTA==",
1566 | "dependencies": {
1567 | "@discoveryjs/json-ext": "^0.5.0",
1568 | "@webpack-cli/configtest": "^1.0.2",
1569 | "@webpack-cli/info": "^1.2.3",
1570 | "@webpack-cli/serve": "^1.3.1",
1571 | "colorette": "^1.2.1",
1572 | "commander": "^7.0.0",
1573 | "enquirer": "^2.3.6",
1574 | "execa": "^5.0.0",
1575 | "fastest-levenshtein": "^1.0.12",
1576 | "import-local": "^3.0.2",
1577 | "interpret": "^2.2.0",
1578 | "rechoir": "^0.7.0",
1579 | "v8-compile-cache": "^2.2.0",
1580 | "webpack-merge": "^5.7.3"
1581 | },
1582 | "bin": {
1583 | "webpack-cli": "bin/cli.js"
1584 | },
1585 | "engines": {
1586 | "node": ">=10.13.0"
1587 | },
1588 | "peerDependencies": {
1589 | "webpack": "4.x.x || 5.x.x"
1590 | },
1591 | "peerDependenciesMeta": {
1592 | "@webpack-cli/generators": {
1593 | "optional": true
1594 | },
1595 | "@webpack-cli/migrate": {
1596 | "optional": true
1597 | },
1598 | "webpack-bundle-analyzer": {
1599 | "optional": true
1600 | },
1601 | "webpack-dev-server": {
1602 | "optional": true
1603 | }
1604 | }
1605 | },
1606 | "node_modules/webpack-cli/node_modules/commander": {
1607 | "version": "7.2.0",
1608 | "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
1609 | "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
1610 | "engines": {
1611 | "node": ">= 10"
1612 | }
1613 | },
1614 | "node_modules/webpack-merge": {
1615 | "version": "5.7.3",
1616 | "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.7.3.tgz",
1617 | "integrity": "sha512-6/JUQv0ELQ1igjGDzHkXbVDRxkfA57Zw7PfiupdLFJYrgFqY5ZP8xxbpp2lU3EPwYx89ht5Z/aDkD40hFCm5AA==",
1618 | "dependencies": {
1619 | "clone-deep": "^4.0.1",
1620 | "wildcard": "^2.0.0"
1621 | },
1622 | "engines": {
1623 | "node": ">=10.0.0"
1624 | }
1625 | },
1626 | "node_modules/webpack-sources": {
1627 | "version": "3.2.3",
1628 | "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz",
1629 | "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==",
1630 | "engines": {
1631 | "node": ">=10.13.0"
1632 | }
1633 | },
1634 | "node_modules/which": {
1635 | "version": "2.0.2",
1636 | "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
1637 | "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
1638 | "dependencies": {
1639 | "isexe": "^2.0.0"
1640 | },
1641 | "bin": {
1642 | "node-which": "bin/node-which"
1643 | },
1644 | "engines": {
1645 | "node": ">= 8"
1646 | }
1647 | },
1648 | "node_modules/wildcard": {
1649 | "version": "2.0.0",
1650 | "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.0.tgz",
1651 | "integrity": "sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw=="
1652 | }
1653 | }
1654 | }
1655 |
--------------------------------------------------------------------------------