Actions: Introduce A11y test (#106806)

This commit is contained in:
Mariell Hoversholm 2025-06-19 08:20:03 +02:00 committed by GitHub
parent bb03ed54d3
commit 1264b5a619
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 1153 additions and 351 deletions

View File

@ -28,7 +28,6 @@ jobs:
persist-credentials: false persist-credentials: false
- uses: dagger/dagger-for-github@e47aba410ef9bb9ed81a4d2a97df31061e5e842e - uses: dagger/dagger-for-github@e47aba410ef9bb9ed81a4d2a97df31061e5e842e
with: with:
version: "8.0.0"
verb: run verb: run
args: go -C grafana run ./pkg/build/cmd artifacts -a targz:grafana:linux/amd64 --grafana-dir="${PWD}/grafana" > out.txt args: go -C grafana run ./pkg/build/cmd artifacts -a targz:grafana:linux/amd64 --grafana-dir="${PWD}/grafana" > out.txt
- run: mv "$(cat out.txt)" grafana.tar.gz - run: mv "$(cat out.txt)" grafana.tar.gz
@ -119,7 +118,6 @@ jobs:
- name: Run E2E tests - name: Run E2E tests
uses: dagger/dagger-for-github@e47aba410ef9bb9ed81a4d2a97df31061e5e842e uses: dagger/dagger-for-github@e47aba410ef9bb9ed81a4d2a97df31061e5e842e
with: with:
version: "8.0.0"
verb: run verb: run
args: go run ./pkg/build/e2e --package=grafana.tar.gz args: go run ./pkg/build/e2e --package=grafana.tar.gz
--suite=${{ matrix.path }} --suite=${{ matrix.path }}
@ -139,12 +137,50 @@ jobs:
path: videos path: videos
retention-days: 1 retention-days: 1
run-a11y-test:
needs:
- build-grafana
- build-e2e-runner
name: A11y test
runs-on: ubuntu-latest-8-cores
permissions:
contents: read
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/download-artifact@v4
with:
name: ${{ needs.build-grafana.outputs.artifact }}
- uses: actions/download-artifact@v4
with:
name: ${{ needs.build-e2e-runner.outputs.artifact }}
- name: chmod +x
run: chmod +x ./e2e-runner
- name: Run PR a11y test
if: github.event_name == 'pull_request'
uses: dagger/dagger-for-github@e47aba410ef9bb9ed81a4d2a97df31061e5e842e
with:
verb: run
args: go run ./pkg/build/a11y --package=grafana.tar.gz
--flags="--json --config ./.pa11yci-pr.conf.js"
- name: Run non-PR a11y test
if: github.event_name != 'pull_request'
uses: dagger/dagger-for-github@e47aba410ef9bb9ed81a4d2a97df31061e5e842e
with:
verb: run
args: go run ./pkg/build/a11y --package=grafana.tar.gz
--flags="--json --config ./.pa11yci.conf.js"
# This is the job that is actually required by rulesets. # This is the job that is actually required by rulesets.
# We want to only require one job instead of all the individual tests. # We want to only require one job instead of all the individual tests.
# Future work also allows us to start skipping some tests based on changed files. # Future work also allows us to start skipping some tests based on changed files.
required-e2e-tests: required-e2e-tests:
needs: needs:
- run-e2e-tests - run-e2e-tests
# a11y test is not listed on purpose: it is not an important E2E test.
# It is also totally fine to fail right now.
# always() is the best function here. # always() is the best function here.
# success() || failure() will skip this function if any need is also skipped. # success() || failure() will skip this function if any need is also skipped.
# That means conditional test suites will fail the entire requirement check. # That means conditional test suites will fail the entire requirement check.

View File

@ -0,0 +1,135 @@
package a11y
import (
"context"
"fmt"
"io"
"os"
"os/exec"
"path"
"github.com/grafana/grafana/e2e/internal/fpaths"
"github.com/grafana/grafana/e2e/internal/outs"
"github.com/urfave/cli/v3"
)
func NewCmd() *cli.Command {
return &cli.Command{
Name: "a11y",
Usage: "Run accessibility tests on the Grafana frontend",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "config",
Usage: "Path to the accessibility test configuration file",
Required: true,
TakesFile: true,
},
&cli.BoolFlag{
Name: "json",
Usage: "Output results in JSON format",
Value: false,
},
&cli.StringFlag{
Name: "grafana-host",
Usage: "Host for the Grafana server",
Value: "localhost",
},
&cli.Uint16Flag{
Name: "grafana-port",
Usage: "Port for the Grafana server",
Value: 3001,
},
&cli.BoolFlag{
Name: "start-grafana",
Usage: "Start and wait for Grafana before running the tests",
Value: true,
Category: "Grafana Server",
},
&cli.StringFlag{
Name: "license-path",
Usage: "Path to the Grafana Enterprise license file (optional; requires --start-grafana)",
Value: "",
TakesFile: true,
Category: "Grafana Server",
},
},
Action: runAction,
}
}
func runAction(ctx context.Context, c *cli.Command) error {
cfgPath, err := fpaths.NormalisePath(c.String("config"))
if err != nil {
return fmt.Errorf("failed to normalise config path %q: %w", c.String("config"), err)
}
repoRoot, err := fpaths.RepoRoot(ctx, ".")
if err != nil {
return fmt.Errorf("failed to get repository root: %w", err)
}
ctx, cancel := context.WithCancel(ctx)
defer cancel()
if c.Bool("start-grafana") {
startServerPath := path.Join(repoRoot, "scripts", "grafana-server", "start-server")
waitForGrafanaPath := path.Join(repoRoot, "scripts", "grafana-server", "wait-for-grafana")
go func() {
defer cancel()
var args []string
if c.String("license-path") != "" {
args = append(args, c.String("license-path"))
}
//nolint:gosec
cmd := exec.CommandContext(ctx, startServerPath, args...)
cmd.Dir = repoRoot
cmd.Env = os.Environ()
cmd.Env = append(cmd.Env, fmt.Sprintf("TZ=%s", c.String("timezone")))
cmd.Stdout = prefixGrafana(os.Stdout)
cmd.Stderr = prefixGrafana(os.Stderr)
cmd.Stdin = nil
if err := cmd.Run(); err != nil {
fmt.Println("Error running Grafana:", err)
}
}()
//nolint:gosec
cmd := exec.CommandContext(ctx, waitForGrafanaPath)
cmd.Dir = repoRoot
cmd.Env = os.Environ()
cmd.Env = append(cmd.Env, fmt.Sprintf("TZ=%s", c.String("timezone")))
cmd.Stdout = prefixGrafana(os.Stdout)
cmd.Stderr = prefixGrafana(os.Stderr)
cmd.Stdin = nil
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to wait for Grafana: %w", err)
}
}
args := []string{"run", "pa11y-ci", "--config", cfgPath}
if c.Bool("json") {
args = append(args, "--json")
}
//nolint:gosec
cmd := exec.CommandContext(ctx, "yarn", args...)
cmd.Dir = repoRoot
cmd.Env = os.Environ()
cmd.Env = append(cmd.Env,
fmt.Sprintf("HOST=%s", c.String("grafana-host")),
fmt.Sprintf("PORT=%d", c.Uint16("grafana-port")))
cmd.Stdout = prefixA11y(os.Stdout)
cmd.Stderr = prefixA11y(os.Stderr)
cmd.Stdin = os.Stdin
return cmd.Run()
}
func prefixA11y(w io.Writer) io.Writer {
return outs.Prefix(w, "A11y", outs.CyanColor)
}
func prefixGrafana(w io.Writer) io.Writer {
return outs.Prefix(w, "Grafana", outs.YellowColor)
}

View File

@ -0,0 +1,251 @@
package cypress
import (
"context"
"fmt"
"io"
"os"
"os/exec"
"path"
"regexp"
"strings"
"time"
"github.com/grafana/grafana/e2e/internal/fpaths"
"github.com/grafana/grafana/e2e/internal/outs"
"github.com/urfave/cli/v3"
)
func NewCmd() *cli.Command {
return &cli.Command{
Name: "cypress",
Usage: "Run a Cypress test suite",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "command",
Usage: "Cypress command to run. 'open' can be useful for development (enum: run, open)",
Value: "run",
Validator: func(s string) error {
if s != "run" && s != "open" {
return fmt.Errorf("invalid command: %s, must be 'run' or 'open'", s)
}
return nil
},
},
&cli.StringFlag{
Name: "browser",
Usage: "Browser to run tests with (e.g.: chrome, electron)",
Value: "chrome",
},
&cli.StringFlag{
Name: "grafana-base-url",
Usage: "Base URL for Grafana",
Value: "http://localhost:3001",
},
&cli.BoolFlag{
Name: "cypress-video",
Usage: "Enable Cypress video recordings",
Value: false,
},
&cli.BoolFlag{
Name: "smtp-plugin",
Usage: "Enable SMTP plugin",
Value: false,
},
&cli.BoolFlag{
Name: "benchmark-plugin",
Usage: "Enable Benchmark plugin",
Value: false,
},
&cli.BoolFlag{
Name: "slowmo",
Usage: "Slow down the test run",
Value: false,
},
&cli.StringSliceFlag{
Name: "env",
Usage: "Additional Cypress environment variables to set (format: KEY=VALUE)",
Validator: func(s []string) error {
pattern := regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*=.*`)
for _, v := range s {
if !pattern.MatchString(v) {
return fmt.Errorf("invalid environment variable format: %s, must be KEY=VALUE", v)
}
}
return nil
},
},
&cli.StringSliceFlag{
Name: "parameters",
Usage: "Additional parameters to pass to the Cypress command (e.g. --headed)",
},
&cli.DurationFlag{
Name: "timeout",
Usage: "Timeout for the Cypress command (precision: milliseconds)",
Value: time.Second * 30,
Validator: func(d time.Duration) error {
if d < 0 {
return fmt.Errorf("timeout must be a positive duration")
}
if d.Round(time.Millisecond) != d {
return fmt.Errorf("timeout must be a whole number of milliseconds")
}
return nil
},
},
&cli.BoolFlag{
Name: "start-grafana",
Usage: "Start and wait for Grafana before running the tests",
Value: true,
Category: "Grafana Server",
},
&cli.StringFlag{
Name: "license-path",
Usage: "Path to the Grafana Enterprise license file (optional; requires --start-grafana)",
Value: "",
TakesFile: true,
Category: "Grafana Server",
},
&cli.BoolFlag{
Name: "image-renderer",
Usage: "Install the image renderer plugin (requires --start-grafana)",
Category: "Grafana Server",
},
&cli.StringFlag{
Name: "suite",
Usage: "Path to the suite to run (e.g. './e2e/dashboards-suite')",
TakesFile: true,
Required: true,
},
},
Action: runAction,
}
}
func runAction(ctx context.Context, c *cli.Command) error {
suitePath := c.String("suite")
suitePath, err := fpaths.NormalisePath(suitePath)
if err != nil {
return fmt.Errorf("failed to normalise suite path: %w", err)
}
repoRoot, err := fpaths.RepoRoot(ctx, suitePath)
if err != nil {
return fmt.Errorf("failed to get git repo root: %w", err)
}
screenshotsFolder := path.Join(suitePath, "screenshots")
videosFolder := path.Join(suitePath, "videos")
fileServerFolder := path.Join(repoRoot, "e2e", "cypress")
fixturesFolder := path.Join(fileServerFolder, "fixtures")
downloadsFolder := path.Join(fileServerFolder, "downloads")
benchmarkPluginResultsFolder := path.Join(suitePath, "benchmark-results")
reporter := path.Join(repoRoot, "e2e", "log-reporter.js")
env := map[string]string{
"BENCHMARK_PLUGIN_ENABLED": fmt.Sprintf("%t", c.Bool("benchmark-plugin")),
"SMTP_PLUGIN_ENABLED": fmt.Sprintf("%t", c.Bool("smtp-plugin")),
"BENCHMARK_PLUGIN_RESULTS_FOLDER": benchmarkPluginResultsFolder,
"SLOWMO": "0",
"BASE_URL": c.String("grafana-base-url"),
}
for _, v := range c.StringSlice("env") {
parts := strings.SplitN(v, "=", 2)
if len(parts) != 2 {
return fmt.Errorf("invalid environment variable format: %s, must be KEY=VALUE", v)
}
env[parts[0]] = parts[1]
}
cypressConfig := map[string]string{
"screenshotsFolder": screenshotsFolder,
"fixturesFolder": fixturesFolder,
"videosFolder": videosFolder,
"downloadsFolder": downloadsFolder,
"fileServerFolder": fileServerFolder,
"reporter": reporter,
"specPattern": path.Join(suitePath, "*.spec.ts"),
"defaultCommandTimeout": fmt.Sprintf("%d", c.Duration("timeout").Milliseconds()),
"viewportWidth": "1920",
"viewportHeight": "1080",
"trashAssetsBeforeRuns": "false",
"baseUrl": c.String("grafana-base-url"),
"video": fmt.Sprintf("%t", c.Bool("cypress-video")),
}
ctx, cancel := context.WithCancel(ctx)
defer cancel()
if c.Bool("start-grafana") {
startServerPath := path.Join(repoRoot, "scripts", "grafana-server", "start-server")
waitForGrafanaPath := path.Join(repoRoot, "scripts", "grafana-server", "wait-for-grafana")
go func() {
defer cancel()
var args []string
if c.String("license-path") != "" {
args = append(args, c.String("license-path"))
}
//nolint:gosec
cmd := exec.CommandContext(ctx, startServerPath, args...)
cmd.Dir = repoRoot
cmd.Env = os.Environ()
cmd.Env = append(cmd.Env, fmt.Sprintf("TZ=%s", c.String("timezone")))
if c.Bool("image-renderer") {
cmd.Env = append(cmd.Env, "INSTALL_IMAGE_RENDERER=true")
}
cmd.Stdout = prefixGrafana(os.Stdout)
cmd.Stderr = prefixGrafana(os.Stderr)
cmd.Stdin = nil
if err := cmd.Run(); err != nil {
fmt.Println("Error running Grafana:", err)
}
}()
//nolint:gosec
cmd := exec.CommandContext(ctx, waitForGrafanaPath)
cmd.Dir = repoRoot
cmd.Env = os.Environ()
cmd.Env = append(cmd.Env, fmt.Sprintf("TZ=%s", c.String("timezone")))
cmd.Stdout = prefixGrafana(os.Stdout)
cmd.Stderr = prefixGrafana(os.Stderr)
cmd.Stdin = nil
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to wait for Grafana: %w", err)
}
}
args := []string{"run", "cypress", c.String("command"),
"--env", joinCypressCfg(env),
"--config", joinCypressCfg(cypressConfig),
"--browser", c.String("browser")}
args = append(args, c.StringSlice("parameters")...)
//nolint:gosec
cmd := exec.CommandContext(ctx, "yarn", args...)
cmd.Dir = repoRoot
cmd.Env = os.Environ()
cmd.Env = append(cmd.Env, fmt.Sprintf("TZ=%s", c.String("timezone")))
cmd.Stdout = prefixCypress(os.Stdout)
cmd.Stderr = prefixCypress(os.Stderr)
cmd.Stdin = os.Stdin
return cmd.Run()
}
func joinCypressCfg(cfg map[string]string) string {
config := make([]string, 0, len(cfg))
for k, v := range cfg {
config = append(config, fmt.Sprintf("%s=%s", k, v))
}
return strings.Join(config, ",")
}
func prefixCypress(w io.Writer) io.Writer {
return outs.Prefix(w, "Cypress", outs.CyanColor)
}
func prefixGrafana(w io.Writer) io.Writer {
return outs.Prefix(w, "Grafana", outs.YellowColor)
}

25
e2e/internal/cmd/root.go Normal file
View File

@ -0,0 +1,25 @@
package cmd
import (
"github.com/grafana/grafana/e2e/internal/cmd/a11y"
"github.com/grafana/grafana/e2e/internal/cmd/cypress"
"github.com/urfave/cli/v3"
)
func Root() *cli.Command {
return &cli.Command{
Name: "e2e",
Usage: "Run an end-to-end test suite",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "timezone",
Usage: "Timezone to set for all containers (e.g. 'America/New_York')",
Value: "Pacific/Honolulu",
},
},
Commands: []*cli.Command{
a11y.NewCmd(),
cypress.NewCmd(),
},
}
}

View File

@ -0,0 +1,35 @@
package fpaths
import (
"context"
"fmt"
"os/exec"
"path"
"path/filepath"
"strings"
)
// RepoRoot finds the root directory of the git repository.
func RepoRoot(ctx context.Context, dir string) (string, error) {
cmd := exec.CommandContext(ctx, "git", "rev-parse", "--show-toplevel")
cmd.Dir = dir
out, err := cmd.Output()
if err != nil {
return "", fmt.Errorf("failed to get git repo root: %w", err)
}
p := strings.TrimSpace(string(out))
p, err = NormalisePath(p)
if err != nil {
return "", fmt.Errorf("failed to normalise git repo root path: %w", err)
}
return p, nil
}
// NormalisePath converts a path to an absolute path, cleans it, and converts it to a forward-slash format.
func NormalisePath(p string) (string, error) {
absPath, err := filepath.Abs(p)
if err != nil {
return "", fmt.Errorf("failed to get absolute path: %w", err)
}
return path.Clean(filepath.ToSlash(absPath)), nil
}

View File

@ -0,0 +1,65 @@
package outs
import (
"bytes"
"io"
"os"
"sync"
)
const (
ResetColor = "\033[0m"
YellowColor = "\033[0;33m"
CyanColor = "\033[0;36m"
)
func Prefix(w io.Writer, name, colour string) io.Writer {
if _, ok := os.LookupEnv("CI"); ok {
return newWrappingOutput(name+": ", "", w)
}
return newWrappingOutput(colour+name+": ", ResetColor, w)
}
var _ io.Writer = (*wrappingOutput)(nil)
type wrappingOutput struct {
prefix string
suffix string
mu *sync.Mutex
inner io.Writer
writtenPrefix bool
}
func newWrappingOutput(prefix, suffix string, inner io.Writer) *wrappingOutput {
return &wrappingOutput{
prefix: prefix,
suffix: suffix,
mu: &sync.Mutex{},
inner: inner,
}
}
func (p *wrappingOutput) Write(b []byte) (int, error) {
p.mu.Lock()
defer p.mu.Unlock()
for line := range bytes.Lines(b) {
if !p.writtenPrefix {
if _, err := p.inner.Write([]byte(p.prefix)); err != nil {
return 0, err
}
p.writtenPrefix = true
}
if _, err := p.inner.Write(line); err != nil {
return 0, err
}
if bytes.HasSuffix(line, []byte("\n")) {
p.writtenPrefix = false
if _, err := p.inner.Write([]byte(p.suffix)); err != nil {
return 0, err
}
}
}
return len(b), nil
}

View File

@ -1,351 +1,21 @@
package main package main
import ( import (
"bytes"
"context" "context"
"fmt" "fmt"
"io"
"os" "os"
"os/exec"
"os/signal" "os/signal"
"path"
"path/filepath"
"regexp"
"strings"
"sync"
"time"
"github.com/urfave/cli/v3" "github.com/grafana/grafana/e2e/internal/cmd"
) )
func main() { func main() {
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt) ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
defer cancel() defer cancel()
if err := Run().Run(ctx, os.Args); err != nil { if err := cmd.Root().Run(ctx, os.Args); err != nil {
cancel() cancel()
fmt.Println(err) fmt.Println(err)
os.Exit(1) os.Exit(1)
} }
} }
func Run() *cli.Command {
var suitePath string
return &cli.Command{
Name: "e2e",
Usage: "Run the test suite",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "command",
Usage: "Cypress command to run. 'open' can be useful for development (enum: run, open)",
Value: "run",
Validator: func(s string) error {
if s != "run" && s != "open" {
return fmt.Errorf("invalid command: %s, must be 'run' or 'open'", s)
}
return nil
},
},
&cli.StringFlag{
Name: "browser",
Usage: "Browser to run tests with (e.g.: chrome, electron)",
Value: "chrome",
},
&cli.StringFlag{
Name: "grafana-base-url",
Usage: "Base URL for Grafana",
Value: "http://localhost:3001",
},
&cli.BoolFlag{
Name: "cypress-video",
Usage: "Enable Cypress video recordings",
Value: false,
},
&cli.BoolFlag{
Name: "smtp-plugin",
Usage: "Enable SMTP plugin",
Value: false,
},
&cli.BoolFlag{
Name: "benchmark-plugin",
Usage: "Enable Benchmark plugin",
Value: false,
},
&cli.BoolFlag{
Name: "slowmo",
Usage: "Slow down the test run",
Value: false,
},
&cli.StringSliceFlag{
Name: "env",
Usage: "Additional Cypress environment variables to set (format: KEY=VALUE)",
Validator: func(s []string) error {
pattern := regexp.MustCompile(`^[a-zA-Z_][a-zA-Z0-9_]*=.*`)
for _, v := range s {
if !pattern.MatchString(v) {
return fmt.Errorf("invalid environment variable format: %s, must be KEY=VALUE", v)
}
}
return nil
},
},
&cli.StringSliceFlag{
Name: "parameters",
Usage: "Additional parameters to pass to the Cypress command (e.g. --headed)",
},
&cli.DurationFlag{
Name: "timeout",
Usage: "Timeout for the Cypress command (precision: milliseconds)",
Value: time.Second * 30,
Validator: func(d time.Duration) error {
if d < 0 {
return fmt.Errorf("timeout must be a positive duration")
}
if d.Round(time.Millisecond) != d {
return fmt.Errorf("timeout must be a whole number of milliseconds")
}
return nil
},
},
&cli.StringFlag{
Name: "timezone",
Usage: "Timezone to set for the Cypress run (e.g. 'America/New_York')",
Value: "Pacific/Honolulu",
},
&cli.BoolFlag{
Name: "start-grafana",
Usage: "Start and wait for Grafana before running the tests",
Value: true,
Category: "Grafana Server",
},
&cli.StringFlag{
Name: "license-path",
Usage: "Path to the Grafana Enterprise license file (optional; requires --start-grafana)",
Value: "",
TakesFile: true,
Category: "Grafana Server",
},
&cli.BoolFlag{
Name: "image-renderer",
Usage: "Install the image renderer plugin (requires --start-grafana)",
Category: "Grafana Server",
},
&cli.StringFlag{
Name: "suite",
Usage: "Path to the suite to run (e.g. './e2e/dashboards-suite')",
TakesFile: true,
Required: true,
Destination: &suitePath,
},
},
Action: runAction,
}
}
func runAction(ctx context.Context, c *cli.Command) error {
suitePath := c.String("suite")
suitePath, err := normalisePath(suitePath)
if err != nil {
return fmt.Errorf("failed to normalise suite path: %w", err)
}
repoRoot, err := gitRepoRoot(ctx, suitePath)
if err != nil {
return fmt.Errorf("failed to get git repo root: %w", err)
}
screenshotsFolder := path.Join(suitePath, "screenshots")
videosFolder := path.Join(suitePath, "videos")
fileServerFolder := path.Join(repoRoot, "e2e", "cypress")
fixturesFolder := path.Join(fileServerFolder, "fixtures")
downloadsFolder := path.Join(fileServerFolder, "downloads")
benchmarkPluginResultsFolder := path.Join(suitePath, "benchmark-results")
reporter := path.Join(repoRoot, "e2e", "log-reporter.js")
env := map[string]string{
"BENCHMARK_PLUGIN_ENABLED": fmt.Sprintf("%t", c.Bool("benchmark-plugin")),
"SMTP_PLUGIN_ENABLED": fmt.Sprintf("%t", c.Bool("smtp-plugin")),
"BENCHMARK_PLUGIN_RESULTS_FOLDER": benchmarkPluginResultsFolder,
"SLOWMO": "0",
"BASE_URL": c.String("grafana-base-url"),
}
for _, v := range c.StringSlice("env") {
parts := strings.SplitN(v, "=", 2)
if len(parts) != 2 {
return fmt.Errorf("invalid environment variable format: %s, must be KEY=VALUE", v)
}
env[parts[0]] = parts[1]
}
cypressConfig := map[string]string{
"screenshotsFolder": screenshotsFolder,
"fixturesFolder": fixturesFolder,
"videosFolder": videosFolder,
"downloadsFolder": downloadsFolder,
"fileServerFolder": fileServerFolder,
"reporter": reporter,
"specPattern": path.Join(suitePath, "*.spec.ts"),
"defaultCommandTimeout": fmt.Sprintf("%d", c.Duration("timeout").Milliseconds()),
"viewportWidth": "1920",
"viewportHeight": "1080",
"trashAssetsBeforeRuns": "false",
"baseUrl": c.String("grafana-base-url"),
"video": fmt.Sprintf("%t", c.Bool("cypress-video")),
}
ctx, cancel := context.WithCancel(ctx)
defer cancel()
if c.Bool("start-grafana") {
startServerPath := path.Join(repoRoot, "scripts", "grafana-server", "start-server")
waitForGrafanaPath := path.Join(repoRoot, "scripts", "grafana-server", "wait-for-grafana")
go func() {
var args []string
if c.String("license-path") != "" {
args = append(args, c.String("license-path"))
}
//nolint:gosec
cmd := exec.CommandContext(ctx, startServerPath, args...)
cmd.Dir = repoRoot
cmd.Env = os.Environ()
cmd.Env = append(cmd.Env, fmt.Sprintf("TZ=%s", c.String("timezone")))
if c.Bool("image-renderer") {
cmd.Env = append(cmd.Env, "INSTALL_IMAGE_RENDERER=true")
}
cmd.Stdout = prefixGrafana(os.Stdout)
cmd.Stderr = prefixGrafana(os.Stderr)
cmd.Stdin = nil
if err := cmd.Run(); err != nil {
fmt.Println("Error running Grafana:", err)
}
}()
//nolint:gosec
cmd := exec.CommandContext(ctx, waitForGrafanaPath)
cmd.Dir = repoRoot
cmd.Env = os.Environ()
cmd.Env = append(cmd.Env, fmt.Sprintf("TZ=%s", c.String("timezone")))
cmd.Stdout = prefixGrafana(os.Stdout)
cmd.Stderr = prefixGrafana(os.Stderr)
cmd.Stdin = nil
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to wait for Grafana: %w", err)
}
}
args := []string{"run", "cypress", c.String("command"),
"--env", joinCypressCfg(env),
"--config", joinCypressCfg(cypressConfig),
"--browser", c.String("browser")}
args = append(args, c.StringSlice("parameters")...)
//nolint:gosec
cmd := exec.CommandContext(ctx, "yarn", args...)
cmd.Dir = repoRoot
cmd.Env = os.Environ()
cmd.Env = append(cmd.Env, fmt.Sprintf("TZ=%s", c.String("timezone")))
cmd.Stdout = prefixCypress(os.Stdout)
cmd.Stderr = prefixCypress(os.Stderr)
cmd.Stdin = os.Stdin
return cmd.Run()
}
func gitRepoRoot(ctx context.Context, dir string) (string, error) {
cmd := exec.CommandContext(ctx, "git", "rev-parse", "--show-toplevel")
cmd.Dir = dir
out, err := cmd.Output()
if err != nil {
return "", fmt.Errorf("failed to get git repo root: %w", err)
}
p := strings.TrimSpace(string(out))
p, err = normalisePath(p)
if err != nil {
return "", fmt.Errorf("failed to normalise git repo root path: %w", err)
}
return p, nil
}
func normalisePath(p string) (string, error) {
absPath, err := filepath.Abs(p)
if err != nil {
return "", fmt.Errorf("failed to get absolute path: %w", err)
}
return path.Clean(filepath.ToSlash(absPath)), nil
}
func joinCypressCfg(cfg map[string]string) string {
config := make([]string, 0, len(cfg))
for k, v := range cfg {
config = append(config, fmt.Sprintf("%s=%s", k, v))
}
return strings.Join(config, ",")
}
const (
resetColor = "\033[0m"
yellowColor = "\033[0;33m"
cyanColor = "\033[0;36m"
)
func prefixCypress(w io.Writer) io.Writer {
if _, ok := os.LookupEnv("CI"); ok {
return w
}
return newWrappingOutput(cyanColor+"Cypress: ", resetColor, w)
}
func prefixGrafana(w io.Writer) io.Writer {
if _, ok := os.LookupEnv("CI"); ok {
return w
}
return newWrappingOutput(yellowColor+"Grafana: ", resetColor, w)
}
var _ io.Writer = (*wrappingOutput)(nil)
type wrappingOutput struct {
prefix string
suffix string
mu *sync.Mutex
inner io.Writer
writtenPrefix bool
}
func newWrappingOutput(prefix, suffix string, inner io.Writer) *wrappingOutput {
return &wrappingOutput{
prefix: prefix,
suffix: suffix,
mu: &sync.Mutex{},
inner: inner,
}
}
func (p *wrappingOutput) Write(b []byte) (int, error) {
p.mu.Lock()
defer p.mu.Unlock()
for line := range bytes.Lines(b) {
if !p.writtenPrefix {
if _, err := p.inner.Write([]byte(p.prefix)); err != nil {
return 0, err
}
p.writtenPrefix = true
}
if _, err := p.inner.Write(line); err != nil {
return 0, err
}
if bytes.HasSuffix(line, []byte("\n")) {
p.writtenPrefix = false
if _, err := p.inner.Write([]byte(p.suffix)); err != nil {
return 0, err
}
}
}
return len(b), nil
}

View File

@ -224,6 +224,7 @@
"node-notifier": "10.0.1", "node-notifier": "10.0.1",
"nx": "20.7.1", "nx": "20.7.1",
"openapi-types": "^12.1.3", "openapi-types": "^12.1.3",
"pa11y-ci": "^3.1.0",
"pdf-parse": "^1.1.1", "pdf-parse": "^1.1.1",
"plop": "^4.0.1", "plop": "^4.0.1",
"postcss": "8.5.1", "postcss": "8.5.1",

152
pkg/build/a11y/main.go Normal file
View File

@ -0,0 +1,152 @@
package main
import (
"context"
"fmt"
"log"
"os"
"os/signal"
"path/filepath"
"dagger.io/dagger"
"github.com/urfave/cli/v3"
)
func main() {
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
defer cancel()
if err := NewApp().Run(ctx, os.Args); err != nil {
cancel()
fmt.Println(err)
os.Exit(1)
}
}
func NewApp() *cli.Command {
return &cli.Command{
Name: "a11y",
Usage: "Run Grafana accessibility tests",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "grafana-dir",
Usage: "Path to the grafana/grafana clone directory",
Value: ".",
Validator: mustBeDir("grafana-dir"),
TakesFile: true,
},
&cli.StringFlag{
Name: "package",
Usage: "Path to the grafana tar.gz package",
Value: "grafana.tar.gz",
Validator: mustBeFile("package", false),
TakesFile: true,
},
&cli.StringFlag{
Name: "license",
Usage: "Path to the Grafana Enterprise license file (optional)",
Validator: mustBeFile("license", true),
TakesFile: true,
},
&cli.StringFlag{
Name: "flags",
Usage: "Flags to pass through to the e2e runner",
},
},
Action: run,
}
}
func run(ctx context.Context, cmd *cli.Command) error {
grafanaDir := cmd.String("grafana-dir")
targzPath := cmd.String("package")
licensePath := cmd.String("license")
runnerFlags := cmd.String("flags")
d, err := dagger.Connect(ctx)
if err != nil {
return fmt.Errorf("failed to connect to Dagger: %w", err)
}
yarnCache := d.CacheVolume("yarn")
//nolint:gosec
nvmrcContents, err := os.ReadFile(filepath.Join(grafanaDir, ".nvmrc"))
if err != nil {
return fmt.Errorf("failed to read .nvmrc file: %w", err)
}
nodeVersion := string(nvmrcContents)
grafana := d.Host().Directory(grafanaDir, dagger.HostDirectoryOpts{
Exclude: []string{"node_modules", "*.tar.gz"},
})
targz := d.Host().File(targzPath)
var license *dagger.File
if licensePath != "" {
license = d.Host().File(licensePath)
}
svc, err := GrafanaService(ctx, d, GrafanaServiceOpts{
GrafanaDir: grafana,
GrafanaTarGz: targz,
License: license,
YarnCache: yarnCache,
NodeVersion: nodeVersion,
})
if err != nil {
return fmt.Errorf("failed to create Grafana service: %w", err)
}
c := RunTest(d, svc, grafana, yarnCache, nodeVersion, runnerFlags)
c, err = c.Sync(ctx)
if err != nil {
return fmt.Errorf("failed to run a11y test suite: %w", err)
}
code, err := c.ExitCode(ctx)
if err != nil {
return fmt.Errorf("failed to get exit code of a11y test suite: %w", err)
}
if code != 0 {
return fmt.Errorf("a11y tests failed with exit code %d", code)
}
log.Println("a11y tests completed successfully")
return nil
}
func mustBeFile(arg string, emptyOk bool) func(string) error {
return func(s string) error {
if s == "" {
if emptyOk {
return nil
}
return cli.Exit(arg+" cannot be empty", 1)
}
stat, err := os.Stat(s)
if err != nil {
return cli.Exit(arg+" does not exist or cannot be read: "+s, 1)
}
if stat.IsDir() {
return cli.Exit(arg+" must be a file, not a directory: "+s, 1)
}
return nil
}
}
func mustBeDir(arg string) func(string) error {
return func(s string) error {
if s == "" {
return cli.Exit(arg+" cannot be empty", 1)
}
stat, err := os.Stat(s)
if err != nil {
return cli.Exit(arg+" does not exist or cannot be read: "+s, 1)
}
if !stat.IsDir() {
return cli.Exit(arg+" must be a directory: "+s, 1)
}
return nil
}
}

25
pkg/build/a11y/run.go Normal file
View File

@ -0,0 +1,25 @@
package main
import (
"fmt"
"dagger.io/dagger"
)
func RunTest(
d *dagger.Client,
svc *dagger.Service,
src *dagger.Directory, cache *dagger.CacheVolume,
nodeVersion, runnerFlags string) *dagger.Container {
command := fmt.Sprintf(
"./e2e-runner a11y --start-grafana=false"+
" --grafana-host grafana --grafana-port 3001 %s", runnerFlags)
return GrafanaFrontend(d, cache, nodeVersion, src).
WithExec([]string{"/bin/sh", "-c", "apt-get update && apt-get install -y git curl"}).
WithExec([]string{"curl", "-LO", "https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb"}).
WithExec([]string{"apt-get", "install", "-y", "./google-chrome-stable_current_amd64.deb"}).
WithWorkdir("/src").
WithServiceBinding("grafana", svc).
WithExec([]string{"/bin/bash", "-c", command}, dagger.ContainerWithExecOpts{Expect: dagger.ReturnTypeAny})
}

97
pkg/build/a11y/service.go Normal file
View File

@ -0,0 +1,97 @@
package main
import (
"context"
"fmt"
"os"
"strings"
"dagger.io/dagger"
)
func NodeImage(version string) string {
return fmt.Sprintf("node:%s-slim", strings.TrimPrefix(strings.TrimSpace(version), "v"))
}
type GrafanaServiceOpts struct {
GrafanaDir *dagger.Directory
GrafanaTarGz *dagger.File
License *dagger.File
YarnCache *dagger.CacheVolume
NodeVersion string
}
func Frontend(src *dagger.Directory) *dagger.Directory {
return src.
WithoutFile("go.mod").
WithoutFile("go.sum").
WithoutFile("go.work").
WithoutFile("go.work.sum").
WithoutDirectory(".github").
WithoutDirectory("docs").
WithoutDirectory("pkg").
WithoutDirectory("apps").
WithoutDirectory("videos")
}
func WithGrafanaFrontend(c *dagger.Container, src *dagger.Directory) *dagger.Container {
return c.WithDirectory("/src", Frontend(src), dagger.ContainerWithDirectoryOpts{
Exclude: []string{
"*drone*",
"*.go",
"*.md",
},
})
}
func WithYarnCache(c *dagger.Container, cache *dagger.CacheVolume) *dagger.Container {
return c.
WithWorkdir("/src").
WithMountedCache("/yarn/cache", cache)
}
func GrafanaFrontend(d *dagger.Client, yarnCache *dagger.CacheVolume, nodeVersion string, grafanaDir *dagger.Directory) *dagger.Container {
container := d.Container().From(NodeImage(nodeVersion))
container = WithGrafanaFrontend(container, grafanaDir)
return WithYarnCache(container, yarnCache).
WithEnvVariable("YARN_CACHE_FOLDER", "/yarn/cache").
WithExec([]string{"yarn", "install", "--immutable"})
}
func GrafanaService(ctx context.Context, d *dagger.Client, opts GrafanaServiceOpts) (*dagger.Service, error) {
src := GrafanaFrontend(d, opts.YarnCache, opts.NodeVersion, opts.GrafanaDir)
container := d.Container().From("alpine:3").
WithExec([]string{"apk", "add", "--no-cache", "bash", "tar", "netcat-openbsd"}).
WithMountedFile("/src/grafana.tar.gz", opts.GrafanaTarGz).
WithExec([]string{"mkdir", "-p", "/src/grafana"}).
WithExec([]string{"tar", "--strip-components=1", "-xzf", "/src/grafana.tar.gz", "-C", "/src/grafana"}).
WithDirectory("/src/grafana/devenv", src.Directory("/src/devenv")).
WithDirectory("/src/grafana/e2e", src.Directory("/src/e2e")).
WithDirectory("/src/grafana/scripts", src.Directory("/src/scripts")).
WithDirectory("/src/grafana/tools", src.Directory("/src/tools")).
WithWorkdir("/src/grafana").
WithEnvVariable("GF_APP_MODE", "development").
WithEnvVariable("GF_SERVER_HTTP_PORT", "3001").
WithEnvVariable("GF_SERVER_ROUTER_LOGGING", "1").
WithExposedPort(3001)
var licenseArg string
if opts.License != nil {
container = container.WithMountedFile("/src/license.jwt", opts.License)
licenseArg = "/src/license.jwt"
}
// We add all GF_ environment variables to allow for overriding Grafana configuration.
// It is unlikely the runner has any such otherwise.
for _, env := range os.Environ() {
if strings.HasPrefix(env, "GF_") {
parts := strings.SplitN(env, "=", 2)
container = container.WithEnvVariable(parts[0], parts[1])
}
}
svc := container.AsService(dagger.ContainerAsServiceOpts{Args: []string{"bash", "-x", "scripts/grafana-server/start-server", licenseArg}})
return svc, nil
}

View File

@ -8,7 +8,7 @@ import (
func RunSuite(d *dagger.Client, svc *dagger.Service, src *dagger.Directory, cache *dagger.CacheVolume, suite, runnerFlags string) *dagger.Container { func RunSuite(d *dagger.Client, svc *dagger.Service, src *dagger.Directory, cache *dagger.CacheVolume, suite, runnerFlags string) *dagger.Container {
command := fmt.Sprintf( command := fmt.Sprintf(
"./e2e-runner --start-grafana=false --cypress-video"+ "./e2e-runner cypress --start-grafana=false --cypress-video"+
" --grafana-base-url http://grafana:3001 --suite %s %s", suite, runnerFlags) " --grafana-base-url http://grafana:3001 --suite %s %s", suite, runnerFlags)
return WithYarnCache(WithGrafanaFrontend(d.Container().From("cypress/included:13.1.0"), src), cache). return WithYarnCache(WithGrafanaFrontend(d.Container().From("cypress/included:13.1.0"), src), cache).

View File

@ -1,4 +1,4 @@
#!/bin/bash #!/usr/bin/env bash
. scripts/grafana-server/variables . scripts/grafana-server/variables

View File

@ -1,4 +1,4 @@
#!/bin/bash #!/usr/bin/env bash
set -eo pipefail set -eo pipefail
. scripts/grafana-server/variables . scripts/grafana-server/variables

View File

@ -1,4 +1,4 @@
#!/bin/bash #!/usr/bin/env bash
DEFAULT_RUNDIR=scripts/grafana-server/tmp DEFAULT_RUNDIR=scripts/grafana-server/tmp
RUNDIR=${RUNDIR:-$DEFAULT_RUNDIR} RUNDIR=${RUNDIR:-$DEFAULT_RUNDIR}

View File

@ -1,4 +1,4 @@
#!/bin/bash #!/usr/bin/env bash
set -eo pipefail set -eo pipefail
. scripts/grafana-server/variables . scripts/grafana-server/variables

334
yarn.lock
View File

@ -11487,6 +11487,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"array-union@npm:^1.0.1":
version: 1.0.2
resolution: "array-union@npm:1.0.2"
dependencies:
array-uniq: "npm:^1.0.1"
checksum: 10/82cec6421b6e6766556c484835a6d476a873f1b71cace5ab2b4f1b15b1e3162dc4da0d16f7a2b04d4aec18146c6638fe8f661340b31ba8e469fd811a1b45dc8d
languageName: node
linkType: hard
"array-union@npm:^2.1.0": "array-union@npm:^2.1.0":
version: 2.1.0 version: 2.1.0
resolution: "array-union@npm:2.1.0" resolution: "array-union@npm:2.1.0"
@ -11494,6 +11503,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"array-uniq@npm:^1.0.1":
version: 1.0.3
resolution: "array-uniq@npm:1.0.3"
checksum: 10/1625f06b093d8bf279b81adfec6e72951c0857d65b5e3f65f053fffe9f9dd61c2fc52cff57e38a4700817e7e3f01a4faa433d505ea9e33cdae4514c334e0bf9e
languageName: node
linkType: hard
"array.prototype.findlast@npm:^1.2.5": "array.prototype.findlast@npm:^1.2.5":
version: 1.2.5 version: 1.2.5
resolution: "array.prototype.findlast@npm:1.2.5" resolution: "array.prototype.findlast@npm:1.2.5"
@ -11655,7 +11671,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"async@npm:^2.6.4": "async@npm:^2.6.4, async@npm:~2.6.4":
version: 2.6.4 version: 2.6.4
resolution: "async@npm:2.6.4" resolution: "async@npm:2.6.4"
dependencies: dependencies:
@ -11765,6 +11781,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"axe-core@npm:~4.2.1":
version: 4.2.4
resolution: "axe-core@npm:4.2.4"
checksum: 10/fc45c087241da298be1e35ea898a1d7189903ddc52ad6f6eb46b27fb47955cb4e097eb854da5b60bedb5eab2df66fabcf334f6bb09105d404e89323cf82e8773
languageName: node
linkType: hard
"axios@npm:^1, axios@npm:^1.7.9, axios@npm:^1.8.2, axios@npm:^1.8.3": "axios@npm:^1, axios@npm:^1.7.9, axios@npm:^1.8.2, axios@npm:^1.8.3":
version: 1.8.4 version: 1.8.4
resolution: "axios@npm:1.8.4" resolution: "axios@npm:1.8.4"
@ -12021,6 +12044,18 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"bfj@npm:~7.0.2":
version: 7.0.2
resolution: "bfj@npm:7.0.2"
dependencies:
bluebird: "npm:^3.5.5"
check-types: "npm:^11.1.1"
hoopy: "npm:^0.1.4"
tryer: "npm:^1.0.1"
checksum: 10/e1040fe6aec2afeb6f6c5231bbbc055616fa99c23c5249c7d20a2919507a69d8fd4d82d2245eca5ee08cbfcd3e70ce817328b8a20acda69af4638f1c11343bc7
languageName: node
linkType: hard
"big.js@npm:^5.2.2": "big.js@npm:^5.2.2":
version: 5.2.2 version: 5.2.2
resolution: "big.js@npm:5.2.2" resolution: "big.js@npm:5.2.2"
@ -12090,7 +12125,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"bluebird@npm:^3.7.2": "bluebird@npm:^3.5.5, bluebird@npm:^3.7.2":
version: 3.7.2 version: 3.7.2
resolution: "bluebird@npm:3.7.2" resolution: "bluebird@npm:3.7.2"
checksum: 10/007c7bad22c5d799c8dd49c85b47d012a1fe3045be57447721e6afbd1d5be43237af1db62e26cb9b0d9ba812d2e4ca3bac82f6d7e016b6b88de06ee25ceb96e7 checksum: 10/007c7bad22c5d799c8dd49c85b47d012a1fe3045be57447721e6afbd1d5be43237af1db62e26cb9b0d9ba812d2e4ca3bac82f6d7e016b6b88de06ee25ceb96e7
@ -12308,7 +12343,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"buffer@npm:^5.5.0, buffer@npm:^5.7.1": "buffer@npm:^5.2.1, buffer@npm:^5.5.0, buffer@npm:^5.7.1":
version: 5.7.1 version: 5.7.1
resolution: "buffer@npm:5.7.1" resolution: "buffer@npm:5.7.1"
dependencies: dependencies:
@ -12725,6 +12760,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"check-types@npm:^11.1.1":
version: 11.2.3
resolution: "check-types@npm:11.2.3"
checksum: 10/557e119fa018d7de4e873ada0a6c8917a0f6e0955dc19293396405f5292cfcfe190457557f4cc422e6845d715ef6bbb1d0ab9198ff6735dd96ac50e3ef1e2424
languageName: node
linkType: hard
"cheerio-select@npm:^2.1.0": "cheerio-select@npm:^2.1.0":
version: 2.1.0 version: 2.1.0
resolution: "cheerio-select@npm:2.1.0" resolution: "cheerio-select@npm:2.1.0"
@ -12739,7 +12781,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"cheerio@npm:^1.0.0": "cheerio@npm:^1.0.0, cheerio@npm:~1.0.0-rc.10":
version: 1.0.0 version: 1.0.0
resolution: "cheerio@npm:1.0.0" resolution: "cheerio@npm:1.0.0"
dependencies: dependencies:
@ -12793,6 +12835,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"chownr@npm:^1.1.1":
version: 1.1.4
resolution: "chownr@npm:1.1.4"
checksum: 10/115648f8eb38bac5e41c3857f3e663f9c39ed6480d1349977c4d96c95a47266fcacc5a5aabf3cb6c481e22d72f41992827db47301851766c4fd77ac21a4f081d
languageName: node
linkType: hard
"chownr@npm:^2.0.0": "chownr@npm:^2.0.0":
version: 2.0.0 version: 2.0.0
resolution: "chownr@npm:2.0.0" resolution: "chownr@npm:2.0.0"
@ -13232,7 +13281,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"commander@npm:^6.2.0, commander@npm:^6.2.1": "commander@npm:^6.2.0, commander@npm:^6.2.1, commander@npm:~6.2.1":
version: 6.2.1 version: 6.2.1
resolution: "commander@npm:6.2.1" resolution: "commander@npm:6.2.1"
checksum: 10/25b88c2efd0380c84f7844b39cf18510da7bfc5013692d68cdc65f764a1c34e6c8a36ea6d72b6620e3710a930cf8fab2695bdec2bf7107a0f4fa30a3ef3b7d0e checksum: 10/25b88c2efd0380c84f7844b39cf18510da7bfc5013692d68cdc65f764a1c34e6c8a36ea6d72b6620e3710a930cf8fab2695bdec2bf7107a0f4fa30a3ef3b7d0e
@ -13246,6 +13295,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"commander@npm:~8.0.0":
version: 8.0.0
resolution: "commander@npm:8.0.0"
checksum: 10/fdae6767935000431360bc72eefb20e8f0e1c639dc5fac956dc5495877dfbc5d18fcdbd32a0abf59854e013efcb2533957d11dbdbdd42b1d76aa2b54328bb15e
languageName: node
linkType: hard
"comment-parser@npm:1.4.1": "comment-parser@npm:1.4.1":
version: 1.4.1 version: 1.4.1
resolution: "comment-parser@npm:1.4.1" resolution: "comment-parser@npm:1.4.1"
@ -14983,6 +15039,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"devtools-protocol@npm:0.0.869402":
version: 0.0.869402
resolution: "devtools-protocol@npm:0.0.869402"
checksum: 10/fd3d12947047c6d6ef597b86cad1e5cf956780dac0237f0ac5de0091a1f449e3ea1eef10c65eb7c416785fb7d9e588c2588995b91b4db2830d7f23cea421472e
languageName: node
linkType: hard
"diff-sequences@npm:^27.5.1": "diff-sequences@npm:^27.5.1":
version: 27.5.1 version: 27.5.1
resolution: "diff-sequences@npm:27.5.1" resolution: "diff-sequences@npm:27.5.1"
@ -15551,6 +15614,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"envinfo@npm:~7.8.1":
version: 7.8.1
resolution: "envinfo@npm:7.8.1"
bin:
envinfo: dist/cli.js
checksum: 10/e7a2d71c7dfe398a4ffda0e844e242d2183ef2627f98e74e4cd71edd2af691c8707a2b34aacef92538c27b3daf9a360d32202f33c0a9f27f767c4e1c6ba8b522
languageName: node
linkType: hard
"environment@npm:^1.0.0": "environment@npm:^1.0.0":
version: 1.1.0 version: 1.1.0
resolution: "environment@npm:1.1.0" resolution: "environment@npm:1.1.0"
@ -16647,7 +16719,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"extract-zip@npm:2.0.1": "extract-zip@npm:2.0.1, extract-zip@npm:^2.0.0":
version: 2.0.1 version: 2.0.1
resolution: "extract-zip@npm:2.0.1" resolution: "extract-zip@npm:2.0.1"
dependencies: dependencies:
@ -16904,6 +16976,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"file-url@npm:^3.0.0":
version: 3.0.0
resolution: "file-url@npm:3.0.0"
checksum: 10/f15c1bdd81df1a09238f3411f877274d7849703df837ec327c4d1df631314f60036cb700a59d826d8c96b79ff66429d3c758480005e1899c00961541b98d5bfe
languageName: node
linkType: hard
"filelist@npm:^1.0.1": "filelist@npm:^1.0.1":
version: 1.0.4 version: 1.0.4
resolution: "filelist@npm:1.0.4" resolution: "filelist@npm:1.0.4"
@ -17814,7 +17893,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"glob@npm:^7.1.2, glob@npm:^7.1.3, glob@npm:^7.1.4": "glob@npm:^7.0.3, glob@npm:^7.1.2, glob@npm:^7.1.3, glob@npm:^7.1.4":
version: 7.2.3 version: 7.2.3
resolution: "glob@npm:7.2.3" resolution: "glob@npm:7.2.3"
dependencies: dependencies:
@ -17981,6 +18060,19 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"globby@npm:~6.1.0":
version: 6.1.0
resolution: "globby@npm:6.1.0"
dependencies:
array-union: "npm:^1.0.1"
glob: "npm:^7.0.3"
object-assign: "npm:^4.0.1"
pify: "npm:^2.0.0"
pinkie-promise: "npm:^2.0.0"
checksum: 10/18109d6b9d55643d2b98b59c3cfae7073ccfe39829632f353d516cc124d836c2ddebe48a23f04af63d66a621b6d86dd4cbd7e6af906f2458a7fe510ffc4bd424
languageName: node
linkType: hard
"globjoin@npm:^0.1.4": "globjoin@npm:^0.1.4":
version: 0.1.4 version: 0.1.4
resolution: "globjoin@npm:0.1.4" resolution: "globjoin@npm:0.1.4"
@ -18262,6 +18354,7 @@ __metadata:
ol: "npm:7.4.0" ol: "npm:7.4.0"
ol-ext: "npm:4.0.33" ol-ext: "npm:4.0.33"
openapi-types: "npm:^12.1.3" openapi-types: "npm:^12.1.3"
pa11y-ci: "npm:^3.1.0"
pdf-parse: "npm:^1.1.1" pdf-parse: "npm:^1.1.1"
plop: "npm:^4.0.1" plop: "npm:^4.0.1"
pluralize: "npm:^8.0.0" pluralize: "npm:^8.0.0"
@ -18655,6 +18748,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"hoopy@npm:^0.1.4":
version: 0.1.4
resolution: "hoopy@npm:0.1.4"
checksum: 10/7a73f1839a7fd6b953356770dff2c3cff813d97d899cddd75b348926c4df36059d987c06bedb57b1b7711504dba83d3b7b986f979a08b1e415da73a51fefa767
languageName: node
linkType: hard
"hosted-git-info@npm:^2.1.4": "hosted-git-info@npm:^2.1.4":
version: 2.8.9 version: 2.8.9
resolution: "hosted-git-info@npm:2.8.9" resolution: "hosted-git-info@npm:2.8.9"
@ -18818,6 +18918,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"html_codesniffer@npm:~2.5.1":
version: 2.5.1
resolution: "html_codesniffer@npm:2.5.1"
checksum: 10/baeb07f54f7a514f7c0abb68318f6f1216266ef853059dbe6fc182d9c49fcc610e331de76599d51d10b2415cd1c0a137a22d656047e002cf6aa8815b9a8434cc
languageName: node
linkType: hard
"htmlparser2@npm:^6.1.0": "htmlparser2@npm:^6.1.0":
version: 6.1.0 version: 6.1.0
resolution: "htmlparser2@npm:6.1.0" resolution: "htmlparser2@npm:6.1.0"
@ -20161,6 +20268,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"is@npm:^3.3.0":
version: 3.3.0
resolution: "is@npm:3.3.0"
checksum: 10/f77dc5a05a1e8fd1f1de282add9bb01c44dae27af72b883bf0ce342151dec48f125b0b8923efa78c1e93c4fb866095629b2c7de3e5e3853aea4ed17c82c5cd8d
languageName: node
linkType: hard
"isarray@npm:0.0.1": "isarray@npm:0.0.1":
version: 0.0.1 version: 0.0.1
resolution: "isarray@npm:0.0.1" resolution: "isarray@npm:0.0.1"
@ -21382,6 +21496,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"kleur@npm:~4.1.4":
version: 4.1.5
resolution: "kleur@npm:4.1.5"
checksum: 10/44d84cc4eedd4311099402ef6d4acd9b2d16e08e499d6ef3bb92389bd4692d7ef09e35248c26e27f98acac532122acb12a1bfee645994ae3af4f0a37996da7df
languageName: node
linkType: hard
"known-css-properties@npm:^0.29.0": "known-css-properties@npm:^0.29.0":
version: 0.29.0 version: 0.29.0
resolution: "known-css-properties@npm:0.29.0" resolution: "known-css-properties@npm:0.29.0"
@ -21937,7 +22058,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"lodash@npm:4.17.21, lodash@npm:^4, lodash@npm:^4.1.1, lodash@npm:^4.15.0, lodash@npm:^4.17.14, lodash@npm:^4.17.15, lodash@npm:^4.17.20, lodash@npm:^4.17.21, lodash@npm:^4.17.4": "lodash@npm:4.17.21, lodash@npm:^4, lodash@npm:^4.1.1, lodash@npm:^4.15.0, lodash@npm:^4.17.14, lodash@npm:^4.17.15, lodash@npm:^4.17.20, lodash@npm:^4.17.21, lodash@npm:^4.17.4, lodash@npm:~4.17.21":
version: 4.17.21 version: 4.17.21
resolution: "lodash@npm:4.17.21" resolution: "lodash@npm:4.17.21"
checksum: 10/c08619c038846ea6ac754abd6dd29d2568aa705feb69339e836dfa8d8b09abbb2f859371e86863eda41848221f9af43714491467b5b0299122431e202bb0c532 checksum: 10/c08619c038846ea6ac754abd6dd29d2568aa705feb69339e836dfa8d8b09abbb2f859371e86863eda41848221f9af43714491467b5b0299122431e202bb0c532
@ -22806,6 +22927,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"mkdirp-classic@npm:^0.5.2":
version: 0.5.3
resolution: "mkdirp-classic@npm:0.5.3"
checksum: 10/3f4e088208270bbcc148d53b73e9a5bd9eef05ad2cbf3b3d0ff8795278d50dd1d11a8ef1875ff5aea3fa888931f95bfcb2ad5b7c1061cfefd6284d199e6776ac
languageName: node
linkType: hard
"mkdirp@npm:^0.5.6": "mkdirp@npm:^0.5.6":
version: 0.5.6 version: 0.5.6
resolution: "mkdirp@npm:0.5.6" resolution: "mkdirp@npm:0.5.6"
@ -23072,6 +23200,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"mustache@npm:~4.2.0":
version: 4.2.0
resolution: "mustache@npm:4.2.0"
bin:
mustache: bin/mustache
checksum: 10/6e668bd5803255ab0779c3983b9412b5c4f4f90e822230e0e8f414f5449ed7a137eed29430e835aa689886f663385cfe05f808eb34b16e1f3a95525889b05cd3
languageName: node
linkType: hard
"mutationobserver-shim@npm:0.3.7": "mutationobserver-shim@npm:0.3.7":
version: 0.3.7 version: 0.3.7
resolution: "mutationobserver-shim@npm:0.3.7" resolution: "mutationobserver-shim@npm:0.3.7"
@ -23291,6 +23428,20 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"node-fetch@npm:~2.6.1":
version: 2.6.13
resolution: "node-fetch@npm:2.6.13"
dependencies:
whatwg-url: "npm:^5.0.0"
peerDependencies:
encoding: ^0.1.0
peerDependenciesMeta:
encoding:
optional: true
checksum: 10/72f94498d547e322207575b2778e7b3d969b0d73f35a19f258c7fb982bf0ae96e5a3a518477300ba1dd2bd22e6b05be074648ed88448c5cf1a9b7d23c6529d1a
languageName: node
linkType: hard
"node-forge@npm:^1, node-forge@npm:^1.3.1": "node-forge@npm:^1, node-forge@npm:^1.3.1":
version: 1.3.1 version: 1.3.1
resolution: "node-forge@npm:1.3.1" resolution: "node-forge@npm:1.3.1"
@ -23414,6 +23565,16 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"node.extend@npm:~2.0.2":
version: 2.0.3
resolution: "node.extend@npm:2.0.3"
dependencies:
hasown: "npm:^2.0.0"
is: "npm:^3.3.0"
checksum: 10/f500ace16d0b90e9db3919676de593eb37e7b82d8d9b67d95a40e5856ef5842592df3364b4d01fc2c3f4c0dea6dd9d627444dd85fe18581b7a22caad5ffab249
languageName: node
linkType: hard
"nodemailer@npm:6.9.13": "nodemailer@npm:6.9.13":
version: 6.9.13 version: 6.9.13
resolution: "nodemailer@npm:6.9.13" resolution: "nodemailer@npm:6.9.13"
@ -24333,6 +24494,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"p-timeout@npm:~4.1.0":
version: 4.1.0
resolution: "p-timeout@npm:4.1.0"
checksum: 10/321fec524c23a754e3f1487f2b0a5516fd32aba960d5610490eac56f8a0114b549a93f9919ffc05aa68956dc52e8330e0519f3ddf951d208d19c845f9cd778de
languageName: node
linkType: hard
"p-try@npm:^1.0.0": "p-try@npm:^1.0.0":
version: 1.0.0 version: 1.0.0
resolution: "p-try@npm:1.0.0" resolution: "p-try@npm:1.0.0"
@ -24356,6 +24524,48 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"pa11y-ci@npm:^3.1.0":
version: 3.1.0
resolution: "pa11y-ci@npm:3.1.0"
dependencies:
async: "npm:~2.6.4"
cheerio: "npm:~1.0.0-rc.10"
commander: "npm:~6.2.1"
globby: "npm:~6.1.0"
kleur: "npm:~4.1.4"
lodash: "npm:~4.17.21"
node-fetch: "npm:~2.6.1"
pa11y: "npm:^6.2.3"
protocolify: "npm:~3.0.0"
puppeteer: "npm:~9.1.1"
wordwrap: "npm:~1.0.0"
bin:
pa11y-ci: bin/pa11y-ci.js
checksum: 10/26f8a9e5aa2a7e1114090f4d2ff9392b2c0fa52cf7282343ce81816a7c0c017e2860d546623b6a869b86f1e597ac2f36415056ae9ccd783e8fc4bc91d76c0e5d
languageName: node
linkType: hard
"pa11y@npm:^6.2.3":
version: 6.2.3
resolution: "pa11y@npm:6.2.3"
dependencies:
axe-core: "npm:~4.2.1"
bfj: "npm:~7.0.2"
commander: "npm:~8.0.0"
envinfo: "npm:~7.8.1"
html_codesniffer: "npm:~2.5.1"
kleur: "npm:~4.1.4"
mustache: "npm:~4.2.0"
node.extend: "npm:~2.0.2"
p-timeout: "npm:~4.1.0"
puppeteer: "npm:~9.1.1"
semver: "npm:~7.3.5"
bin:
pa11y: bin/pa11y.js
checksum: 10/3903f10475aa6132279c8af21862dbcc3133e78d3c3ab6273094964093d73f60e312e9bc6eebcdcdc5c733d9229c1e63e3890103f0e860897ffd4ec2bf0f62b3
languageName: node
linkType: hard
"package-json-from-dist@npm:^1.0.0": "package-json-from-dist@npm:^1.0.0":
version: 1.0.0 version: 1.0.0
resolution: "package-json-from-dist@npm:1.0.0" resolution: "package-json-from-dist@npm:1.0.0"
@ -24895,7 +25105,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"pify@npm:^2.2.0, pify@npm:^2.3.0": "pify@npm:^2.0.0, pify@npm:^2.2.0, pify@npm:^2.3.0":
version: 2.3.0 version: 2.3.0
resolution: "pify@npm:2.3.0" resolution: "pify@npm:2.3.0"
checksum: 10/9503aaeaf4577acc58642ad1d25c45c6d90288596238fb68f82811c08104c800e5a7870398e9f015d82b44ecbcbef3dc3d4251a1cbb582f6e5959fe09884b2ba checksum: 10/9503aaeaf4577acc58642ad1d25c45c6d90288596238fb68f82811c08104c800e5a7870398e9f015d82b44ecbcbef3dc3d4251a1cbb582f6e5959fe09884b2ba
@ -24916,6 +25126,22 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"pinkie-promise@npm:^2.0.0":
version: 2.0.1
resolution: "pinkie-promise@npm:2.0.1"
dependencies:
pinkie: "npm:^2.0.0"
checksum: 10/b53a4a2e73bf56b6f421eef711e7bdcb693d6abb474d57c5c413b809f654ba5ee750c6a96dd7225052d4b96c4d053cdcb34b708a86fceed4663303abee52fcca
languageName: node
linkType: hard
"pinkie@npm:^2.0.0":
version: 2.0.4
resolution: "pinkie@npm:2.0.4"
checksum: 10/11d207257a044d1047c3755374d36d84dda883a44d030fe98216bf0ea97da05a5c9d64e82495387edeb9ee4f52c455bca97cdb97629932be65e6f54b29f5aec8
languageName: node
linkType: hard
"pirates@npm:^4.0.4": "pirates@npm:^4.0.4":
version: 4.0.6 version: 4.0.6
resolution: "pirates@npm:4.0.6" resolution: "pirates@npm:4.0.6"
@ -25516,6 +25742,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"prepend-http@npm:^3.0.0":
version: 3.0.1
resolution: "prepend-http@npm:3.0.1"
checksum: 10/8f4ea0c73e7fc9d42b33a0487a458504dd1098b64ac7832ed4b0c401d292f1cf312c631efe078d9d92b0aa898be57f63459206793882061a8f507a52f0045cc7
languageName: node
linkType: hard
"prettier@npm:3.4.2, prettier@npm:^3.1.1, prettier@npm:^3.2.5": "prettier@npm:3.4.2, prettier@npm:^3.1.1, prettier@npm:^3.2.5":
version: 3.4.2 version: 3.4.2
resolution: "prettier@npm:3.4.2" resolution: "prettier@npm:3.4.2"
@ -25622,6 +25855,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"progress@npm:^2.0.1":
version: 2.0.3
resolution: "progress@npm:2.0.3"
checksum: 10/e6f0bcb71f716eee9dfac0fe8a2606e3704d6a64dd93baaf49fbadbc8499989a610fe14cf1bc6f61b6d6653c49408d94f4a94e124538084efd8e4cf525e0293d
languageName: node
linkType: hard
"promise-all-reject-late@npm:^1.0.0": "promise-all-reject-late@npm:^1.0.0":
version: 1.0.1 version: 1.0.1
resolution: "promise-all-reject-late@npm:1.0.1" resolution: "promise-all-reject-late@npm:1.0.1"
@ -25735,6 +25975,16 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"protocolify@npm:~3.0.0":
version: 3.0.0
resolution: "protocolify@npm:3.0.0"
dependencies:
file-url: "npm:^3.0.0"
prepend-http: "npm:^3.0.0"
checksum: 10/6517361b577d42ef80b975e6ad03673f1f3ea5aedae003bed7a1b6fffb06bc9c7542302f36c3517c82fb3271ba0c7b592e7b80830fa96f397ee261cba258c396
languageName: node
linkType: hard
"protocols@npm:^2.0.0, protocols@npm:^2.0.1": "protocols@npm:^2.0.0, protocols@npm:^2.0.1":
version: 2.0.1 version: 2.0.1
resolution: "protocols@npm:2.0.1" resolution: "protocols@npm:2.0.1"
@ -25818,6 +26068,26 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"puppeteer@npm:~9.1.1":
version: 9.1.1
resolution: "puppeteer@npm:9.1.1"
dependencies:
debug: "npm:^4.1.0"
devtools-protocol: "npm:0.0.869402"
extract-zip: "npm:^2.0.0"
https-proxy-agent: "npm:^5.0.0"
node-fetch: "npm:^2.6.1"
pkg-dir: "npm:^4.2.0"
progress: "npm:^2.0.1"
proxy-from-env: "npm:^1.1.0"
rimraf: "npm:^3.0.2"
tar-fs: "npm:^2.0.0"
unbzip2-stream: "npm:^1.3.3"
ws: "npm:^7.2.3"
checksum: 10/eab8e4982ea2e6007adbe7b1028504fe55dac39476e5f6f4d93b1b5abc5276ee0b398149e1824510e51c8b9a2a15048b31518563bdbe10135e2c6ced915b8018
languageName: node
linkType: hard
"pure-rand@npm:^6.0.0": "pure-rand@npm:^6.0.0":
version: 6.0.3 version: 6.0.3
resolution: "pure-rand@npm:6.0.3" resolution: "pure-rand@npm:6.0.3"
@ -28283,6 +28553,17 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"semver@npm:~7.3.5":
version: 7.3.8
resolution: "semver@npm:7.3.8"
dependencies:
lru-cache: "npm:^6.0.0"
bin:
semver: bin/semver.js
checksum: 10/c8c04a4d41d30cffa7277904e0ad6998623dd61e36bca9578b0128d8c683b705a3924beada55eae7fa004fb30a9359a53a4ead2b68468d778b602f3b1a28f8e3
languageName: node
linkType: hard
"send@npm:0.19.0": "send@npm:0.19.0":
version: 0.19.0 version: 0.19.0
resolution: "send@npm:0.19.0" resolution: "send@npm:0.19.0"
@ -30079,7 +30360,19 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"tar-stream@npm:~2.2.0": "tar-fs@npm:^2.0.0":
version: 2.1.3
resolution: "tar-fs@npm:2.1.3"
dependencies:
chownr: "npm:^1.1.1"
mkdirp-classic: "npm:^0.5.2"
pump: "npm:^3.0.0"
tar-stream: "npm:^2.1.4"
checksum: 10/37fdfd3aa73f4f49c0821ef75f67647ecafd5370d2e311d9ace6ff3825ff4355014055c3d43407c6a655adf6c5bfb0cbcf93412161dad5af7110eb7d7a0c2eae
languageName: node
linkType: hard
"tar-stream@npm:^2.1.4, tar-stream@npm:~2.2.0":
version: 2.2.0 version: 2.2.0
resolution: "tar-stream@npm:2.2.0" resolution: "tar-stream@npm:2.2.0"
dependencies: dependencies:
@ -30547,6 +30840,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"tryer@npm:^1.0.1":
version: 1.0.1
resolution: "tryer@npm:1.0.1"
checksum: 10/4d869d187bd715136903b349f39d1cc3e5c19f742689a348190aff92408ee8dd3d7d9adc26dc9265c35d722731184c979ed316109b6c1239249a8707bb92cc49
languageName: node
linkType: hard
"ts-api-utils@npm:^2.0.0, ts-api-utils@npm:^2.1.0": "ts-api-utils@npm:^2.0.0, ts-api-utils@npm:^2.1.0":
version: 2.1.0 version: 2.1.0
resolution: "ts-api-utils@npm:2.1.0" resolution: "ts-api-utils@npm:2.1.0"
@ -31062,6 +31362,16 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"unbzip2-stream@npm:^1.3.3":
version: 1.4.3
resolution: "unbzip2-stream@npm:1.4.3"
dependencies:
buffer: "npm:^5.2.1"
through: "npm:^2.3.8"
checksum: 10/4ffc0e14f4af97400ed0f37be83b112b25309af21dd08fa55c4513e7cb4367333f63712aec010925dbe491ef6e92db1248e1e306e589f9f6a8da8b3a9c4db90b
languageName: node
linkType: hard
"unc-path-regex@npm:^0.1.2": "unc-path-regex@npm:^0.1.2":
version: 0.1.2 version: 0.1.2
resolution: "unc-path-regex@npm:0.1.2" resolution: "unc-path-regex@npm:0.1.2"
@ -32280,7 +32590,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"wordwrap@npm:^1.0.0": "wordwrap@npm:^1.0.0, wordwrap@npm:~1.0.0":
version: 1.0.0 version: 1.0.0
resolution: "wordwrap@npm:1.0.0" resolution: "wordwrap@npm:1.0.0"
checksum: 10/497d40beb2bdb08e6d38754faa17ce20b0bf1306327f80cb777927edb23f461ee1f6bc659b3c3c93f26b08e1cf4b46acc5bae8fda1f0be3b5ab9a1a0211034cd checksum: 10/497d40beb2bdb08e6d38754faa17ce20b0bf1306327f80cb777927edb23f461ee1f6bc659b3c3c93f26b08e1cf4b46acc5bae8fda1f0be3b5ab9a1a0211034cd
@ -32383,7 +32693,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"ws@npm:^7.2.0, ws@npm:^7.3.1": "ws@npm:^7.2.0, ws@npm:^7.2.3, ws@npm:^7.3.1":
version: 7.5.10 version: 7.5.10
resolution: "ws@npm:7.5.10" resolution: "ws@npm:7.5.10"
peerDependencies: peerDependencies: