diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index f0f3e66ecf..9e44d79afa 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -80,7 +80,7 @@ jobs:
uses: rutajdash/prettier-cli-action@v1.0.0
with:
config_path: ${{ github.workspace }}/internal/nginx/modules/.prettierrc
- file_pattern: ${{ github.workspace }}/internal/nginx/modules/*.js
+ file_pattern: ${{ github.workspace }}/internal/nginx/modules/**/*.js
prettier_version: 2.6.2
- name: Prettier Output
if: ${{ failure() }}
@@ -119,9 +119,9 @@ jobs:
- name: Setup Node.js Environment
uses: actions/setup-node@v3
with:
- node_version: 18
+ node-version: 18
- run: npm install mocha@^8.2 esm chai
- - run: npx mocha -r esm ${{ github.workspace }}/internal/nginx/modules/httpmatches_test.js
+ - run: npx mocha -r esm ${{ github.workspace }}/internal/nginx/modules/test/httpmatches.test.js
binary:
name: Build Binary
diff --git a/.gitignore b/.gitignore
index 6e9aaa342d..2f35f87350 100644
--- a/.gitignore
+++ b/.gitignore
@@ -25,3 +25,9 @@ cover.html
# Binary and Artifacts
build/.out
+
+# Node modules
+internal/nginx/modules/node_modules
+
+# JS test coverage
+internal/nginx/modules/coverage
diff --git a/Makefile b/Makefile
index b5d5a92374..6e8cccd43b 100644
--- a/Makefile
+++ b/Makefile
@@ -67,10 +67,10 @@ fmt: ## Run go fmt against code.
.PHONY: njs-fmt
njs-fmt: ## Run prettier against the njs httpmatches module.
- docker run --rm \
- -v $(PWD)/internal/nginx/modules/:/njs-modules/ \
+ docker run --rm -w /modules \
+ -v $(PWD)/internal/nginx/modules/:/modules/ \
node:18 \
- npx prettier@2.6.2 --write njs-modules/ --config=njs-modules/.prettierrc
+ /bin/bash -c "npm install && npm run format"
.PHONY: vet
vet: ## Run go vet against code.
@@ -86,10 +86,10 @@ unit-test: ## Run unit tests for the go code
go tool cover -html=cover.out -o cover.html
njs-unit-test: ## Run unit tests for the njs httpmatches module.
- docker run --rm -w /src \
- -v $(PWD)/internal/nginx/modules/:/src/njs-modules/ \
+ docker run --rm -w /modules \
+ -v $(PWD)/internal/nginx/modules:/modules/ \
node:18 \
- /bin/bash -c "npm install mocha@^8.2 esm chai && npx mocha -r esm njs-modules/httpmatches_test.js"
+ /bin/bash -c "npm install && npm test && npm run clean"
.PHONY: dev-all
dev-all: deps fmt njs-fmt vet lint unit-test njs-unit-test
diff --git a/README.md b/README.md
index 1a764270c0..babefc384e 100644
--- a/README.md
+++ b/README.md
@@ -69,7 +69,7 @@ You can deploy NGINX Kubernetes Gateway on an existing Kubernetes 1.16+ cluster.
1. Create the njs-modules configmap:
```
- kubectl create configmap njs-modules --from-file=internal/nginx/modules/httpmatches.js -n nginx-gateway
+ kubectl create configmap njs-modules --from-file=internal/nginx/modules/src/httpmatches.js -n nginx-gateway
```
1. Deploy the NGINX Kubernetes Gateway:
diff --git a/examples/advanced-routing/README.md b/examples/advanced-routing/README.md
index 26e621698f..5547236853 100644
--- a/examples/advanced-routing/README.md
+++ b/examples/advanced-routing/README.md
@@ -3,6 +3,10 @@
In this example we will deploy NGINX Kubernetes Gateway and configure advanced routing rules for a simple cafe application.
We will use `HTTPRoute` resources to route traffic to the cafe application based on a combination of the request method, headers, and query parameters.
+The cafe application consists of four services: `coffee-v1-svc`, `coffee-v2-svc`, `tea-svc`, and `tea-post-svc`. In the next section we will create the following routing rules for the cafe application:
+- For the path `/coffee` route requests with the header `version` set to `v2` or with the query param `TEST` set to `v2` to `coffee-v2-svc`, and all other requests to `coffee-v1-svc`.
+- For the path `/tea` route POST requests to `tea-post-svc`, and all other requests, such as `GET` requests, to `tea-svc`.
+
## Running the Example
## 1. Deploy NGINX Kubernetes Gateway
@@ -33,9 +37,11 @@ We will use `HTTPRoute` resources to route traffic to the cafe application based
```
kubectl -n default get pods
- NAME READY STATUS RESTARTS AGE
- coffee-6f4b79b975-2sb28 1/1 Running 0 12s
- tea-6fb46d899f-fm7zr 1/1 Running 0 12s
+ NAME READY STATUS RESTARTS AGE
+ coffee-v1-75869cf7ff-vlfpq 1/1 Running 0 17m
+ coffee-v2-67499ff985-2k6cc 1/1 Running 0 17m
+ tea-6fb46d899f-hjzwr 1/1 Running 0 17m
+ tea-post-648dfcdd6c-2rlqb 1/1 Running 0 17m
```
## 3. Configure Routing
@@ -48,51 +54,68 @@ We will use `HTTPRoute` resources to route traffic to the cafe application based
## 4. Test the Application
-We will use `curl` to send requests to the `coffee` and `tea` services.
+We will use `curl` to send requests to the `/coffee` and `/tea` endpoints of the cafe application.
### 4.1 Access coffee
-Send a `POST` request to the path `/coffee` with the headers `X-Demo-Header:Demo-X1` and `version:v1`:
+Send a request with the header `version:v2` and confirm that the response comes from `coffee-v2-svc`:
-```
-curl --resolve cafe.example.com:$GW_PORT:$GW_IP http://cafe.example.com:$GW_PORT/coffee -X POST -H "X-Demo-Header:Demo-X1" -H "version:v1"
-Server address: 10.12.0.18:80
-Server name: coffee-7586895968-r26zn
+```bash
+curl --resolve cafe.example.com:$GW_PORT:$GW_IP http://cafe.example.com:$GW_PORT/coffee -H "version:v2"
+Server address: 10.116.2.67:8080
+Server name: coffee-v2-67499ff985-gw6vt
+...
```
-Header keys are case-insensitive, so we can also access coffee with the following request:
+Send a request with the query parameter `TEST=v2` and confirm that the response comes from `coffee-v2-svc`:
-```
-curl --resolve cafe.example.com:$GW_PORT:$GW_IP http://cafe.example.com:$GW_PORT/coffee -X POST -H "X-DEMO-HEADER:Demo-X1" -H "Version:v1"
-Server address: 10.12.0.18:80
-Server name: coffee-7586895968-r26zn
+```bash
+curl --resolve cafe.example.com:$GW_PORT:$GW_IP http://cafe.example.com:$GW_PORT/coffee?TEST=v2
+Server address: 10.116.2.67:8080
+Server name: coffee-v2-67499ff985-gw6vt
+...
```
-Only `POST` requests to the path `/coffee` with the headers `X-Demo-Header:Demo-X1` and `version:v1` will be able to access coffee.
-For example, try sending the following `GET` request:
-```
-curl --resolve cafe.example.com:$GW_PORT:$GW_IP http://cafe.example.com:$GW_PORT/coffee -H "X-Demo-Header:Demo-X1" -H "version:v1"
-```
+Send a request without the header or the query parameter and confirm the response comes from `coffee-v1-svc`:
-NGINX Kubernetes Gateway returns a 405 since the request method does not match the method defined in the routing rule for `/coffee`.
+```bash
+curl --resolve cafe.example.com:$GW_PORT:$GW_IP http://cafe.example.com:$GW_PORT/coffee
+Server address: 10.116.2.70:8080
+Server name: coffee-v1-75869cf7ff-vlfpq
+...
+```
### 4.2 Access tea
-Send a request to the path `/tea` with the query parameter `Great=Example`:
+Send a POST request and confirm that the response comes from `tea-post-svc`:
+```bash
+curl --resolve cafe.example.com:$GW_PORT:$GW_IP http://cafe.example.com:$GW_PORT/tea -X POST
+Server address: 10.116.2.72:8080
+Server name: tea-post-648dfcdd6c-2rlqb
+...
```
-curl --resolve cafe.example.com:$GW_PORT:$GW_IP http://cafe.example.com:$GW_PORT/tea?Great=Example
-Server address: 10.12.0.19:80
-Server name: tea-7cd44fcb4d-xfw2x
-```
-
-Query parameters are case-sensitive, so the case must match what you specify in the `HTTPRoute` resource.
-Only requests to the path `/tea` with the query parameter `Great=Example` will be able to access tea.
-For example, try sending the following request:
+Send a GET request and confirm that the response comes from `tea-svc`:
-```
+```bash
curl --resolve cafe.example.com:$GW_PORT:$GW_IP http://cafe.example.com:$GW_PORT/tea
+Server address: 10.116.3.30:8080
+Server name: tea-6fb46d899f-hjzwr
+...
```
-NGINX Kubernetes Gateway returns a 404 since the request does not satisfy the routing rule configured for `/tea`.
+The `/tea` endpoint has routing rules configured for GET and POST requests. If you send a request with a different method, NGINX Kubernetes Gateway will return a 404.
+
+Send a PUT request and confirm the 404 Not Found response:
+
+```bash
+curl --resolve cafe.example.com:$GW_PORT:$GW_IP http://cafe.example.com:$GW_PORT/tea -X PUT
+
+
404 Not Found
+
+404 Not Found
+
nginx/1.21.3
+
+
+```
diff --git a/examples/advanced-routing/cafe-routes.yaml b/examples/advanced-routing/cafe-routes.yaml
index 99964a5a7c..970a6ce153 100644
--- a/examples/advanced-routing/cafe-routes.yaml
+++ b/examples/advanced-routing/cafe-routes.yaml
@@ -1,21 +1,5 @@
apiVersion: gateway.networking.k8s.io/v1alpha2
kind: HTTPRoute
-metadata:
- name: cafe
-spec:
- parentRefs:
- - name: gateway
- namespace: nginx-gateway
- sectionName: http
- hostnames:
- - "cafe.example.com"
- rules:
- - backendRefs:
- - name: main
- port: 80
----
-apiVersion: gateway.networking.k8s.io/v1alpha2
-kind: HTTPRoute
metadata:
name: coffee
spec:
@@ -30,14 +14,24 @@ spec:
- path:
type: PathPrefix
value: /coffee
- method: POST
+ backendRefs:
+ - name: coffee-v1-svc
+ port: 80
+ - matches:
+ - path:
+ type: PathPrefix
+ value: /coffee
headers:
- - name: X-Demo-Header # header names are case-insensitive
- value: Demo-X1 # header values are case-sensitive
- name: version
- value: v1
+ value: v2
+ - path:
+ type: PathPrefix
+ value: /coffee
+ queryParams:
+ - name: TEST
+ value: v2
backendRefs:
- - name: coffee
+ - name: coffee-v2-svc
port: 80
---
apiVersion: gateway.networking.k8s.io/v1alpha2
@@ -56,9 +50,15 @@ spec:
- path:
type: PathPrefix
value: /tea
- queryParams:
- - name: Great # query params and values are case-sensitive
- value: Example
+ method: POST
+ backendRefs:
+ - name: tea-post-svc
+ port: 80
+ - matches:
+ - path:
+ type: PathPrefix
+ value: /tea
+ method: GET
backendRefs:
- - name: tea
+ - name: tea-svc
port: 80
diff --git a/examples/advanced-routing/cafe.yaml b/examples/advanced-routing/cafe.yaml
index 2d03ae59ff..40236b7138 100644
--- a/examples/advanced-routing/cafe.yaml
+++ b/examples/advanced-routing/cafe.yaml
@@ -1,19 +1,19 @@
apiVersion: apps/v1
kind: Deployment
metadata:
- name: coffee
+ name: coffee-v1
spec:
replicas: 1
selector:
matchLabels:
- app: coffee
+ app: coffee-v1
template:
metadata:
labels:
- app: coffee
+ app: coffee-v1
spec:
containers:
- - name: coffee
+ - name: coffee-v1
image: nginxdemos/nginx-hello:plain-text
ports:
- containerPort: 8080
@@ -21,7 +21,7 @@ spec:
apiVersion: v1
kind: Service
metadata:
- name: coffee
+ name: coffee-v1-svc
spec:
ports:
- port: 80
@@ -29,7 +29,73 @@ spec:
protocol: TCP
name: http
selector:
- app: coffee
+ app: coffee-v1
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: coffee-v2
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: coffee-v2
+ template:
+ metadata:
+ labels:
+ app: coffee-v2
+ spec:
+ containers:
+ - name: coffee-v2
+ image: nginxdemos/nginx-hello:plain-text
+ ports:
+ - containerPort: 8080
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: coffee-v2-svc
+spec:
+ ports:
+ - port: 80
+ targetPort: 8080
+ protocol: TCP
+ name: http
+ selector:
+ app: coffee-v2
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: tea-post
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: tea-post
+ template:
+ metadata:
+ labels:
+ app: tea-post
+ spec:
+ containers:
+ - name: tea-post
+ image: nginxdemos/nginx-hello:plain-text
+ ports:
+ - containerPort: 8080
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: tea-post-svc
+spec:
+ ports:
+ - port: 80
+ targetPort: 8080
+ protocol: TCP
+ name: http
+ selector:
+ app: tea-post
---
apiVersion: apps/v1
kind: Deployment
@@ -54,7 +120,7 @@ spec:
apiVersion: v1
kind: Service
metadata:
- name: tea
+ name: tea-svc
spec:
ports:
- port: 80
diff --git a/internal/nginx/config/generator.go b/internal/nginx/config/generator.go
index 5293d9f30a..ce2937501f 100644
--- a/internal/nginx/config/generator.go
+++ b/internal/nginx/config/generator.go
@@ -56,42 +56,49 @@ func (g *GeneratorImpl) Generate(conf state.Configuration) ([]byte, Warnings) {
func generate(httpServer state.HTTPServer, serviceStore state.ServiceStore) (server, Warnings) {
warnings := newWarnings()
- locs := make([]location, 0, len(httpServer.PathRules)) // FIXME(pleshakov): expand with rules.Routes
-
- for _, rules := range httpServer.PathRules {
- // number of routes in a group is always at least 1
- // otherwise, it is a bug in the state.Configuration code, so it is OK to panic here
- r := rules.MatchRules[0] // FIXME(pleshakov): for now, we only handle the first route in case there are multiple routes
- address, err := getBackendAddress(r.Source.Spec.Rules[r.RuleIdx].BackendRefs, r.Source.Namespace, serviceStore)
- if err != nil {
- warnings.AddWarning(r.Source, err.Error())
+ locs := make([]location, 0, len(httpServer.PathRules)) // FIXME(pleshakov): expand with rule.Routes
+
+ for _, rule := range httpServer.PathRules {
+ matches := make([]httpMatch, 0, len(rule.MatchRules))
+
+ for ruleIdx, r := range rule.MatchRules {
+
+ address, err := getBackendAddress(r.Source.Spec.Rules[r.RuleIdx].BackendRefs, r.Source.Namespace, serviceStore)
+
+ if err != nil {
+ warnings.AddWarning(r.Source, err.Error())
+ }
+
+ m := r.GetMatch()
+
+ // handle case where the only route is a path-only match
+ // generate a standard location block without http_matches.
+ if len(rule.MatchRules) == 1 && isPathOnlyMatch(m) {
+ locs = append(locs, location{
+ Path: rule.Path,
+ ProxyPass: generateProxyPass(address),
+ })
+ } else {
+ path := createPathForMatch(rule.Path, ruleIdx)
+ locs = append(locs, generateMatchLocation(path, address))
+ matches = append(matches, createHTTPMatch(m, path))
+ }
}
- match, exists := r.GetMatch()
- if exists && matchLocationNeeded(match) {
- // FIXME(kate-osborn): route index is hardcoded to 0 for now.
- // Once we support multiple routes we will need to change this to the index of the current route.
- path := createPathForMatch(rules.Path, 0)
- // generate location block for this rule and match
- mLoc := generateMatchLocation(path, address)
- // generate the http_matches variable value
- b, err := json.Marshal(createHTTPMatch(match, path))
+ if len(matches) > 0 {
+ b, err := json.Marshal(matches)
+
if err != nil {
// panic is safe here because we should never fail to marshal the match unless we constructed it incorrectly.
panic(fmt.Errorf("could not marshal http match: %w", err))
}
- loc := location{
- Path: rules.Path,
+ pathLoc := location{
+ Path: rule.Path,
HTTPMatchVar: string(b),
}
- locs = append(locs, loc, mLoc)
- } else {
- loc := location{
- Path: rules.Path,
- ProxyPass: generateProxyPass(address),
- }
- locs = append(locs, loc)
+
+ locs = append(locs, pathLoc)
}
}
@@ -158,6 +165,8 @@ func createPathForMatch(path string, routeIdx int) string {
// The NJS httpmatches module will lookup this variable on the request object and compare the request against the Method, Headers, and QueryParams contained in httpMatch.
// If the request satisfies the httpMatch, the request will be internally redirected to the location RedirectPath by NGINX.
type httpMatch struct {
+ // Any represents a match with no match conditions.
+ Any bool `json:"any,omitempty"`
// Method is the HTTPMethod of the HTTPRouteMatch.
Method v1alpha2.HTTPMethod `json:"method,omitempty"`
// Headers is a list of HTTPHeaders name value pairs with the format "{name}:{value}".
@@ -173,6 +182,11 @@ func createHTTPMatch(match v1alpha2.HTTPRouteMatch, redirectPath string) httpMat
RedirectPath: redirectPath,
}
+ if isPathOnlyMatch(match) {
+ hm.Any = true
+ return hm
+ }
+
if match.Method != nil {
hm.Method = *match.Method
}
@@ -217,7 +231,6 @@ func createHeaderKeyValString(h v1alpha2.HTTPHeaderMatch) string {
return string(h.Name) + ":" + h.Value
}
-// A match location is needed if the match specifies at least one of the following: Method, Headers, or QueryParams.
-func matchLocationNeeded(match v1alpha2.HTTPRouteMatch) bool {
- return match.Method != nil || match.Headers != nil || match.QueryParams != nil
+func isPathOnlyMatch(match v1alpha2.HTTPRouteMatch) bool {
+ return match.Method == nil && match.Headers == nil && match.QueryParams == nil
}
diff --git a/internal/nginx/config/generator_test.go b/internal/nginx/config/generator_test.go
index 2faa1d7de3..0b698b539b 100644
--- a/internal/nginx/config/generator_test.go
+++ b/internal/nginx/config/generator_test.go
@@ -55,6 +55,17 @@ func TestGenerate(t *testing.T) {
},
Method: helpers.GetHTTPMethodPointer(v1alpha2.HTTPMethodPost),
},
+ {
+ Path: &v1alpha2.HTTPPathMatch{
+ Value: helpers.GetStringPointer("/"),
+ },
+ Method: helpers.GetHTTPMethodPointer(v1alpha2.HTTPMethodPatch),
+ },
+ {
+ Path: &v1alpha2.HTTPPathMatch{
+ Value: helpers.GetStringPointer("/"), // should generate an "any" httpmatch since other matches exists for /
+ },
+ },
},
BackendRefs: []v1alpha2.HTTPBackendRef{
{
@@ -114,7 +125,6 @@ func TestGenerate(t *testing.T) {
Path: &v1alpha2.HTTPPathMatch{
Value: helpers.GetStringPointer("/path-only"),
},
- // matches that only have path specified will not generate an internal location block
},
},
BackendRefs: []v1alpha2.HTTPBackendRef{
@@ -144,6 +154,16 @@ func TestGenerate(t *testing.T) {
RuleIdx: 0,
Source: hr,
},
+ {
+ MatchIdx: 1,
+ RuleIdx: 0,
+ Source: hr,
+ },
+ {
+ MatchIdx: 2,
+ RuleIdx: 0,
+ Source: hr,
+ },
},
},
{
@@ -172,7 +192,7 @@ func TestGenerate(t *testing.T) {
fakeServiceStore := &statefakes.FakeServiceStore{}
fakeServiceStore.ResolveReturns("10.0.0.1", nil)
- expectedMatchString := func(m httpMatch) string {
+ expectedMatchString := func(m []httpMatch) string {
b, err := json.Marshal(m)
if err != nil {
t.Errorf("error marshaling test match: %v", err)
@@ -180,38 +200,56 @@ func TestGenerate(t *testing.T) {
return string(b)
}
- slashMatches := httpMatch{Method: v1alpha2.HTTPMethodPost, RedirectPath: "/_route0"}
- testMatches := httpMatch{
- Method: v1alpha2.HTTPMethodGet,
- Headers: []string{"Version:V1", "test:foo", "my-header:my-value"},
- QueryParams: []string{"GrEat=EXAMPLE", "test=foo=bar"},
- RedirectPath: "/test_route0",
+ slashMatches := []httpMatch{
+ {Method: v1alpha2.HTTPMethodPost, RedirectPath: "/_route0"},
+ {Method: v1alpha2.HTTPMethodPatch, RedirectPath: "/_route1"},
+ {Any: true, RedirectPath: "/_route2"},
}
+ testMatches := []httpMatch{
+ {
+ Method: v1alpha2.HTTPMethodGet,
+ Headers: []string{"Version:V1", "test:foo", "my-header:my-value"},
+ QueryParams: []string{"GrEat=EXAMPLE", "test=foo=bar"},
+ RedirectPath: "/test_route0",
+ },
+ }
+
+ const backendAddr = "http://10.0.0.1:80"
expected := server{
ServerName: "example.com",
Locations: []location{
{
- Path: "/",
- HTTPMatchVar: expectedMatchString(slashMatches),
+ Path: "/_route0",
+ Internal: true,
+ ProxyPass: backendAddr,
},
{
- Path: "/_route0",
+ Path: "/_route1",
Internal: true,
- ProxyPass: "http://10.0.0.1:80",
+ ProxyPass: backendAddr,
},
{
- Path: "/test",
- HTTPMatchVar: expectedMatchString(testMatches),
+ Path: "/_route2",
+ Internal: true,
+ ProxyPass: backendAddr,
+ },
+ {
+ Path: "/",
+ HTTPMatchVar: expectedMatchString(slashMatches),
},
{
Path: "/test_route0",
Internal: true,
ProxyPass: "http://" + nginx502Server,
},
+ {
+ Path: "/test",
+ HTTPMatchVar: expectedMatchString(testMatches),
+ },
{
Path: "/path-only",
- ProxyPass: "http://10.0.0.1:80",
+ ProxyPass: backendAddr,
},
},
}
@@ -519,7 +557,7 @@ func TestMatchLocationNeeded(t *testing.T) {
Value: helpers.GetStringPointer("/path"),
},
},
- expected: false,
+ expected: true,
msg: "path only match",
},
{
@@ -529,7 +567,7 @@ func TestMatchLocationNeeded(t *testing.T) {
},
Method: helpers.GetHTTPMethodPointer(v1alpha2.HTTPMethodGet),
},
- expected: true,
+ expected: false,
msg: "method defined in match",
},
{
@@ -544,7 +582,7 @@ func TestMatchLocationNeeded(t *testing.T) {
},
},
},
- expected: true,
+ expected: false,
msg: "headers defined in match",
},
{
@@ -560,22 +598,24 @@ func TestMatchLocationNeeded(t *testing.T) {
},
},
},
- expected: true,
+ expected: false,
msg: "query params defined in match",
},
}
for _, tc := range tests {
- result := matchLocationNeeded(tc.match)
+ result := isPathOnlyMatch(tc.match)
if result != tc.expected {
- t.Errorf("matchLocationNeeded() returned %t but expected %t for test case %q", result, tc.expected, tc.msg)
+ t.Errorf("isPathOnlyMatch() returned %t but expected %t for test case %q", result, tc.expected, tc.msg)
}
}
}
func TestCreateHTTPMatch(t *testing.T) {
testPath := "/internal_loc"
+
+ testPathMatch := v1alpha2.HTTPPathMatch{Value: helpers.GetStringPointer("/")}
testMethodMatch := helpers.GetHTTPMethodPointer(v1alpha2.HTTPMethodPut)
testHeaderMatches := []v1alpha2.HTTPHeaderMatch{
{
@@ -626,6 +666,7 @@ func TestCreateHTTPMatch(t *testing.T) {
expectedHeaders := []string{"header-1:val-1", "header-2:val-2", "header-3:val-3"}
expectedArgs := []string{"arg1=val1", "arg2=val2=another-val", "arg3===val3"}
+
tests := []struct {
match v1alpha2.HTTPRouteMatch
expected httpMatch
@@ -633,6 +674,17 @@ func TestCreateHTTPMatch(t *testing.T) {
}{
{
match: v1alpha2.HTTPRouteMatch{
+ Path: &testPathMatch,
+ },
+ expected: httpMatch{
+ Any: true,
+ RedirectPath: testPath,
+ },
+ msg: "path only match",
+ },
+ {
+ match: v1alpha2.HTTPRouteMatch{
+ Path: &testPathMatch, // A path match with a method should not set the Any field to true
Method: testMethodMatch,
},
expected: httpMatch{
diff --git a/internal/nginx/modules/README.md b/internal/nginx/modules/README.md
new file mode 100644
index 0000000000..9626e86d87
--- /dev/null
+++ b/internal/nginx/modules/README.md
@@ -0,0 +1,96 @@
+# NGINX JavaScript Modules
+
+This directory contains the [njs](http://nginx.org/en/docs/njs/) modules for NGINX Kubernetes Gateway.
+
+## Prerequisites
+
+We recommend using [nvm](https://github.com/nvm-sh/nvm/blob/master/README.md) to install the following dependencies:
+
+- [Node.js](https://nodejs.org/en/) (version 1.18)
+- [npm](https://docs.npmjs.com/)
+
+Once you've installed Node.js and npm, run `npm install` in this directory to install the rest of the project's dependencies.
+
+## Modules
+
+- [httpmatches](./src/httpmatches.js): a location handler for HTTP requests. It redirects requests to an internal location block based on the request's headers, arguments, and method.
+
+### Helpful Resources for Module Development
+
+When developing njs modules, it's important to remember that njs is a subset of JavaScript, and its compliance with ECMAScript is still evolving.
+Not all JavaScript functionality is available in njs, and njs is not fully compatible with ECMAScript. The following docs are helpful development resources:
+
+- [HTTP njs module](https://nginx.org/en/docs/http/ngx_http_js_module.html)
+- [List of njs properties that are compatible with ECMAScript](http://nginx.org/en/docs/njs/compatibility.html)
+- [List of njs properties, methods, and objects that are not compatible with ECMAScript](http://nginx.org/en/docs/njs/reference.html)
+
+**Note**: You must use the [default export statement](https://developer.mozilla.org/en-US/docs/web/javascript/reference/statements/export) to export functions in an njs module.
+
+## Unit Tests
+
+This project uses the [Mocha](https://mochajs.org/) test framework and the [Chai](https://www.chaijs.com/) assertion library to write BDD-style unit tests. Tests for the modules are placed in the `/tests` directory and named as `.test.js`.
+
+To run unit tests against the [httpmatches](./src/httpmatches.js) modules you must:
+- Use the [default import statement](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import#importing_defaults) to import the module.
+- Run mocha with the `--require esm` option.
+- Mock the [NGINX HTTP Request Object](http://nginx.org/en/docs/njs/reference.html#http) and pass it to the exported function. Not all functions and fields on the HTTP request object need to be mocked, just the ones that are used in the module.
+
+### Run Unit Tests
+
+To run the unit tests:
+
+```bash
+npm test
+```
+
+## Debugging
+
+#### Debug Unit Tests
+
+To debug on the command-line:
+- Set a breakpoint using the [debugger](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/debugger) statement.
+- Run the tests with the inspect argument:
+
+```bash
+npx mocha inspect -r esm
+```
+
+If you are using JetBrains or VSCode for development, you can debug the unit tests in your IDE.
+
+For JetBrains:
+- [Create a run/debug configuration for mocha](https://www.jetbrains.com/help/idea/run-debug-configuration-mocha.html).
+- Add `--require esm` to the `Extra Mocha Options` field in your run/debug configuration.
+
+For VSCode:
+- [Create a debug configuration for mocha](https://dev.to/wakeupmh/debugging-mocha-tests-in-vscode-468a).
+- Add `--require esm` to the configuration args.
+
+#### Log Statements
+
+You can add log statements to debug njs code at runtime. The following log functions are available on the [NGINX HTTP Request Object](http://nginx.org/en/docs/njs/reference.html#http):
+
+Log at error level:
+
+```bash
+r.error(string)
+```
+
+Log at info level:
+
+```bash
+r.log(string)
+```
+
+Log at warn level:
+
+```bash
+r.warn(string)
+```
+
+## Format Code
+
+This project uses [prettier](https://prettier.io/) to lint and format the JavaScript code. To format the code run:
+
+```bash
+npm run format
+```
diff --git a/internal/nginx/modules/httpmatches.js b/internal/nginx/modules/httpmatches.js
deleted file mode 100644
index a771363fb6..0000000000
--- a/internal/nginx/modules/httpmatches.js
+++ /dev/null
@@ -1,97 +0,0 @@
-export default { redirect };
-
-const matchesVariable = 'http_matches';
-
-// FIXME(osborn): Need to add special handling for repeated headers.
-// Should follow guidance from https://www.rfc-editor.org/rfc/rfc7230.html#section-3.2.2.
-function headersMatch(r, headers) {
- for (let i = 0; i < headers.length; i++) {
- const h = headers[i];
- const kv = h.split(':');
- if (kv.length !== 2) {
- r.error('invalid header match: ' + h);
- throw 500;
- }
- // Header names are compared in a case-insensitive manner, meaning header name "FOO" is equivalent to "foo".
- // The NGINX request's headersIn object lookup is case-insensitive as well.
- // This means that r.headersIn['FOO'] is equivalent to r.headersIn['foo'].
- let val = r.headersIn[kv[0]];
- if (!val || val !== kv[1]) {
- throw 404;
- }
- }
-}
-
-function paramsMatch(r, params) {
- for (let i = 0; i < params.length; i++) {
- let p = params[i];
- // We store query parameter matches as strings with the format "key=value"; however, there may be more than one instance of "=" in the string.
- // To recover the key and value, we need to find the first occurrence of "=" in the string.
- const idx = p.indexOf('=');
- // Check for an improperly constructed query parameter match. There are three possible error cases:
- // (1) if the index is -1, then there are no "=" in the string (e.g. "keyvalue")
- // (2) if the index is 0, then there is no value in the string (e.g. "key=").
- // NOTE: While query parameter values are permitted to be empty, the Gateway API Spec forces the value to be a non-empty string.
- // https://github.com/kubernetes-sigs/gateway-api/blob/50e61865db9659111582080daa5ca1a91bbe265d/apis/v1alpha2/httproute_types.go#L375
- // (3) if the index is equal to length -1, then there is no key in the string (e.g. "=value").
- if (idx === -1 || (idx === 0) | (idx === p.length - 1)) {
- r.error('invalid query parameter: ' + p);
- throw 500;
- }
-
- // Divide string into key value using the index.
- let kv = [p.slice(0, idx), p.slice(idx + 1)];
-
- const val = r.args[kv[0]];
- if (!val || val !== kv[1]) {
- throw 404;
- }
- }
-}
-
-function redirect(r) {
- if (!r.variables[matchesVariable]) {
- r.error(
- 'cannot redirect the request; the variable ' +
- matchesVariable +
- ' is not defined on the request object',
- );
- r.return(500);
- return;
- }
-
- const match = JSON.parse(r.variables[matchesVariable]);
-
- if (match.method && match.method !== r.method) {
- r.return(405);
- return;
- }
-
- if (match.headers) {
- try {
- headersMatch(r, match.headers);
- } catch (e) {
- r.return(e);
- return;
- }
- }
-
- if (match.params) {
- try {
- paramsMatch(r, match.params);
- } catch (e) {
- r.return(e);
- return;
- }
- }
-
- // If we pass all the above checks then the request satisfies the http match conditions and we need to redirect to the path.
- // Make sure there is a path to redirect traffic to.
- if (!match.redirectPath) {
- r.error('no path defined in http match');
- r.return(500);
- return;
- }
-
- r.internalRedirect(match.redirectPath);
-}
diff --git a/internal/nginx/modules/httpmatches_test.js b/internal/nginx/modules/httpmatches_test.js
deleted file mode 100644
index 94583c379e..0000000000
--- a/internal/nginx/modules/httpmatches_test.js
+++ /dev/null
@@ -1,281 +0,0 @@
-import redirect from './httpmatches.js';
-
-let expect = require('chai').expect;
-
-// NGINX HTTP Request Object.
-//See documentation for all properties available: http://nginx.org/en/docs/njs/reference.html
-let r = {
- // Test mocks
- return(statusCode) {
- r.testReturned = statusCode;
- },
- internalRedirect(redirectPath) {
- r.testRedirectedTo = redirectPath;
- },
- error(msg) {
- console.log('\terror:', msg);
- },
-};
-const testHeaderMatches = {
- headers: ['header1:VALUE1', 'header2:value2', 'header3:value3'],
- redirectPath: '/headers',
-};
-const testQueryParamMatches = {
- params: ['Arg1=value1', 'arg2=value2=SOME=other=value', 'arg3===value3&*1(*+'],
- redirectPath: '/params',
-};
-const testAllMatchTypes = {
- method: 'GET',
- headers: ['header1:value1', 'header2:value2'],
- params: ['Arg1=value1', 'arg2=value2=SOME=other=value'],
- redirectPath: '/a-match',
-};
-
-describe('redirect', function () {
- beforeEach(function () {
- // reset fields on the test request object
- r.method = 'GET';
- r.variables = {};
- r.args = {};
- r.headersIn = {};
- // properties added for testing
- r.testReturned = 0;
- r.testRedirectedTo = '';
- });
-
- const tests = [
- {
- name: 'returns 500 if http_matches variable is not defined',
- expectedError: 500,
- },
- {
- name: 'returns 500 if http_matches is empty',
- matches: {},
- expectedError: 500,
- },
- {
- name: 'redirects to the redirectPath if no conditions are defined in match',
- matches: {
- redirectPath: 'no-conditions',
- },
- expectedPath: 'no-conditions',
- },
- {
- name: 'returns 405 if method does not match',
- matches: {
- method: 'GET',
- redirectPath: 'get-location',
- },
- expectedError: 405,
- requestModifier: (r) => {
- r.method = 'POST';
- },
- },
- {
- name: 'redirects to match redirectPath if method matches (GET)',
- matches: {
- method: 'GET',
- redirectPath: 'get-location',
- },
- expectedPath: 'get-location',
- },
- {
- name: 'redirects to match redirectPath if method matches (POST)',
- matches: {
- method: 'POST',
- redirectPath: 'post-location',
- },
- expectedPath: 'post-location',
- requestModifier: (r) => {
- r.method = 'POST';
- },
- },
- {
- name: 'returns 404 if no headers exist in request',
- matches: testHeaderMatches,
- expectedError: 404,
- },
- {
- name: 'returns 404 if not all headers exist in request',
- matches: testHeaderMatches,
- expectedError: 404,
- requestModifier: (r) => {
- r.headersIn = {
- header3: 'value3',
- header1: 'VALUE1',
- };
- },
- },
- {
- name: 'returns 404 if all headers exist in request but not all match',
- matches: testHeaderMatches,
- expectedError: 404,
- requestModifier: (r) => {
- r.headersIn = {
- header3: 'value3',
- header1: 'value1', // header values are case-sensitive so this is not equivalent to header1:VALUE1.
- header2: 'value2',
- };
- },
- },
- {
- name: 'returns 500 if a header match is malformed',
- matches: { headers: ['header-without-a-colon'] },
- expectedError: 500,
- },
- {
- name: 'redirects to redirectPath if all headers exist in request and all match',
- matches: testHeaderMatches,
- expectedPath: '/headers',
- requestModifier: (r) => {
- r.headersIn = {
- header3: 'value3',
- header1: 'VALUE1',
- header2: 'value2',
- };
- },
- },
- {
- name: 'returns 404 if no args exist in request',
- matches: testQueryParamMatches,
- expectedError: 404,
- },
- {
- name: 'returns 404 if not all args exist in request',
- matches: testQueryParamMatches,
- expectedError: 404,
- requestModifier: (r) => {
- r.args = {
- Arg1: 'value1',
- arg2: 'value2=SOME=other=value',
- };
- },
- },
- {
- name: 'returns 404 if all args exist in request but not all match',
- matches: testQueryParamMatches,
- expectedError: 404,
- requestModifier: (r) => {
- r.args = {
- Arg1: 'value1',
- arg2: 'value2=SOME=other=value',
- ARg3: '==value3&*1(*+', // query param matching is case sensitive, so this shouldn't match.
- };
- },
- },
- {
- name: 'returns 500 if param match is malformed (no "=")',
- matches: {
- params: ['arg-without-an-equal-sign'],
- },
- expectedError: 500,
- },
- {
- name: 'returns 500 if param match is malformed (no key)',
- matches: {
- params: ['=arg-without-a-key'],
- },
- expectedError: 500,
- },
- {
- name: 'returns 500 if param match is malformed (no value)',
- matches: {
- params: ['arg-without-a-value='],
- },
- expectedError: 500,
- },
- {
- name: 'redirects to redirectPath if all args exist in request and all match',
- matches: testQueryParamMatches,
- expectedPath: '/params',
- requestModifier: (r) => {
- r.args = {
- Arg1: 'value1',
- arg2: 'value2=SOME=other=value',
- arg3: '==value3&*1(*+',
- };
- },
- },
- {
- name: 'returns 405 if method does not match',
- matches: testAllMatchTypes,
- expectedError: 405,
- requestModifier: (r) => {
- r.method = 'POST';
- r.headersIn = {
- header1: 'value1',
- header2: 'value2',
- };
- r.args = {
- Arg1: 'value1',
- arg2: 'value2=SOME=other=value',
- };
- },
- },
- {
- name: 'returns 404 headers do not match',
- matches: testAllMatchTypes,
- expectedError: 404,
- requestModifier: (r) => {
- r.method = 'GET';
- r.headersIn = {
- header1: 'value1',
- };
- r.args = {
- Arg1: 'value1',
- arg2: 'value2=SOME=other=value',
- };
- },
- },
- {
- name: 'returns 404 if args do not match',
- matches: testAllMatchTypes,
- expectedError: 404,
- requestModifier: (r) => {
- r.method = 'GET';
- r.headersIn = {
- header1: 'value1',
- header2: 'value2',
- };
- r.args = {
- arg2: 'value2=SOME=other=value',
- };
- },
- },
- {
- name: 'redirects to redirectPath if all conditions match',
- matches: testAllMatchTypes,
- expectedPath: '/a-match',
- requestModifier: (r) => {
- r.method = 'GET';
- r.headersIn = {
- header1: 'value1',
- header2: 'value2',
- };
- r.args = {
- Arg1: 'value1',
- arg2: 'value2=SOME=other=value',
- };
- },
- },
- ];
-
- tests.forEach((test) => {
- it(test.name, () => {
- if (test.requestModifier) {
- test.requestModifier(r);
- }
- if (test.matches) {
- r.variables = {
- http_matches: JSON.stringify(test.matches),
- };
- }
- redirect.redirect(r);
- if (test.expectedPath) {
- expect(r.testRedirectedTo).to.equal(test.expectedPath);
- } else {
- expect(r.testReturned).to.equal(test.expectedError);
- }
- });
- });
-});
diff --git a/internal/nginx/modules/package-lock.json b/internal/nginx/modules/package-lock.json
new file mode 100644
index 0000000000..530eb0b31e
--- /dev/null
+++ b/internal/nginx/modules/package-lock.json
@@ -0,0 +1,2294 @@
+{
+ "name": "modules",
+ "lockfileVersion": 2,
+ "requires": true,
+ "packages": {
+ "": {
+ "dependencies": {
+ "chai": "^4.3.6",
+ "esm": "^3.2.25",
+ "mocha": "^8.4.0"
+ },
+ "devDependencies": {
+ "c8": "^7.11.3",
+ "prettier": "^2.6.2"
+ }
+ },
+ "node_modules/@bcoe/v8-coverage": {
+ "version": "0.2.3",
+ "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz",
+ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
+ "dev": true
+ },
+ "node_modules/@istanbuljs/schema": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz",
+ "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.0.7.tgz",
+ "integrity": "sha512-8cXDaBBHOr2pQ7j77Y6Vp5VDT2sIqWyWQ56TjEq4ih/a4iST3dItRe8Q9fp0rrIl9DoKhWQtUQz/YpOxLkXbNA==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.4.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.13.tgz",
+ "integrity": "sha512-GryiOJmNcWbovBxTfZSF71V/mXbgcV3MewDe3kIMCLyIh5e7SKAeUZs+rMnJ8jkMolZ/4/VsdBmMrw3l+VdZ3w==",
+ "dev": true
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.13.tgz",
+ "integrity": "sha512-o1xbKhp9qnIAoHJSWd6KlCZfqslL4valSF81H8ImioOAxluWYWOpWkpyktY2vnt4tbrX9XYaxovq6cgowaJp2w==",
+ "dev": true,
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.0.3",
+ "@jridgewell/sourcemap-codec": "^1.4.10"
+ }
+ },
+ "node_modules/@types/istanbul-lib-coverage": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz",
+ "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==",
+ "dev": true
+ },
+ "node_modules/@ungap/promise-all-settled": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz",
+ "integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q=="
+ },
+ "node_modules/ansi-colors": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz",
+ "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz",
+ "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/anymatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz",
+ "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==",
+ "dependencies": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
+ },
+ "node_modules/assertion-error": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz",
+ "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==",
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
+ },
+ "node_modules/binary-extensions": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
+ "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/braces": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
+ "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+ "dependencies": {
+ "fill-range": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/browser-stdout": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz",
+ "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw=="
+ },
+ "node_modules/c8": {
+ "version": "7.11.3",
+ "resolved": "https://registry.npmjs.org/c8/-/c8-7.11.3.tgz",
+ "integrity": "sha512-6YBmsaNmqRm9OS3ZbIiL2EZgi1+Xc4O24jL3vMYGE6idixYuGdy76rIfIdltSKDj9DpLNrcXSonUTR1miBD0wA==",
+ "dev": true,
+ "dependencies": {
+ "@bcoe/v8-coverage": "^0.2.3",
+ "@istanbuljs/schema": "^0.1.3",
+ "find-up": "^5.0.0",
+ "foreground-child": "^2.0.0",
+ "istanbul-lib-coverage": "^3.2.0",
+ "istanbul-lib-report": "^3.0.0",
+ "istanbul-reports": "^3.1.4",
+ "rimraf": "^3.0.2",
+ "test-exclude": "^6.0.0",
+ "v8-to-istanbul": "^9.0.0",
+ "yargs": "^16.2.0",
+ "yargs-parser": "^20.2.9"
+ },
+ "bin": {
+ "c8": "bin/c8.js"
+ },
+ "engines": {
+ "node": ">=10.12.0"
+ }
+ },
+ "node_modules/c8/node_modules/yargs-parser": {
+ "version": "20.2.9",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz",
+ "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/camelcase": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
+ "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/chai": {
+ "version": "4.3.6",
+ "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.6.tgz",
+ "integrity": "sha512-bbcp3YfHCUzMOvKqsztczerVgBKSsEijCySNlHHbX3VG1nskvqjz5Rfso1gGwD6w6oOV3eI60pKuMOV5MV7p3Q==",
+ "dependencies": {
+ "assertion-error": "^1.1.0",
+ "check-error": "^1.0.2",
+ "deep-eql": "^3.0.1",
+ "get-func-name": "^2.0.0",
+ "loupe": "^2.3.1",
+ "pathval": "^1.1.1",
+ "type-detect": "^4.0.5"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/chalk/node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/check-error": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz",
+ "integrity": "sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==",
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/chokidar": {
+ "version": "3.5.1",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.1.tgz",
+ "integrity": "sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw==",
+ "dependencies": {
+ "anymatch": "~3.1.1",
+ "braces": "~3.0.2",
+ "glob-parent": "~5.1.0",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.5.0"
+ },
+ "engines": {
+ "node": ">= 8.10.0"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.1"
+ }
+ },
+ "node_modules/cliui": {
+ "version": "7.0.4",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
+ "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==",
+ "dependencies": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.0",
+ "wrap-ansi": "^7.0.0"
+ }
+ },
+ "node_modules/cliui/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/cliui/node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/cliui/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/cliui/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
+ },
+ "node_modules/convert-source-map": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.8.0.tgz",
+ "integrity": "sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==",
+ "dev": true,
+ "dependencies": {
+ "safe-buffer": "~5.1.1"
+ }
+ },
+ "node_modules/convert-source-map/node_modules/safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+ "dev": true
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
+ "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
+ "dev": true,
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
+ "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==",
+ "dependencies": {
+ "ms": "2.1.2"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/debug/node_modules/ms": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+ },
+ "node_modules/decamelize": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz",
+ "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/deep-eql": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz",
+ "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==",
+ "dependencies": {
+ "type-detect": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=0.12"
+ }
+ },
+ "node_modules/diff": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz",
+ "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==",
+ "engines": {
+ "node": ">=0.3.1"
+ }
+ },
+ "node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
+ },
+ "node_modules/escalade": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
+ "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/esm": {
+ "version": "3.2.25",
+ "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz",
+ "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
+ "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/flat": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz",
+ "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==",
+ "bin": {
+ "flat": "cli.js"
+ }
+ },
+ "node_modules/foreground-child": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz",
+ "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==",
+ "dev": true,
+ "dependencies": {
+ "cross-spawn": "^7.0.0",
+ "signal-exit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+ "hasInstallScript": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/get-caller-file": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+ "engines": {
+ "node": "6.* || 8.* || >= 10.*"
+ }
+ },
+ "node_modules/get-func-name": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz",
+ "integrity": "sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==",
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/glob": {
+ "version": "7.1.6",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
+ "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
+ "dependencies": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.0.4",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ },
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "dependencies": {
+ "is-glob": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/growl": {
+ "version": "1.10.5",
+ "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz",
+ "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==",
+ "engines": {
+ "node": ">=4.x"
+ }
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/he": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
+ "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
+ "bin": {
+ "he": "bin/he"
+ }
+ },
+ "node_modules/html-escaper": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
+ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
+ "dev": true
+ },
+ "node_modules/inflight": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+ "dependencies": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
+ },
+ "node_modules/is-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "dependencies": {
+ "binary-extensions": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-fullwidth-code-point": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
+ "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/is-plain-obj": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz",
+ "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
+ },
+ "node_modules/istanbul-lib-coverage": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz",
+ "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/istanbul-lib-report": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz",
+ "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==",
+ "dev": true,
+ "dependencies": {
+ "istanbul-lib-coverage": "^3.0.0",
+ "make-dir": "^3.0.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/istanbul-lib-report/node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/istanbul-reports": {
+ "version": "3.1.4",
+ "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.4.tgz",
+ "integrity": "sha512-r1/DshN4KSE7xWEknZLLLLDn5CJybV3nw01VTkp6D5jzLuELlcbudfj/eSQFvrKsJuTVCGnePO7ho82Nw9zzfw==",
+ "dev": true,
+ "dependencies": {
+ "html-escaper": "^2.0.0",
+ "istanbul-lib-report": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/js-yaml": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.0.0.tgz",
+ "integrity": "sha512-pqon0s+4ScYUvX30wxQi3PogGFAlUyH0awepWvwkj4jD4v+ova3RiYw8bmA6x2rDrEaj8i/oWKoRxpVNW+Re8Q==",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/log-symbols": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.0.0.tgz",
+ "integrity": "sha512-FN8JBzLx6CzeMrB0tg6pqlGU1wCrXW+ZXGH481kfsBqer0hToTIiHdjH4Mq8xJUbvATujKCvaREGWpGUionraA==",
+ "dependencies": {
+ "chalk": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/loupe": {
+ "version": "2.3.4",
+ "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.4.tgz",
+ "integrity": "sha512-OvKfgCC2Ndby6aSTREl5aCCPTNIzlDfQZvZxNUrBrihDhL3xcrYegTblhmEiCrg2kKQz4XsFIaemE5BF4ybSaQ==",
+ "dependencies": {
+ "get-func-name": "^2.0.0"
+ }
+ },
+ "node_modules/make-dir": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
+ "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
+ "dev": true,
+ "dependencies": {
+ "semver": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
+ "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/mocha": {
+ "version": "8.4.0",
+ "resolved": "https://registry.npmjs.org/mocha/-/mocha-8.4.0.tgz",
+ "integrity": "sha512-hJaO0mwDXmZS4ghXsvPVriOhsxQ7ofcpQdm8dE+jISUOKopitvnXFQmpRR7jd2K6VBG6E26gU3IAbXXGIbu4sQ==",
+ "dependencies": {
+ "@ungap/promise-all-settled": "1.1.2",
+ "ansi-colors": "4.1.1",
+ "browser-stdout": "1.3.1",
+ "chokidar": "3.5.1",
+ "debug": "4.3.1",
+ "diff": "5.0.0",
+ "escape-string-regexp": "4.0.0",
+ "find-up": "5.0.0",
+ "glob": "7.1.6",
+ "growl": "1.10.5",
+ "he": "1.2.0",
+ "js-yaml": "4.0.0",
+ "log-symbols": "4.0.0",
+ "minimatch": "3.0.4",
+ "ms": "2.1.3",
+ "nanoid": "3.1.20",
+ "serialize-javascript": "5.0.1",
+ "strip-json-comments": "3.1.1",
+ "supports-color": "8.1.1",
+ "which": "2.0.2",
+ "wide-align": "1.1.3",
+ "workerpool": "6.1.0",
+ "yargs": "16.2.0",
+ "yargs-parser": "20.2.4",
+ "yargs-unparser": "2.0.0"
+ },
+ "bin": {
+ "_mocha": "bin/_mocha",
+ "mocha": "bin/mocha"
+ },
+ "engines": {
+ "node": ">= 10.12.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/mochajs"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
+ },
+ "node_modules/nanoid": {
+ "version": "3.1.20",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.20.tgz",
+ "integrity": "sha512-a1cQNyczgKbLX9jwbS/+d7W8fX/RfgYR7lVWwWOGIPNgK2m0MWvrGF6/m4kk6U3QcFMnZf3RIhL0v2Jgh/0Uxw==",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "dependencies": {
+ "p-limit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-is-absolute": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/pathval": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz",
+ "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==",
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/prettier": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.6.2.tgz",
+ "integrity": "sha512-PkUpF+qoXTqhOeWL9fu7As8LXsIUZ1WYaJiY/a7McAQzxjk82OF0tibkFXVCDImZtWxbvojFjerkiLb0/q8mew==",
+ "dev": true,
+ "bin": {
+ "prettier": "bin-prettier.js"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ },
+ "funding": {
+ "url": "https://github.com/prettier/prettier?sponsor=1"
+ }
+ },
+ "node_modules/randombytes": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
+ "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
+ "dependencies": {
+ "safe-buffer": "^5.1.0"
+ }
+ },
+ "node_modules/readdirp": {
+ "version": "3.5.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz",
+ "integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==",
+ "dependencies": {
+ "picomatch": "^2.2.1"
+ },
+ "engines": {
+ "node": ">=8.10.0"
+ }
+ },
+ "node_modules/require-directory": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+ "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/rimraf": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
+ "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+ "dev": true,
+ "dependencies": {
+ "glob": "^7.1.3"
+ },
+ "bin": {
+ "rimraf": "bin.js"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ]
+ },
+ "node_modules/semver": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
+ "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+ "dev": true,
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/serialize-javascript": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-5.0.1.tgz",
+ "integrity": "sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA==",
+ "dependencies": {
+ "randombytes": "^2.1.0"
+ }
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/signal-exit": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
+ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
+ "dev": true
+ },
+ "node_modules/string-width": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
+ "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==",
+ "dependencies": {
+ "is-fullwidth-code-point": "^2.0.0",
+ "strip-ansi": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
+ "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
+ "dependencies": {
+ "ansi-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/strip-json-comments": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "8.1.1",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
+ "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/supports-color?sponsor=1"
+ }
+ },
+ "node_modules/test-exclude": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz",
+ "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==",
+ "dev": true,
+ "dependencies": {
+ "@istanbuljs/schema": "^0.1.2",
+ "glob": "^7.1.4",
+ "minimatch": "^3.0.4"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/type-detect": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
+ "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/v8-to-istanbul": {
+ "version": "9.0.0",
+ "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.0.0.tgz",
+ "integrity": "sha512-HcvgY/xaRm7isYmyx+lFKA4uQmfUbN0J4M0nNItvzTvH/iQ9kW5j/t4YSR+Ge323/lrgDAWJoF46tzGQHwBHFw==",
+ "dev": true,
+ "dependencies": {
+ "@jridgewell/trace-mapping": "^0.3.7",
+ "@types/istanbul-lib-coverage": "^2.0.1",
+ "convert-source-map": "^1.6.0"
+ },
+ "engines": {
+ "node": ">=10.12.0"
+ }
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/wide-align": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz",
+ "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==",
+ "dependencies": {
+ "string-width": "^1.0.2 || 2"
+ }
+ },
+ "node_modules/workerpool": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.1.0.tgz",
+ "integrity": "sha512-toV7q9rWNYha963Pl/qyeZ6wG+3nnsyvolaNUS8+R5Wtw6qJPTxIlOP1ZSvcGhEJw+l3HMMmtiNo9Gl61G4GVg=="
+ },
+ "node_modules/wrap-ansi": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/wrap-ansi/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrap-ansi/node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrap-ansi/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrap-ansi/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
+ },
+ "node_modules/y18n": {
+ "version": "5.0.8",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
+ "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/yargs": {
+ "version": "16.2.0",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
+ "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==",
+ "dependencies": {
+ "cliui": "^7.0.2",
+ "escalade": "^3.1.1",
+ "get-caller-file": "^2.0.5",
+ "require-directory": "^2.1.1",
+ "string-width": "^4.2.0",
+ "y18n": "^5.0.5",
+ "yargs-parser": "^20.2.2"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/yargs-parser": {
+ "version": "20.2.4",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz",
+ "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/yargs-unparser": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz",
+ "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==",
+ "dependencies": {
+ "camelcase": "^6.0.0",
+ "decamelize": "^4.0.0",
+ "flat": "^5.0.2",
+ "is-plain-obj": "^2.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/yargs/node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/yargs/node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/yargs/node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/yargs/node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ }
+ },
+ "dependencies": {
+ "@bcoe/v8-coverage": {
+ "version": "0.2.3",
+ "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz",
+ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
+ "dev": true
+ },
+ "@istanbuljs/schema": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz",
+ "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==",
+ "dev": true
+ },
+ "@jridgewell/resolve-uri": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.0.7.tgz",
+ "integrity": "sha512-8cXDaBBHOr2pQ7j77Y6Vp5VDT2sIqWyWQ56TjEq4ih/a4iST3dItRe8Q9fp0rrIl9DoKhWQtUQz/YpOxLkXbNA==",
+ "dev": true
+ },
+ "@jridgewell/sourcemap-codec": {
+ "version": "1.4.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.13.tgz",
+ "integrity": "sha512-GryiOJmNcWbovBxTfZSF71V/mXbgcV3MewDe3kIMCLyIh5e7SKAeUZs+rMnJ8jkMolZ/4/VsdBmMrw3l+VdZ3w==",
+ "dev": true
+ },
+ "@jridgewell/trace-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.13.tgz",
+ "integrity": "sha512-o1xbKhp9qnIAoHJSWd6KlCZfqslL4valSF81H8ImioOAxluWYWOpWkpyktY2vnt4tbrX9XYaxovq6cgowaJp2w==",
+ "dev": true,
+ "requires": {
+ "@jridgewell/resolve-uri": "^3.0.3",
+ "@jridgewell/sourcemap-codec": "^1.4.10"
+ }
+ },
+ "@types/istanbul-lib-coverage": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz",
+ "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==",
+ "dev": true
+ },
+ "@ungap/promise-all-settled": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz",
+ "integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q=="
+ },
+ "ansi-colors": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz",
+ "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA=="
+ },
+ "ansi-regex": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz",
+ "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw=="
+ },
+ "ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "requires": {
+ "color-convert": "^2.0.1"
+ }
+ },
+ "anymatch": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz",
+ "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==",
+ "requires": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ }
+ },
+ "argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
+ },
+ "assertion-error": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz",
+ "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw=="
+ },
+ "balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
+ },
+ "binary-extensions": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
+ "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA=="
+ },
+ "brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "requires": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "braces": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
+ "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+ "requires": {
+ "fill-range": "^7.0.1"
+ }
+ },
+ "browser-stdout": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz",
+ "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw=="
+ },
+ "c8": {
+ "version": "7.11.3",
+ "resolved": "https://registry.npmjs.org/c8/-/c8-7.11.3.tgz",
+ "integrity": "sha512-6YBmsaNmqRm9OS3ZbIiL2EZgi1+Xc4O24jL3vMYGE6idixYuGdy76rIfIdltSKDj9DpLNrcXSonUTR1miBD0wA==",
+ "dev": true,
+ "requires": {
+ "@bcoe/v8-coverage": "^0.2.3",
+ "@istanbuljs/schema": "^0.1.3",
+ "find-up": "^5.0.0",
+ "foreground-child": "^2.0.0",
+ "istanbul-lib-coverage": "^3.2.0",
+ "istanbul-lib-report": "^3.0.0",
+ "istanbul-reports": "^3.1.4",
+ "rimraf": "^3.0.2",
+ "test-exclude": "^6.0.0",
+ "v8-to-istanbul": "^9.0.0",
+ "yargs": "^16.2.0",
+ "yargs-parser": "^20.2.9"
+ },
+ "dependencies": {
+ "yargs-parser": {
+ "version": "20.2.9",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz",
+ "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==",
+ "dev": true
+ }
+ }
+ },
+ "camelcase": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
+ "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA=="
+ },
+ "chai": {
+ "version": "4.3.6",
+ "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.6.tgz",
+ "integrity": "sha512-bbcp3YfHCUzMOvKqsztczerVgBKSsEijCySNlHHbX3VG1nskvqjz5Rfso1gGwD6w6oOV3eI60pKuMOV5MV7p3Q==",
+ "requires": {
+ "assertion-error": "^1.1.0",
+ "check-error": "^1.0.2",
+ "deep-eql": "^3.0.1",
+ "get-func-name": "^2.0.0",
+ "loupe": "^2.3.1",
+ "pathval": "^1.1.1",
+ "type-detect": "^4.0.5"
+ }
+ },
+ "chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "requires": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "dependencies": {
+ "supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "requires": {
+ "has-flag": "^4.0.0"
+ }
+ }
+ }
+ },
+ "check-error": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz",
+ "integrity": "sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA=="
+ },
+ "chokidar": {
+ "version": "3.5.1",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.1.tgz",
+ "integrity": "sha512-9+s+Od+W0VJJzawDma/gvBNQqkTiqYTWLuZoyAsivsI4AaWTCzHG06/TMjsf1cYe9Cb97UCEhjz7HvnPk2p/tw==",
+ "requires": {
+ "anymatch": "~3.1.1",
+ "braces": "~3.0.2",
+ "fsevents": "~2.3.1",
+ "glob-parent": "~5.1.0",
+ "is-binary-path": "~2.1.0",
+ "is-glob": "~4.0.1",
+ "normalize-path": "~3.0.0",
+ "readdirp": "~3.5.0"
+ }
+ },
+ "cliui": {
+ "version": "7.0.4",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
+ "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==",
+ "requires": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.0",
+ "wrap-ansi": "^7.0.0"
+ },
+ "dependencies": {
+ "ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="
+ },
+ "is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="
+ },
+ "string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "requires": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ }
+ },
+ "strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "requires": {
+ "ansi-regex": "^5.0.1"
+ }
+ }
+ }
+ },
+ "color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "requires": {
+ "color-name": "~1.1.4"
+ }
+ },
+ "color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
+ },
+ "concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
+ },
+ "convert-source-map": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.8.0.tgz",
+ "integrity": "sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==",
+ "dev": true,
+ "requires": {
+ "safe-buffer": "~5.1.1"
+ },
+ "dependencies": {
+ "safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+ "dev": true
+ }
+ }
+ },
+ "cross-spawn": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
+ "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
+ "dev": true,
+ "requires": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ }
+ },
+ "debug": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
+ "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==",
+ "requires": {
+ "ms": "2.1.2"
+ },
+ "dependencies": {
+ "ms": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+ }
+ }
+ },
+ "decamelize": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz",
+ "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ=="
+ },
+ "deep-eql": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz",
+ "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==",
+ "requires": {
+ "type-detect": "^4.0.0"
+ }
+ },
+ "diff": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz",
+ "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w=="
+ },
+ "emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
+ },
+ "escalade": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
+ "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw=="
+ },
+ "escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="
+ },
+ "esm": {
+ "version": "3.2.25",
+ "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz",
+ "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA=="
+ },
+ "fill-range": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
+ "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+ "requires": {
+ "to-regex-range": "^5.0.1"
+ }
+ },
+ "find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "requires": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ }
+ },
+ "flat": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz",
+ "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ=="
+ },
+ "foreground-child": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz",
+ "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==",
+ "dev": true,
+ "requires": {
+ "cross-spawn": "^7.0.0",
+ "signal-exit": "^3.0.2"
+ }
+ },
+ "fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
+ },
+ "fsevents": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+ "optional": true
+ },
+ "get-caller-file": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="
+ },
+ "get-func-name": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz",
+ "integrity": "sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig=="
+ },
+ "glob": {
+ "version": "7.1.6",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
+ "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
+ "requires": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.0.4",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ }
+ },
+ "glob-parent": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+ "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+ "requires": {
+ "is-glob": "^4.0.1"
+ }
+ },
+ "growl": {
+ "version": "1.10.5",
+ "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz",
+ "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA=="
+ },
+ "has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="
+ },
+ "he": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
+ "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="
+ },
+ "html-escaper": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
+ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
+ "dev": true
+ },
+ "inflight": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+ "requires": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
+ },
+ "is-binary-path": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
+ "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
+ "requires": {
+ "binary-extensions": "^2.0.0"
+ }
+ },
+ "is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="
+ },
+ "is-fullwidth-code-point": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
+ "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w=="
+ },
+ "is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "requires": {
+ "is-extglob": "^2.1.1"
+ }
+ },
+ "is-number": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="
+ },
+ "is-plain-obj": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz",
+ "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA=="
+ },
+ "isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
+ },
+ "istanbul-lib-coverage": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz",
+ "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==",
+ "dev": true
+ },
+ "istanbul-lib-report": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz",
+ "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==",
+ "dev": true,
+ "requires": {
+ "istanbul-lib-coverage": "^3.0.0",
+ "make-dir": "^3.0.0",
+ "supports-color": "^7.1.0"
+ },
+ "dependencies": {
+ "supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "requires": {
+ "has-flag": "^4.0.0"
+ }
+ }
+ }
+ },
+ "istanbul-reports": {
+ "version": "3.1.4",
+ "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.4.tgz",
+ "integrity": "sha512-r1/DshN4KSE7xWEknZLLLLDn5CJybV3nw01VTkp6D5jzLuELlcbudfj/eSQFvrKsJuTVCGnePO7ho82Nw9zzfw==",
+ "dev": true,
+ "requires": {
+ "html-escaper": "^2.0.0",
+ "istanbul-lib-report": "^3.0.0"
+ }
+ },
+ "js-yaml": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.0.0.tgz",
+ "integrity": "sha512-pqon0s+4ScYUvX30wxQi3PogGFAlUyH0awepWvwkj4jD4v+ova3RiYw8bmA6x2rDrEaj8i/oWKoRxpVNW+Re8Q==",
+ "requires": {
+ "argparse": "^2.0.1"
+ }
+ },
+ "locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "requires": {
+ "p-locate": "^5.0.0"
+ }
+ },
+ "log-symbols": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.0.0.tgz",
+ "integrity": "sha512-FN8JBzLx6CzeMrB0tg6pqlGU1wCrXW+ZXGH481kfsBqer0hToTIiHdjH4Mq8xJUbvATujKCvaREGWpGUionraA==",
+ "requires": {
+ "chalk": "^4.0.0"
+ }
+ },
+ "loupe": {
+ "version": "2.3.4",
+ "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.4.tgz",
+ "integrity": "sha512-OvKfgCC2Ndby6aSTREl5aCCPTNIzlDfQZvZxNUrBrihDhL3xcrYegTblhmEiCrg2kKQz4XsFIaemE5BF4ybSaQ==",
+ "requires": {
+ "get-func-name": "^2.0.0"
+ }
+ },
+ "make-dir": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
+ "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
+ "dev": true,
+ "requires": {
+ "semver": "^6.0.0"
+ }
+ },
+ "minimatch": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
+ "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
+ "requires": {
+ "brace-expansion": "^1.1.7"
+ }
+ },
+ "mocha": {
+ "version": "8.4.0",
+ "resolved": "https://registry.npmjs.org/mocha/-/mocha-8.4.0.tgz",
+ "integrity": "sha512-hJaO0mwDXmZS4ghXsvPVriOhsxQ7ofcpQdm8dE+jISUOKopitvnXFQmpRR7jd2K6VBG6E26gU3IAbXXGIbu4sQ==",
+ "requires": {
+ "@ungap/promise-all-settled": "1.1.2",
+ "ansi-colors": "4.1.1",
+ "browser-stdout": "1.3.1",
+ "chokidar": "3.5.1",
+ "debug": "4.3.1",
+ "diff": "5.0.0",
+ "escape-string-regexp": "4.0.0",
+ "find-up": "5.0.0",
+ "glob": "7.1.6",
+ "growl": "1.10.5",
+ "he": "1.2.0",
+ "js-yaml": "4.0.0",
+ "log-symbols": "4.0.0",
+ "minimatch": "3.0.4",
+ "ms": "2.1.3",
+ "nanoid": "3.1.20",
+ "serialize-javascript": "5.0.1",
+ "strip-json-comments": "3.1.1",
+ "supports-color": "8.1.1",
+ "which": "2.0.2",
+ "wide-align": "1.1.3",
+ "workerpool": "6.1.0",
+ "yargs": "16.2.0",
+ "yargs-parser": "20.2.4",
+ "yargs-unparser": "2.0.0"
+ }
+ },
+ "ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
+ },
+ "nanoid": {
+ "version": "3.1.20",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.20.tgz",
+ "integrity": "sha512-a1cQNyczgKbLX9jwbS/+d7W8fX/RfgYR7lVWwWOGIPNgK2m0MWvrGF6/m4kk6U3QcFMnZf3RIhL0v2Jgh/0Uxw=="
+ },
+ "normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="
+ },
+ "once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "requires": {
+ "wrappy": "1"
+ }
+ },
+ "p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "requires": {
+ "yocto-queue": "^0.1.0"
+ }
+ },
+ "p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "requires": {
+ "p-limit": "^3.0.2"
+ }
+ },
+ "path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="
+ },
+ "path-is-absolute": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="
+ },
+ "path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true
+ },
+ "pathval": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz",
+ "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ=="
+ },
+ "picomatch": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+ "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="
+ },
+ "prettier": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.6.2.tgz",
+ "integrity": "sha512-PkUpF+qoXTqhOeWL9fu7As8LXsIUZ1WYaJiY/a7McAQzxjk82OF0tibkFXVCDImZtWxbvojFjerkiLb0/q8mew==",
+ "dev": true
+ },
+ "randombytes": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
+ "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
+ "requires": {
+ "safe-buffer": "^5.1.0"
+ }
+ },
+ "readdirp": {
+ "version": "3.5.0",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz",
+ "integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==",
+ "requires": {
+ "picomatch": "^2.2.1"
+ }
+ },
+ "require-directory": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+ "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="
+ },
+ "rimraf": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
+ "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+ "dev": true,
+ "requires": {
+ "glob": "^7.1.3"
+ }
+ },
+ "safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
+ },
+ "semver": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
+ "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+ "dev": true
+ },
+ "serialize-javascript": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-5.0.1.tgz",
+ "integrity": "sha512-SaaNal9imEO737H2c05Og0/8LUXG7EnsZyMa8MzkmuHoELfT6txuj0cMqRj6zfPKnmQ1yasR4PCJc8x+M4JSPA==",
+ "requires": {
+ "randombytes": "^2.1.0"
+ }
+ },
+ "shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "requires": {
+ "shebang-regex": "^3.0.0"
+ }
+ },
+ "shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true
+ },
+ "signal-exit": {
+ "version": "3.0.7",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
+ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
+ "dev": true
+ },
+ "string-width": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
+ "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==",
+ "requires": {
+ "is-fullwidth-code-point": "^2.0.0",
+ "strip-ansi": "^4.0.0"
+ }
+ },
+ "strip-ansi": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
+ "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
+ "requires": {
+ "ansi-regex": "^3.0.0"
+ }
+ },
+ "strip-json-comments": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="
+ },
+ "supports-color": {
+ "version": "8.1.1",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
+ "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
+ "requires": {
+ "has-flag": "^4.0.0"
+ }
+ },
+ "test-exclude": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz",
+ "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==",
+ "dev": true,
+ "requires": {
+ "@istanbuljs/schema": "^0.1.2",
+ "glob": "^7.1.4",
+ "minimatch": "^3.0.4"
+ }
+ },
+ "to-regex-range": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+ "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+ "requires": {
+ "is-number": "^7.0.0"
+ }
+ },
+ "type-detect": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
+ "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g=="
+ },
+ "v8-to-istanbul": {
+ "version": "9.0.0",
+ "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.0.0.tgz",
+ "integrity": "sha512-HcvgY/xaRm7isYmyx+lFKA4uQmfUbN0J4M0nNItvzTvH/iQ9kW5j/t4YSR+Ge323/lrgDAWJoF46tzGQHwBHFw==",
+ "dev": true,
+ "requires": {
+ "@jridgewell/trace-mapping": "^0.3.7",
+ "@types/istanbul-lib-coverage": "^2.0.1",
+ "convert-source-map": "^1.6.0"
+ }
+ },
+ "which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "requires": {
+ "isexe": "^2.0.0"
+ }
+ },
+ "wide-align": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz",
+ "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==",
+ "requires": {
+ "string-width": "^1.0.2 || 2"
+ }
+ },
+ "workerpool": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.1.0.tgz",
+ "integrity": "sha512-toV7q9rWNYha963Pl/qyeZ6wG+3nnsyvolaNUS8+R5Wtw6qJPTxIlOP1ZSvcGhEJw+l3HMMmtiNo9Gl61G4GVg=="
+ },
+ "wrap-ansi": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "requires": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "dependencies": {
+ "ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="
+ },
+ "is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="
+ },
+ "string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "requires": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ }
+ },
+ "strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "requires": {
+ "ansi-regex": "^5.0.1"
+ }
+ }
+ }
+ },
+ "wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
+ },
+ "y18n": {
+ "version": "5.0.8",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
+ "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="
+ },
+ "yargs": {
+ "version": "16.2.0",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
+ "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==",
+ "requires": {
+ "cliui": "^7.0.2",
+ "escalade": "^3.1.1",
+ "get-caller-file": "^2.0.5",
+ "require-directory": "^2.1.1",
+ "string-width": "^4.2.0",
+ "y18n": "^5.0.5",
+ "yargs-parser": "^20.2.2"
+ },
+ "dependencies": {
+ "ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="
+ },
+ "is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="
+ },
+ "string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "requires": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ }
+ },
+ "strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "requires": {
+ "ansi-regex": "^5.0.1"
+ }
+ }
+ }
+ },
+ "yargs-parser": {
+ "version": "20.2.4",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz",
+ "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA=="
+ },
+ "yargs-unparser": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz",
+ "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==",
+ "requires": {
+ "camelcase": "^6.0.0",
+ "decamelize": "^4.0.0",
+ "flat": "^5.0.2",
+ "is-plain-obj": "^2.1.0"
+ }
+ },
+ "yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="
+ }
+ }
+}
diff --git a/internal/nginx/modules/package.json b/internal/nginx/modules/package.json
new file mode 100644
index 0000000000..744d3db50a
--- /dev/null
+++ b/internal/nginx/modules/package.json
@@ -0,0 +1,17 @@
+{
+ "dependencies": {
+ "chai": "^4.3.6",
+ "esm": "^3.2.25",
+ "mocha": "^8.4.0"
+ },
+ "scripts": {
+ "format": "prettier -w src/ test/",
+ "lint": "prettier -c src/ test/",
+ "test": "c8 mocha -r esm",
+ "clean": "rm -rf node_modules coverage"
+ },
+ "devDependencies": {
+ "c8": "^7.11.3",
+ "prettier": "^2.6.2"
+ }
+}
diff --git a/internal/nginx/modules/src/httpmatches.js b/internal/nginx/modules/src/httpmatches.js
new file mode 100644
index 0000000000..56adf29415
--- /dev/null
+++ b/internal/nginx/modules/src/httpmatches.js
@@ -0,0 +1,193 @@
+const MATCHES_VARIABLE = 'http_matches';
+const HTTP_CODES = {
+ notFound: 404,
+ internalServerError: 500,
+};
+
+function redirect(r) {
+ let matches;
+
+ try {
+ matches = extractMatchesFromRequest(r);
+ } catch (e) {
+ r.error(e.message);
+ r.return(HTTP_CODES.internalServerError);
+ return;
+ }
+
+ // Matches is a list of http matches in order of precedence.
+ // We will accept the first match that the request satisfies.
+ // If there's a match, redirect request to internal location block.
+ // If an exception occurs, return 500.
+ // If no matches are found, return 404.
+ let match;
+ try {
+ match = findWinningMatch(r, matches);
+ } catch (e) {
+ r.error(e.message);
+ r.return(HTTP_CODES.internalServerError);
+ return;
+ }
+
+ if (!match) {
+ r.return(HTTP_CODES.notFound);
+ return;
+ }
+
+ if (!match.redirectPath) {
+ r.error(
+ `cannot redirect the request; the match ${JSON.stringify(
+ match,
+ )} does not have a redirectPath set`,
+ );
+ r.return(HTTP_CODES.internalServerError);
+ return;
+ }
+
+ r.internalRedirect(match.redirectPath);
+}
+
+function extractMatchesFromRequest(r) {
+ if (!r.variables[MATCHES_VARIABLE]) {
+ throw Error(
+ `cannot redirect the request; the variable ${MATCHES_VARIABLE} is not defined on the request object`,
+ );
+ }
+
+ let matches;
+
+ try {
+ matches = JSON.parse(r.variables[MATCHES_VARIABLE]);
+ } catch (e) {
+ throw Error(
+ `cannot redirect the request; error parsing ${r.variables[MATCHES_VARIABLE]} into a JSON object: ${e}`,
+ );
+ }
+
+ if (!Array.isArray(matches)) {
+ throw Error(`cannot redirect the request; expected a list of matches, got ${matches}`);
+ }
+
+ if (matches.length === 0) {
+ throw Error(`cannot redirect the request; matches is an empty list`);
+ }
+
+ return matches;
+}
+
+function findWinningMatch(r, matches) {
+ for (let i = 0; i < matches.length; i++) {
+ try {
+ let found = testMatch(r, matches[i]);
+ if (found) {
+ return matches[i];
+ }
+ } catch (e) {
+ throw e;
+ }
+ }
+
+ return null;
+}
+
+function testMatch(r, match) {
+ // check for any
+ if (match.any) {
+ return true;
+ }
+
+ // check method
+ if (match.method && r.method !== match.method) {
+ return false;
+ }
+
+ // check headers
+ if (match.headers) {
+ try {
+ let found = headersMatch(r.headersIn, match.headers);
+ if (!found) {
+ return false;
+ }
+ } catch (e) {
+ throw e;
+ }
+ }
+
+ // check params
+ if (match.params) {
+ try {
+ let found = paramsMatch(r.args, match.params);
+ if (!found) {
+ return false;
+ }
+ } catch (e) {
+ throw e;
+ }
+ }
+
+ // all match conditions are satisfied so return true
+ return true;
+}
+
+// FIXME(osborn): Need to add special handling for repeated headers.
+// Should follow guidance from https://www.rfc-editor.org/rfc/rfc7230.html#section-3.2.2.
+function headersMatch(requestHeaders, headers) {
+ for (let i = 0; i < headers.length; i++) {
+ const h = headers[i];
+ const kv = h.split(':');
+
+ if (kv.length !== 2) {
+ throw Error(`invalid header match: ${h}`);
+ }
+ // Header names are compared in a case-insensitive manner, meaning header name "FOO" is equivalent to "foo".
+ // The NGINX request's headersIn object lookup is case-insensitive as well.
+ // This means that requestHeaders['FOO'] is equivalent to requestHeaders['foo'].
+ let val = requestHeaders[kv[0]];
+
+ if (!val || val !== kv[1]) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+function paramsMatch(requestParams, params) {
+ for (let i = 0; i < params.length; i++) {
+ let p = params[i];
+ // We store query parameter matches as strings with the format "key=value"; however, there may be more than one instance of "=" in the string.
+ // To recover the key and value, we need to find the first occurrence of "=" in the string.
+ const idx = params[i].indexOf('=');
+ // Check for an improperly constructed query parameter match. There are three possible error cases:
+ // (1) if the index is -1, then there are no "=" in the string (e.g. "keyvalue")
+ // (2) if the index is 0, then there is no value in the string (e.g. "key=").
+ // NOTE: While query parameter values are permitted to be empty, the Gateway API Spec forces the value to be a non-empty string.
+ // https://github.com/kubernetes-sigs/gateway-api/blob/50e61865db9659111582080daa5ca1a91bbe265d/apis/v1alpha2/httproute_types.go#L375
+ // (3) if the index is equal to length -1, then there is no key in the string (e.g. "=value").
+ if (idx === -1 || (idx === 0) | (idx === p.length - 1)) {
+ throw Error(`invalid query parameter: ${p}`);
+ }
+
+ // Divide string into key value using the index.
+ let kv = [p.slice(0, idx), p.slice(idx + 1)];
+
+ const val = requestParams[kv[0]];
+
+ if (!val || val !== kv[1]) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+export default {
+ redirect,
+ testMatch,
+ findWinningMatch,
+ headersMatch,
+ paramsMatch,
+ extractMatchesFromRequest,
+ HTTP_CODES,
+ MATCHES_VARIABLE,
+};
diff --git a/internal/nginx/modules/test/httpmatches.test.js b/internal/nginx/modules/test/httpmatches.test.js
new file mode 100644
index 0000000000..cb7a6c7550
--- /dev/null
+++ b/internal/nginx/modules/test/httpmatches.test.js
@@ -0,0 +1,400 @@
+import { default as hm } from '../src/httpmatches.js';
+
+let expect = require('chai').expect;
+
+// Creates a NGINX HTTP Request Object for testing.
+// See documentation for all properties available: http://nginx.org/en/docs/njs/reference.html
+function createRequest({ method = '', headers = {}, params = {}, matches = '' } = {}) {
+ let r = {
+ // Test mocks
+ return(statusCode) {
+ r.testReturned = statusCode;
+ },
+ internalRedirect(redirectPath) {
+ r.testRedirectedTo = redirectPath;
+ },
+ error(msg) {
+ console.log('\tngx_error:', msg);
+ },
+ variables: {},
+ };
+
+ if (method) {
+ r.method = method;
+ }
+
+ if (headers) {
+ r.headersIn = headers;
+ }
+
+ if (params) {
+ r.args = params;
+ }
+
+ if (matches) {
+ r.variables[hm.MATCHES_VARIABLE] = matches;
+ }
+
+ return r;
+}
+
+describe('extractMatchesFromRequest', () => {
+ const tests = [
+ {
+ name: 'throws if matches variable does not exist on request',
+ request: createRequest(),
+ expectThrow: true,
+ errSubstring: 'http_matches is not defined',
+ },
+ {
+ name: 'throws if matches variable is not JSON',
+ request: createRequest({ matches: 'not-JSON' }),
+ expectThrow: true,
+ errSubstring: 'error parsing',
+ },
+ {
+ name: 'throws if matches variable is not an array',
+ request: createRequest({ matches: '{}' }),
+ expectThrow: true,
+ errSubstring: 'expected a list of matches',
+ },
+ {
+ name: 'throws if the length of the matches variable is zero',
+ request: createRequest({ matches: '[]' }),
+ expectThrow: true,
+ errSubstring: 'matches is an empty list',
+ },
+ {
+ name: 'does not throw if matches variable is expected list of matches',
+ request: createRequest({ matches: '[{"any":true}]' }),
+ expectThrow: false,
+ },
+ ];
+ tests.forEach((test) => {
+ it(test.name, () => {
+ if (test.expectThrow) {
+ expect(() => hm.extractMatchesFromRequest(test.request)).to.throw(test.errSubstring);
+ } else {
+ expect(() => hm.extractMatchesFromRequest(test.request).to.not.throw());
+ }
+ });
+ });
+});
+
+describe('testMatch', () => {
+ const tests = [
+ {
+ name: 'returns true if any is set to true',
+ match: { any: true },
+ request: createRequest(),
+ expected: true,
+ },
+ {
+ name: 'returns true if method matches and no other conditions are set',
+ match: { method: 'GET' },
+ request: createRequest({ method: 'GET' }),
+ expected: true,
+ },
+ {
+ name: 'returns true if headers match and no other conditions are set',
+ match: { headers: ['header:value'] },
+ request: createRequest({ headers: { header: 'value' } }),
+ expected: true,
+ },
+ {
+ name: 'returns true if query parameters match and no other conditions are set',
+ match: { params: ['key=value'] },
+ request: createRequest({ params: { key: 'value' } }),
+ expected: true,
+ },
+ {
+ name: 'returns true if multiple conditions match',
+ match: { method: 'GET', headers: ['header:value'], params: ['key=value'] },
+ request: createRequest({
+ method: 'GET',
+ headers: { header: 'value' },
+ params: { key: 'value' },
+ }),
+ expected: true,
+ },
+ {
+ name: 'returns false if method does not match',
+ match: { method: 'POST' },
+ request: createRequest({ method: 'GET' }),
+ expected: false,
+ },
+ {
+ name: 'returns false if headers do not match',
+ match: { method: 'GET', headers: ['header:value'] },
+ request: createRequest({ method: 'GET' }), // no headers are set on request
+ expected: false,
+ },
+ {
+ name: 'returns false if query parameters do not match',
+ match: { method: 'GET', headers: ['header:value'], params: ['key=value'] },
+ request: createRequest({ method: 'GET', headers: { header: 'value' } }), // no params set on request
+ expected: false,
+ },
+ {
+ name: 'throws if headers are malformed',
+ match: { headers: ['malformedheader'] },
+ request: createRequest(),
+ expectThrow: true,
+ errSubstring: 'invalid header match',
+ },
+ {
+ name: 'throws if params are malformed',
+ match: { params: ['keyvalue'] },
+ request: createRequest(),
+ expectThrow: true,
+ errSubstring: 'invalid query parameter',
+ },
+ ];
+
+ tests.forEach((test) => {
+ it(test.name, () => {
+ if (test.expectThrow) {
+ expect(() => hm.testMatch(test.request, test.match)).to.throw(test.errSubstring);
+ } else {
+ const result = hm.testMatch(test.request, test.match);
+ expect(result).to.equal(test.expected);
+ }
+ });
+ });
+});
+
+describe('findWinningMatch', () => {
+ const headerMatch = { headers: ['header:value'] };
+ const queryParamMatch = { params: ['key=value'] };
+ const methodMatch = { method: 'POST' };
+ const anyMatch = { any: true };
+ const malformedMatch = { headers: ['malformed'] };
+
+ const tests = [
+ {
+ name: 'returns first match that the request satisfies',
+ matches: [headerMatch, queryParamMatch, methodMatch, anyMatch], // second match should be returned
+ request: createRequest({ method: 'POST', params: { key: 'value' } }),
+ expected: queryParamMatch,
+ },
+ {
+ name: 'returns null when no match exists',
+ matches: [headerMatch, queryParamMatch, methodMatch],
+ request: createRequest({ method: 'GET' }),
+ expected: null,
+ },
+ {
+ name: 'throws if an exception occurs while finding a match',
+ matches: [headerMatch, queryParamMatch, malformedMatch],
+ request: createRequest({ method: 'GET' }),
+ expectThrow: true,
+ errSubstring: 'invalid header match',
+ },
+ ];
+
+ tests.forEach((test) => {
+ it(test.name, () => {
+ test.request.variables = {
+ http_matches: JSON.stringify(test.matches),
+ };
+
+ if (test.expectThrow) {
+ expect(() => hm.findWinningMatch(test.request, test.matches)).to.throw(test.errSubstring);
+ } else {
+ const result = hm.findWinningMatch(test.request, test.matches);
+ expect(result).to.equal(test.expected);
+ }
+ });
+ });
+});
+
+describe('headersMatch', () => {
+ const multipleHeaders = ['header1:VALUE1', 'header2:value2', 'header3:value3']; // case matters for header values
+
+ const tests = [
+ {
+ name: 'throws an error if a header has multiple colons',
+ headers: ['too:many:colons'],
+ expectThrow: true,
+ },
+ {
+ name: 'throws an error if a header has no colon',
+ headers: ['wrong=delimiter'],
+ requestHeaders: {},
+ expectThrow: true,
+ },
+ {
+ name: 'returns false if one of the header values does not match',
+ headers: multipleHeaders,
+ requestHeaders: {
+ header1: 'VALUE1',
+ header2: 'value2',
+ header3: 'wrong-value', // this value does not match
+ },
+ expected: false,
+ },
+ {
+ name: 'returns false if one of the header values case does not match',
+ headers: multipleHeaders,
+ requestHeaders: {
+ header1: 'value1', // this value is not the correct case
+ header2: 'value2',
+ header3: 'value3',
+ },
+ expected: false,
+ },
+ {
+ name: 'returns true if all headers match',
+ headers: multipleHeaders,
+ requestHeaders: {
+ header1: 'VALUE1', // this value is not the correct case
+ header2: 'value2',
+ header3: 'value3',
+ },
+ expected: true,
+ },
+ ];
+
+ tests.forEach((test) => {
+ it(test.name, () => {
+ if (test.expectThrow) {
+ expect(() => hm.headersMatch(test.requestHeaders, test.headers)).to.throw(
+ 'invalid header match',
+ );
+ } else {
+ expect(hm.headersMatch(test.requestHeaders, test.headers)).to.equal(test.expected);
+ }
+ });
+ });
+});
+
+describe('paramsMatch', () => {
+ const params = ['Arg1=value1', 'arg2=value2=SOME=other=value', 'arg3===value3&*1(*+']; // case matters for header values
+
+ const tests = [
+ {
+ name: 'throws an error a param has no key',
+ params: ['=nokey'],
+ expectThrow: true,
+ },
+ {
+ name: 'throws an error if a param has no value',
+ params: ['novalue='],
+ expectThrow: true,
+ },
+ {
+ name: 'throws an error a param has no equal sign delimiter',
+ params: ['keyval'],
+ expectThrow: true,
+ },
+ {
+ name: 'returns false if one of the params is missing from request',
+ params: params,
+ requestParams: ['arg2=value2=SOME=other=value', 'arg3===value3&*1(*+'],
+ expected: false,
+ },
+ {
+ name: 'returns false if one of the param values does not match',
+ params: params,
+ requestParams: ['Arg1=not-value-1', 'arg2=value2=SOME=other=value', 'arg3===value3&*1(*+'],
+ expected: false,
+ },
+ {
+ name: 'returns false if the case of one param values does not match',
+ params: params,
+ requestParams: ['Arg1=VALUE1', 'arg2=value2=SOME=other=value', 'arg3===value3&*1(*+'],
+ expected: false,
+ },
+ {
+ name: 'returns true if all params match',
+ params: params,
+ requestParams: params,
+ expected: false,
+ },
+ ];
+
+ tests.forEach((test) => {
+ it(test.name, () => {
+ if (test.expectThrow) {
+ expect(() => hm.paramsMatch(test.requestParams, test.params)).to.throw(
+ 'invalid query parameter',
+ );
+ } else {
+ expect(hm.paramsMatch(test.requestParams, test.params)).to.equal(test.expected);
+ }
+ });
+ });
+});
+
+describe('redirect', () => {
+ const testAnyMatch = { any: true, redirectPath: '/any' };
+ const testHeaderMatches = {
+ headers: ['header1:VALUE1', 'header2:value2', 'header3:value3'],
+ redirectPath: '/headers',
+ };
+ const testQueryParamMatches = {
+ params: ['Arg1=value1', 'arg2=value2=SOME=other=value', 'arg3===value3&*1(*+'],
+ redirectPath: '/params',
+ };
+ const testAllMatchTypes = {
+ method: 'GET',
+ headers: ['header1:value1', 'header2:value2'],
+ params: ['Arg1=value1', 'arg2=value2=SOME=other=value'],
+ redirectPath: '/a-match',
+ };
+
+ const tests = [
+ {
+ name: 'returns Internal Server Error status code if http_matches variable is not set',
+ request: createRequest(),
+ matches: null,
+ expectedReturn: hm.HTTP_CODES.internalServerError,
+ },
+ {
+ name: 'returns Internal Server Error status code if http_matches contains malformed match',
+ request: createRequest(),
+ matches: [{ headers: ['malformedheader'] }],
+ expectedReturn: hm.HTTP_CODES.internalServerError,
+ },
+ {
+ name: 'returns Not Found status code if request does not satisfy any match',
+ request: createRequest({ method: 'GET' }),
+ matches: [{ method: 'POST' }],
+ expectedReturn: hm.HTTP_CODES.notFound,
+ },
+ {
+ name: 'returns Internal Server Error status code if request satisfies match, but the redirectPath is missing',
+ request: createRequest({ method: 'GET' }),
+ matches: [{ method: 'GET' }],
+ expectedReturn: hm.HTTP_CODES.internalServerError,
+ },
+ {
+ name: 'redirects to the redirectPath of the first match the request satisfies',
+ request: createRequest({
+ method: 'GET',
+ headers: { header1: 'value1', header2: 'value2' },
+ params: { Arg1: 'value1', arg2: 'value2=SOME=other=value' },
+ }),
+ matches: [testHeaderMatches, testQueryParamMatches, testAllMatchTypes, testAnyMatch], // request matches testAllMatchTypes and testAnyMatch. But first match should win.
+ expectedRedirect: '/a-match',
+ },
+ ];
+
+ tests.forEach((test) => {
+ it(test.name, () => {
+ if (test.matches) {
+ // set http_matches variable
+ test.request.variables = {
+ http_matches: JSON.stringify(test.matches),
+ };
+ }
+
+ hm.redirect(test.request);
+ if (test.expectedReturn) {
+ expect(test.request.testReturned).to.equal(test.expectedReturn);
+ } else if (test.expectedRedirect) {
+ expect(test.request.testRedirectedTo).to.equal(test.expectedRedirect);
+ }
+ });
+ });
+});
diff --git a/internal/state/configuration.go b/internal/state/configuration.go
index 864c672796..3cced8eb79 100644
--- a/internal/state/configuration.go
+++ b/internal/state/configuration.go
@@ -3,7 +3,6 @@ package state
import (
"sort"
- metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/gateway-api/apis/v1alpha2"
)
@@ -33,8 +32,10 @@ type PathRule struct {
}
// MatchRule represents a routing rule. It corresponds directly to a Match in the HTTPRoute resource.
+// An HTTPRoute is guaranteed to have at least one rule with one match.
+// If no rule or match is specified by the user, the default rule {{path:{ type: "PathPrefix", value: "/"}}} is set by the schema.
type MatchRule struct {
- // MatchIdx is the index of the rule in the Rule.Matches or -1 if there are no matches.
+ // MatchIdx is the index of the rule in the Rule.Matches.
MatchIdx int
// RuleIdx is the index of the corresponding rule in the HTTPRoute.
RuleIdx int
@@ -42,13 +43,9 @@ type MatchRule struct {
Source *v1alpha2.HTTPRoute
}
-// GetMatch returns the HTTPRouteMatch of the Route and true if it exists.
-// If there is no Match defined on the Route, GetMatch returns an empty HTTPRouteMatch and false.
-func (r *MatchRule) GetMatch() (v1alpha2.HTTPRouteMatch, bool) {
- if r.MatchIdx == -1 {
- return v1alpha2.HTTPRouteMatch{}, false
- }
- return r.Source.Spec.Rules[r.RuleIdx].Matches[r.MatchIdx], true
+// GetMatch returns the HTTPRouteMatch of the Route .
+func (r *MatchRule) GetMatch() v1alpha2.HTTPRouteMatch {
+ return r.Source.Spec.Rules[r.RuleIdx].Matches[r.MatchIdx]
}
// buildConfiguration builds the Configuration from the graph.
@@ -104,12 +101,7 @@ func buildConfiguration(graph *graph) Configuration {
}
for _, r := range rules {
- // sort matches in every PathRule based on the Source timestamp and its namespace/name
- // for conflict resolution of conflicting rules
- // stable sort so that the order of matches within one HTTPRoute is preserved
- sort.SliceStable(r.MatchRules, func(i, j int) bool {
- return lessObjectMeta(&r.MatchRules[i].Source.ObjectMeta, &r.MatchRules[j].Source.ObjectMeta)
- })
+ sortMatchRules(r.MatchRules)
s.PathRules = append(s.PathRules, r)
}
@@ -132,17 +124,6 @@ func buildConfiguration(graph *graph) Configuration {
}
}
-func lessObjectMeta(meta1 *metav1.ObjectMeta, meta2 *metav1.ObjectMeta) bool {
- if meta1.CreationTimestamp.Equal(&meta2.CreationTimestamp) {
- if meta1.Namespace == meta2.Namespace {
- return meta1.Name < meta2.Name
- }
- return meta1.Namespace < meta2.Namespace
- }
-
- return meta1.CreationTimestamp.Before(&meta2.CreationTimestamp)
-}
-
func getPath(path *v1alpha2.HTTPPathMatch) string {
if path == nil || path.Value == nil || *path.Value == "" {
return "/"
diff --git a/internal/state/configuration_test.go b/internal/state/configuration_test.go
index 84f4e5106f..6dcbf50f6e 100644
--- a/internal/state/configuration_test.go
+++ b/internal/state/configuration_test.go
@@ -443,48 +443,29 @@ func TestMatchRuleGetMatch(t *testing.T) {
tests := []struct {
name,
expPath string
- rule MatchRule
- matchExists bool
+ rule MatchRule
}{
{
- name: "match does not exist",
- expPath: "",
- rule: MatchRule{MatchIdx: -1},
- matchExists: false,
+ name: "first match in first rule",
+ expPath: "/path-1",
+ rule: MatchRule{MatchIdx: 0, RuleIdx: 0, Source: hr},
},
{
- name: "first match in first rule",
- expPath: "/path-1",
- rule: MatchRule{MatchIdx: 0, RuleIdx: 0, Source: hr},
- matchExists: true,
+ name: "second match in first rule",
+ expPath: "/path-2",
+ rule: MatchRule{MatchIdx: 1, RuleIdx: 0, Source: hr},
},
{
- name: "second match in first rule",
- expPath: "/path-2",
- rule: MatchRule{MatchIdx: 1, RuleIdx: 0, Source: hr},
- matchExists: true,
- },
- {
- name: "second match in second rule",
- expPath: "/path-4",
- rule: MatchRule{MatchIdx: 1, RuleIdx: 1, Source: hr},
- matchExists: true,
+ name: "second match in second rule",
+ expPath: "/path-4",
+ rule: MatchRule{MatchIdx: 1, RuleIdx: 1, Source: hr},
},
}
for _, tc := range tests {
- actual, exists := tc.rule.GetMatch()
- if !tc.matchExists {
- if exists {
- t.Errorf("rule.GetMatch() incorrectly returned true (match exists) for test case: %q", tc.name)
- }
- } else {
- if !exists {
- t.Errorf("rule.GetMatch() incorrectly returned false (match does not exist) for test case: %q", tc.name)
- }
- if *actual.Path.Value != tc.expPath {
- t.Errorf("rule.GetMatch() returned incorrect match with path: %s, expected path: %s for test case: %q", *actual.Path.Value, tc.expPath, tc.name)
- }
+ actual := tc.rule.GetMatch()
+ if *actual.Path.Value != tc.expPath {
+ t.Errorf("MatchRule.GetMatch() returned incorrect match with path: %s, expected path: %s for test case: %q", *actual.Path.Value, tc.expPath, tc.name)
}
}
}
diff --git a/internal/state/sort.go b/internal/state/sort.go
new file mode 100644
index 0000000000..41add8a2c0
--- /dev/null
+++ b/internal/state/sort.go
@@ -0,0 +1,73 @@
+package state
+
+import (
+ "sort"
+
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+func sortMatchRules(matchRules []MatchRule) {
+ // stable sort is used so that the order of matches (as defined in each HTTPRoute rule) is preserved
+ // this is important, because the winning match is the first match to win.
+ sort.SliceStable(
+ matchRules, func(i, j int) bool {
+ return higherPriority(matchRules[i], matchRules[j])
+ },
+ )
+}
+
+/*
+Returns true if rule1 has a higher priority than rule2.
+
+From the spec:
+ Precedence must be given to the Rule with the largest number of (Continuing on ties):
+ - Characters in a matching non-wildcard hostname.
+ - Characters in a matching hostname.
+ - Characters in a matching path.
+ - Header matches.
+ - Query param matches.
+
+ If ties still exist across multiple Routes, matching precedence MUST be determined in order of the following criteria, continuing on ties:
+ - The oldest Route based on creation timestamp.
+ - The Route appearing first in alphabetical order by “{namespace}/{name}”.
+
+ If ties still exist within the Route that has been given precedence, matching precedence MUST be granted to the first matching rule meeting the above criteria.
+
+higherPriority will determine precedence by comparing len(headers), len(query parameters), creation timestamp, and namespace name. The other criteria are handled by NGINX.
+*/
+func higherPriority(rule1, rule2 MatchRule) bool {
+ // Get the matches from the rules
+ match1 := rule1.GetMatch()
+ match2 := rule2.GetMatch()
+
+ // If both matches exists then compare the number of header matches
+ // The match with the largest number of header matches wins
+ l1 := len(match1.Headers)
+ l2 := len(match2.Headers)
+
+ if l1 != l2 {
+ return l1 > l2
+ }
+ // If the number of headers is equal then compare the number of query param matches
+ // The match with the most query param matches wins
+ l1 = len(match1.QueryParams)
+ l2 = len(match2.QueryParams)
+
+ if l1 != l2 {
+ return l1 > l2
+ }
+
+ // If still tied, compare the object meta of the two routes.
+ return lessObjectMeta(&rule1.Source.ObjectMeta, &rule2.Source.ObjectMeta)
+}
+
+func lessObjectMeta(meta1 *metav1.ObjectMeta, meta2 *metav1.ObjectMeta) bool {
+ if meta1.CreationTimestamp.Equal(&meta2.CreationTimestamp) {
+ if meta1.Namespace == meta2.Namespace {
+ return meta1.Name < meta2.Name
+ }
+ return meta1.Namespace < meta2.Namespace
+ }
+
+ return meta1.CreationTimestamp.Before(&meta2.CreationTimestamp)
+}
diff --git a/internal/state/sort_test.go b/internal/state/sort_test.go
new file mode 100644
index 0000000000..0b6f38ba28
--- /dev/null
+++ b/internal/state/sort_test.go
@@ -0,0 +1,206 @@
+package state
+
+import (
+ "testing"
+ "time"
+
+ "github.com/google/go-cmp/cmp"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "sigs.k8s.io/gateway-api/apis/v1alpha2"
+
+ "github.com/nginxinc/nginx-kubernetes-gateway/internal/helpers"
+)
+
+func TestSort(t *testing.T) {
+ // timestamps
+ earlier := metav1.Now()
+ later := metav1.NewTime(earlier.Add(1 * time.Second))
+
+ // matches
+ pathOnlyMatch := v1alpha2.HTTPRouteMatch{
+ Path: &v1alpha2.HTTPPathMatch{
+ Value: helpers.GetStringPointer("/path"), // path match only (low priority)
+ },
+ }
+ twoHeaderMatch := v1alpha2.HTTPRouteMatch{
+ Path: &v1alpha2.HTTPPathMatch{
+ Value: helpers.GetStringPointer("/path"),
+ },
+ Headers: []v1alpha2.HTTPHeaderMatch{
+ {
+ Name: "header1",
+ Value: "value1",
+ },
+ {
+ Name: "header2",
+ Value: "value2",
+ },
+ },
+ }
+ threeHeaderMatch := v1alpha2.HTTPRouteMatch{
+ Path: &v1alpha2.HTTPPathMatch{
+ Value: helpers.GetStringPointer("/path"),
+ },
+ Headers: []v1alpha2.HTTPHeaderMatch{
+ {
+ Name: "header1",
+ Value: "value1",
+ },
+ {
+ Name: "header2",
+ Value: "value2",
+ },
+ {
+ Name: "header3",
+ Value: "value3",
+ },
+ },
+ }
+ twoHeaderOneParamMatch := v1alpha2.HTTPRouteMatch{
+ Path: &v1alpha2.HTTPPathMatch{
+ Value: helpers.GetStringPointer("/path"),
+ },
+ Headers: []v1alpha2.HTTPHeaderMatch{
+ {
+ Name: "header1",
+ Value: "value1",
+ },
+ {
+ Name: "header2",
+ Value: "value2",
+ },
+ },
+ QueryParams: []v1alpha2.HTTPQueryParamMatch{
+ {
+ Name: "key1",
+ Value: "value1",
+ },
+ },
+ }
+
+ hr1 := v1alpha2.HTTPRoute{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "hr1",
+ Namespace: "test",
+ CreationTimestamp: earlier,
+ },
+ Spec: v1alpha2.HTTPRouteSpec{
+ Rules: []v1alpha2.HTTPRouteRule{
+ {
+ Matches: []v1alpha2.HTTPRouteMatch{pathOnlyMatch},
+ },
+ {
+ Matches: []v1alpha2.HTTPRouteMatch{twoHeaderMatch},
+ },
+ {
+ Matches: []v1alpha2.HTTPRouteMatch{
+ twoHeaderOneParamMatch, // tie decided on params
+ threeHeaderMatch, // tie decided on headers
+ },
+ },
+ },
+ },
+ }
+
+ hr2 := v1alpha2.HTTPRoute{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "hr2",
+ Namespace: "test",
+ CreationTimestamp: later,
+ },
+ Spec: v1alpha2.HTTPRouteSpec{
+ Rules: []v1alpha2.HTTPRouteRule{
+ {
+ Matches: []v1alpha2.HTTPRouteMatch{twoHeaderMatch}, // tie decided on creation timestamp
+ },
+ },
+ },
+ }
+
+ hr3 := v1alpha2.HTTPRoute{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "hr3",
+ Namespace: "a-test", // tie decided by namespace name
+ CreationTimestamp: later,
+ },
+ Spec: v1alpha2.HTTPRouteSpec{
+ Rules: []v1alpha2.HTTPRouteRule{
+ {
+ Matches: []v1alpha2.HTTPRouteMatch{twoHeaderMatch},
+ },
+ },
+ },
+ }
+
+ routes := []MatchRule{
+ {
+ MatchIdx: 0, // pathOnlyMatch
+ RuleIdx: 0,
+ Source: &hr1,
+ },
+ {
+ MatchIdx: 0, // twoHeaderMatch / earlier timestamp
+ RuleIdx: 1,
+ Source: &hr1,
+ },
+ {
+ MatchIdx: 0, // twoHeaderOneParamMatch
+ RuleIdx: 2,
+ Source: &hr1,
+ },
+ {
+ MatchIdx: 1, // threeHeaderMatch
+ RuleIdx: 2,
+ Source: &hr1,
+ },
+ {
+ MatchIdx: 0, // twoHeaderMatch / later timestamp / test/hr2
+ RuleIdx: 0,
+ Source: &hr2,
+ },
+ {
+ MatchIdx: 0, // twoHeaderMatch / later timestamp / a-test/hr3
+ RuleIdx: 0,
+ Source: &hr3,
+ },
+ }
+
+ sortedRoutes := []MatchRule{
+ {
+ MatchIdx: 1, // threeHeaderMatch
+ RuleIdx: 2,
+ Source: &hr1,
+ },
+ {
+ MatchIdx: 0, // twoHeaderOneParamMatch
+ RuleIdx: 2,
+ Source: &hr1,
+ },
+ {
+ MatchIdx: 0, // twoHeaderMatch / earlier timestamp
+ RuleIdx: 1,
+ Source: &hr1,
+ },
+ {
+ MatchIdx: 0, // twoHeaderMatch / later timestamp / a-test/hr3
+ RuleIdx: 0,
+ Source: &hr3,
+ },
+ {
+ MatchIdx: 0, // twoHeaderMatch / later timestamp / test/hr2
+ RuleIdx: 0,
+ Source: &hr2,
+ },
+ {
+ MatchIdx: 0, // pathOnlyMatch
+ RuleIdx: 0,
+ Source: &hr1,
+ },
+ }
+
+ sortMatchRules(routes)
+
+ if diff := cmp.Diff(sortedRoutes, routes); diff != "" {
+ t.Errorf("sortMatchRules() mismatch (-want +got):\n%s", diff)
+ }
+}