diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2d552a4..14eba0f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,6 +33,7 @@ jobs: "mingw", "nginx", "node", + "npm-packages", "playwright-deps", "python", "sonar-scanner-cli", diff --git a/README.md b/README.md index 32ce2a0..eb41a55 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,7 @@ Below is a list with included features, click on the link for more details. | [mingw](./features/src/mingw/README.md) | Installs MinGW. | | [nginx](./features/src/nginx/README.md) | Installs Nginx. | | [node](./features/src/node/README.md) | Installs Node.js. | +| [npm-packages](./features/src/npm-packages/README.md) | Installs NPM packages globally. | | [playwright-deps](./features/src/playwright-deps/README.md) | Installs all dependencies required to run Playwright. | | [python](./features/src/python/README.md) | Installs Python. | | [sonar-scanner-cli](./features/src/sonar-scanner-cli/README.md) | Installs the SonarScanner CLI. | diff --git a/build/build.go b/build/build.go index 4dc885b..714bbb2 100644 --- a/build/build.go +++ b/build/build.go @@ -170,6 +170,11 @@ func init() { gotaskr.Task("Feature:node:Test", func() error { return testFeature("node") }) gotaskr.Task("Feature:node:Publish", func() error { return publishFeature("node") }) + ////////// npm-packages + gotaskr.Task("Feature:npm-packages:Package", func() error { return packageFeature("npm-packages") }) + gotaskr.Task("Feature:npm-packages:Test", func() error { return testFeature("npm-packages") }) + gotaskr.Task("Feature:npm-packages:Publish", func() error { return publishFeature("npm-packages") }) + ////////// playwright-deps gotaskr.Task("Feature:playwright-deps:Package", func() error { return packageFeature("playwright-deps") }) gotaskr.Task("Feature:playwright-deps:Test", func() error { return testFeature("playwright-deps") }) diff --git a/features/src/npm-packages/README.md b/features/src/npm-packages/README.md new file mode 100755 index 0000000..b17bd92 --- /dev/null +++ b/features/src/npm-packages/README.md @@ -0,0 +1,19 @@ +# NPM Packages (npm-packages) + +Installs NPM packages globally. + +## Example Usage + +```json +"features": { + "ghcr.io/postfinance/devcontainer-features/npm-packages:0.1.0": { + "packages": "" + } +} +``` + +## Options + +| Option | Description | Type | Default Value | Proposals | +|-----|-----|-----|-----|-----| +| packages | Comma-separated list of packages to install | string | <empty> | @devcontainers/cli,which@3.0.1 | diff --git a/features/src/npm-packages/devcontainer-feature.json b/features/src/npm-packages/devcontainer-feature.json new file mode 100644 index 0000000..035b13a --- /dev/null +++ b/features/src/npm-packages/devcontainer-feature.json @@ -0,0 +1,20 @@ +{ + "id": "npm-packages", + "version": "0.1.0", + "name": "NPM Packages", + "description": "Installs NPM packages globally.", + "options": { + "packages": { + "type": "string", + "proposals": [ + "@devcontainers/cli,which@3.0.1" + ], + "default": "", + "description": "Comma-separated list of packages to install" + } + }, + "installsAfter": [ + "ghcr.io/postfinance/devcontainer-features/node", + "ghcr.io/devcontainers/features/node" + ] +} \ No newline at end of file diff --git a/features/src/npm-packages/install.sh b/features/src/npm-packages/install.sh new file mode 100755 index 0000000..7e5bbbb --- /dev/null +++ b/features/src/npm-packages/install.sh @@ -0,0 +1,4 @@ +. ./functions.sh + +"./installer_$(detect_arch)" \ + -packages="${PACKAGES:-""}" diff --git a/features/src/npm-packages/installer.go b/features/src/npm-packages/installer.go new file mode 100644 index 0000000..1a7b205 --- /dev/null +++ b/features/src/npm-packages/installer.go @@ -0,0 +1,51 @@ +package main + +import ( + "builder/installer" + "flag" + "fmt" + "os" + "os/exec" + "strings" +) + +////////// +// Main +////////// + +func main() { + if err := runMain(); err != nil { + fmt.Printf("Error: %v\n", err) + os.Exit(1) + } +} + +func runMain() error { + // Handle the flags + packages := flag.String("packages", "", "") + flag.Parse() + + if _, err := exec.LookPath("npm"); err != nil { + return fmt.Errorf("could not find npm, make sure to install it first.") + } + + packagesList := strings.Split(*packages, ",") + var pkgs []string + for _, p := range packagesList { + trimmed := strings.TrimSpace(p) + if trimmed != "" { + pkgs = append(pkgs, trimmed) + } + } + if len(pkgs) == 0 { + fmt.Println("No packages specified for installation.") + return nil + } + for _, pkg := range pkgs { + fmt.Printf("Installing '%s'\n", pkg) + if err := installer.Tools.Npm.InstallGlobalPackage(pkg); err != nil { + return err + } + } + return nil +} diff --git a/features/test/npm-packages/install.sh b/features/test/npm-packages/install.sh new file mode 100755 index 0000000..5715ad9 --- /dev/null +++ b/features/test/npm-packages/install.sh @@ -0,0 +1,8 @@ +#!/bin/bash +set -e + +[[ -f "$(dirname "$0")/../functions.sh" ]] && source "$(dirname "$0")/../functions.sh" +[[ -f "$(dirname "$0")/functions.sh" ]] && source "$(dirname "$0")/functions.sh" + +check_version "$(npm list -g @devcontainers/cli 2>&1)" "@devcontainers/cli" +check_version "$(npm list -g which@3.0.1 2>&1)" "which@3.0.1" diff --git a/features/test/npm-packages/scenarios.json b/features/test/npm-packages/scenarios.json new file mode 100644 index 0000000..c03c90e --- /dev/null +++ b/features/test/npm-packages/scenarios.json @@ -0,0 +1,18 @@ +{ + "install": { + "build": { + "dockerfile": "Dockerfile", + "options": [ + "--add-host=host.docker.internal:host-gateway" + ] + }, + "features": { + "ghcr.io/postfinance/devcontainer-features/node": { + "version": "24.4.1" + }, + "./npm-packages": { + "packages": "@devcontainers/cli,which@3.0.1" + } + } + } +} \ No newline at end of file diff --git a/features/test/npm-packages/test-images.json b/features/test/npm-packages/test-images.json new file mode 100644 index 0000000..baaeb21 --- /dev/null +++ b/features/test/npm-packages/test-images.json @@ -0,0 +1,5 @@ +[ + "mcr.microsoft.com/devcontainers/base:debian-11", + "mcr.microsoft.com/devcontainers/base:debian-12", + "mcr.microsoft.com/devcontainers/base:ubuntu-24.04" +] \ No newline at end of file diff --git a/installer/npm.go b/installer/npm.go index 8efdbd7..3a86f0f 100644 --- a/installer/npm.go +++ b/installer/npm.go @@ -9,7 +9,7 @@ import ( type npm struct{} -func (npm) GetLatestPackageVersion(npmPackage string) (string, error) { +func (t npm) GetLatestPackageVersion(npmPackage string) (string, error) { stdout, stderr, err := execr.RunGetOutput(false, "npm", "view", npmPackage, "version") if err != nil { return "", fmt.Errorf("failed getting latest version for %s: %s", npmPackage, stderr) @@ -17,7 +17,7 @@ func (npm) GetLatestPackageVersion(npmPackage string) (string, error) { return stdout, nil } -func (npm) GetAllPackageVersions(npmPackage string) ([]string, error) { +func (t npm) GetAllPackageVersions(npmPackage string) ([]string, error) { stdout, stderr, err := execr.RunGetOutput(false, "npm", "view", npmPackage, "versions", "--json") if err != nil { return nil, fmt.Errorf("failed getting all versions for %s: %s", npmPackage, stderr) @@ -28,3 +28,19 @@ func (npm) GetAllPackageVersions(npmPackage string) ([]string, error) { } return jsonData, nil } + +func (t npm) InstallGlobalPackageWithVersion(npmPackage, version string) error { + packageWithVersion := npmPackage + if version != "" && version != "latest" { + packageWithVersion = fmt.Sprintf("%s@%s", npmPackage, version) + } + return t.InstallGlobalPackage(packageWithVersion) +} + +func (t npm) InstallGlobalPackage(npmPackage string) error { + _, stderr, err := execr.RunGetOutput(true, "npm", "install", "-g", "--omit=dev", npmPackage) + if err != nil { + return fmt.Errorf("failed installing global npm package %s: %s", npmPackage, stderr) + } + return nil +}