Skip to content

Commit

Permalink
feat: Add config to use package-lock.json for dependency installation (
Browse files Browse the repository at this point in the history
…#964)

* feat: Add usePackageLock config option

If `true`, during installation of npm packages, the provided
package-lock.json will be retained which will speed up the installation
process by allowing npm to skip computing the full dependency tree.

* Add `usePackageLock` to schema

* Enforce an exact version in package.json

* whitespace
  • Loading branch information
mhan83 authored Nov 6, 2024
1 parent 71491e5 commit b63fcf3
Show file tree
Hide file tree
Showing 8 changed files with 198 additions and 5 deletions.
4 changes: 4 additions & 0 deletions api/saucectl.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,10 @@
"description": "Settings specific to npm.",
"type": "object",
"properties": {
"usePackageLock": {
"type": "boolean",
"description": "Specifies whether to use the project's package-lock.json when installing npm dependencies. If true, package-lock.json will be used during installation which will improve the speed of installation."
},
"packages": {
"description": "Specifies any npm packages that are required to run tests.",
"type": "object"
Expand Down
4 changes: 4 additions & 0 deletions api/v1/subschema/npm.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@
"description": "Settings specific to npm.",
"type": "object",
"properties": {
"usePackageLock": {
"type": "boolean",
"description": "Specifies whether to use the project's package-lock.json when installing npm dependencies. If true, package-lock.json will be used during installation which will improve the speed of installation."
},
"packages": {
"description": "Specifies any npm packages that are required to run tests.",
"type": "object"
Expand Down
44 changes: 39 additions & 5 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"os"
"path/filepath"
"reflect"
"regexp"
"strconv"
"strings"
"time"
Expand All @@ -20,6 +21,7 @@ import (
_ "github.com/santhosh-tekuri/jsonschema/v5/httploader"

"github.com/saucelabs/saucectl/internal/msg"
"github.com/saucelabs/saucectl/internal/node"
"github.com/saucelabs/saucectl/internal/viper"
)

Expand Down Expand Up @@ -199,11 +201,12 @@ type Registry struct {
// Npm represents the npm settings
type Npm struct {
// Deprecated. Use Registries instead.
Registry string `yaml:"registry,omitempty" json:"registry,omitempty"`
Registries []Registry `yaml:"registries" json:"registries,omitempty"`
Packages map[string]string `yaml:"packages,omitempty" json:"packages"`
Dependencies []string `yaml:"dependencies,omitempty" json:"dependencies"`
StrictSSL *bool `yaml:"strictSSL,omitempty" json:"strictSSL"`
Registry string `yaml:"registry,omitempty" json:"registry,omitempty"`
Registries []Registry `yaml:"registries" json:"registries,omitempty"`
Packages map[string]string `yaml:"packages,omitempty" json:"packages"`
Dependencies []string `yaml:"dependencies,omitempty" json:"dependencies"`
StrictSSL *bool `yaml:"strictSSL,omitempty" json:"strictSSL"`
UsePackageLock bool `yaml:"usePackageLock,omitempty" json:"usePackageLock"`
}

// Defaults represents default suite settings.
Expand Down Expand Up @@ -610,3 +613,34 @@ func ValidateArtifacts(artifacts Artifacts) error {
}
return nil
}

func ValidatePackageLock() error {
_, err := os.Stat("package-lock.json")
if err != nil {
return fmt.Errorf("missing package-lock.json")
}
return nil
}

var reExactVersion = regexp.MustCompile(`^\d`)

func ValidatePackage(packages node.Package, frameworkName string, expectedVersion string) error {
var ver string
var ok bool
ver, ok = packages.Dependencies[frameworkName]
if !ok {
ver, ok = packages.DevDependencies[frameworkName]
}

if !ok {
return fmt.Errorf("missing framework version. The framework version in your config file (%s) must exactly match the framework version in your package.json", expectedVersion)
}
if !reExactVersion.MatchString(ver) {
return fmt.Errorf("invalid framework version. The framework version in your package.json (%s) must be exact", ver)
}
if expectedVersion != "package.json" && expectedVersion != ver {
return fmt.Errorf("framework version mismatch. The framework version in your config file (%s) must exactly match the framework version in your package.json (%s)", expectedVersion, ver)
}

return nil
}
97 changes: 97 additions & 0 deletions internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"strings"
"testing"

"github.com/saucelabs/saucectl/internal/node"
"github.com/stretchr/testify/assert"
)

Expand Down Expand Up @@ -364,3 +365,99 @@ func TestValidateRegistries(t *testing.T) {
})
}
}

func TestValidatePackage(t *testing.T) {
type args struct {
parsedPackages node.Package
framework string
expectedVersion string
}
tests := []struct {
name string
args args
wantErr bool
}{
{
name: "passes with all requirements met in Dependencies",
args: args{
parsedPackages: node.Package{
Dependencies: map[string]string{
"framework": "1.2.3",
},
},
framework: "framework",
expectedVersion: "1.2.3",
},
wantErr: false,
},
{
name: "passes with all requirements met in DevDependencies",
args: args{
parsedPackages: node.Package{
Dependencies: map[string]string{
"framework": "1.2.3",
},
},
framework: "framework",
expectedVersion: "1.2.3",
},
wantErr: false,
},
{
name: "passes with all requirements met using package.json",
args: args{
parsedPackages: node.Package{
Dependencies: map[string]string{
"framework": "1.2.3",
},
},
framework: "framework",
expectedVersion: "package.json",
},
wantErr: false,
},
{
name: "fails when framework missing in packages",
args: args{
parsedPackages: node.Package{},
framework: "framework",
expectedVersion: "1.2.3",
},
wantErr: true,
},
{
name: "fails when using semver range",
args: args{
parsedPackages: node.Package{
Dependencies: map[string]string{
"framework": "~1.2.0",
},
},
framework: "framework",
expectedVersion: "1.2.3",
},
wantErr: true,
},
{
name: "fails when versions don't match",
args: args{
parsedPackages: node.Package{
Dependencies: map[string]string{
"framework": "1.2.3",
},
},
framework: "framework",
expectedVersion: "2.0.0",
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ValidatePackage(tt.args.parsedPackages, tt.args.framework, tt.args.expectedVersion)
if (err != nil) != tt.wantErr {
t.Errorf("ValidatePackage() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
13 changes: 13 additions & 0 deletions internal/cucumber/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"github.com/saucelabs/saucectl/internal/fpath"
"github.com/saucelabs/saucectl/internal/insights"
"github.com/saucelabs/saucectl/internal/msg"
"github.com/saucelabs/saucectl/internal/node"
"github.com/saucelabs/saucectl/internal/region"
"github.com/saucelabs/saucectl/internal/saucereport"
)
Expand Down Expand Up @@ -189,6 +190,18 @@ func Validate(p *Project) error {
if err := config.ValidateArtifacts(p.Artifacts); err != nil {
return err
}
if p.Npm.UsePackageLock {
if err := config.ValidatePackageLock(); err != nil {
return fmt.Errorf("invalid use of usePackageLock: %w", err)
}
packages, err := node.PackageFromFile("package.json")
if err != nil {
return fmt.Errorf("invalid use of usePackageLock. Failed to read package.json: %w", err)
}
if err := config.ValidatePackage(packages, "@playwright/test", p.Playwright.Version); err != nil {
return fmt.Errorf("invalid use of usePackageLock: %w", err)
}
}

p.Playwright.Version = config.StandardizeVersionFormat(p.Playwright.Version)
if p.Playwright.Version == "" {
Expand Down
13 changes: 13 additions & 0 deletions internal/cypress/v1/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/saucelabs/saucectl/internal/cypress/suite"
"github.com/saucelabs/saucectl/internal/fpath"
"github.com/saucelabs/saucectl/internal/msg"
"github.com/saucelabs/saucectl/internal/node"
"github.com/saucelabs/saucectl/internal/region"
"github.com/saucelabs/saucectl/internal/sauceignore"
"github.com/saucelabs/saucectl/internal/saucereport"
Expand Down Expand Up @@ -219,6 +220,18 @@ func (p *Project) Validate() error {
if err := config.ValidateArtifacts(p.Artifacts); err != nil {
return err
}
if p.Npm.UsePackageLock {
if err := config.ValidatePackageLock(); err != nil {
return fmt.Errorf("invalid use of usePackageLock: %w", err)
}
packages, err := node.PackageFromFile("package.json")
if err != nil {
return fmt.Errorf("invalid use of usePackageLock. Failed to read package.json: %w", err)
}
if err := config.ValidatePackage(packages, "cypress", p.Cypress.Version); err != nil {
return fmt.Errorf("invalid use of usePackageLock: %w", err)
}
}

if p.Sauce.LaunchOrder != "" && p.Sauce.LaunchOrder != config.LaunchOrderFailRate {
return fmt.Errorf(msg.InvalidLaunchingOption, p.Sauce.LaunchOrder, string(config.LaunchOrderFailRate))
Expand Down
14 changes: 14 additions & 0 deletions internal/playwright/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/saucelabs/saucectl/internal/fpath"
"github.com/saucelabs/saucectl/internal/insights"
"github.com/saucelabs/saucectl/internal/msg"
"github.com/saucelabs/saucectl/internal/node"
"github.com/saucelabs/saucectl/internal/playwright/grep"
"github.com/saucelabs/saucectl/internal/region"
"github.com/saucelabs/saucectl/internal/sauceignore"
Expand Down Expand Up @@ -328,6 +329,19 @@ func Validate(p *Project) error {
return err
}

if p.Npm.UsePackageLock {
if err := config.ValidatePackageLock(); err != nil {
return fmt.Errorf("invalid use of usePackageLock: %w", err)
}
packages, err := node.PackageFromFile("package.json")
if err != nil {
return fmt.Errorf("invalid use of usePackageLock. Failed to read package.json: %w", err)
}
if err := config.ValidatePackage(packages, "@playwright/test", p.Playwright.Version); err != nil {
return fmt.Errorf("invalid use of usePackageLock: %w", err)
}
}

if p.Sauce.LaunchOrder != "" && p.Sauce.LaunchOrder != config.LaunchOrderFailRate {
return fmt.Errorf(msg.InvalidLaunchingOption, p.Sauce.LaunchOrder, string(config.LaunchOrderFailRate))
}
Expand Down
14 changes: 14 additions & 0 deletions internal/testcafe/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/saucelabs/saucectl/internal/fpath"
"github.com/saucelabs/saucectl/internal/insights"
"github.com/saucelabs/saucectl/internal/msg"
"github.com/saucelabs/saucectl/internal/node"
"github.com/saucelabs/saucectl/internal/region"
"github.com/saucelabs/saucectl/internal/sauceignore"
"github.com/saucelabs/saucectl/internal/saucereport"
Expand Down Expand Up @@ -263,6 +264,19 @@ func Validate(p *Project) error {
return err
}

if p.Npm.UsePackageLock {
if err := config.ValidatePackageLock(); err != nil {
return fmt.Errorf("invalid use of usePackageLock: %w", err)
}
packages, err := node.PackageFromFile("package.json")
if err != nil {
return fmt.Errorf("invalid use of usePackageLock. Failed to read package.json: %w", err)
}
if err := config.ValidatePackage(packages, "testcafe", p.Testcafe.Version); err != nil {
return fmt.Errorf("invalid use of usePackageLock: %w", err)
}
}

p.Testcafe.Version = config.StandardizeVersionFormat(p.Testcafe.Version)
if p.Testcafe.Version == "" {
return errors.New(msg.MissingFrameworkVersionConfig)
Expand Down

0 comments on commit b63fcf3

Please sign in to comment.