This is an automated email from the ASF dual-hosted git repository.
kezhenxu94 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/skywalking-eyes.git
The following commit(s) were added to refs/heads/main by this push:
new 5537368 fix: compatibility issue with Node.js 24 (#252)
5537368 is described below
commit 55373684d1b70e5f8fd9fc8ec114a89ad11a56a3
Author: XL Zhao <[email protected]>
AuthorDate: Mon Dec 22 20:31:28 2025 +0800
fix: compatibility issue with Node.js 24 (#252)
closes apache/skywalking#13517
---
pkg/deps/npm.go | 141 +++++++++++++++++
pkg/deps/npm_resolver_cross_platform_test.go | 216 +++++++++++++++++++++++++++
pkg/deps/result.go | 1 +
3 files changed, 358 insertions(+)
diff --git a/pkg/deps/npm.go b/pkg/deps/npm.go
index b50c74c..0579158 100644
--- a/pkg/deps/npm.go
+++ b/pkg/deps/npm.go
@@ -26,6 +26,8 @@ import (
"os"
"os/exec"
"path/filepath"
+ "regexp"
+ "runtime"
"strings"
"time"
@@ -33,6 +35,58 @@ import (
"github.com/apache/skywalking-eyes/pkg/logger"
)
+// Constants for architecture names to avoid string duplication
+const (
+ archAMD64 = "amd64"
+ archARM64 = "arm64"
+ archARM = "arm"
+)
+
+// Cross-platform package pattern recognition (for precise matching)
+// These patterns work for both scoped (@scope/package-platform-arch) and
+// non-scoped (package-platform-arch) npm packages, as the platform/arch
+// suffix always appears at the end of the package name.
+// Examples:
+// - Scoped: @scope/foo-linux-x64
+// - Non-scoped: foo-linux-x64
+//
+// regex: matches package names ending with a specific string (e.g.,
"-linux-x64")
+// os: target operating system (e.g., "linux", "darwin", "windows")
+// arch: target CPU architecture (e.g., "x64", "arm64")
+var platformPatterns = []struct {
+ regex *regexp.Regexp
+ os string
+ arch string
+}{
+ // Android
+ {regexp.MustCompile(`-android-arm64$`), "android", archARM64},
+ {regexp.MustCompile(`-android-arm$`), "android", archARM},
+ {regexp.MustCompile(`-android-x64$`), "android", "x64"},
+
+ // Darwin (macOS)
+ {regexp.MustCompile(`-darwin-arm64$`), "darwin", archARM64},
+ {regexp.MustCompile(`-darwin-x64$`), "darwin", "x64"},
+
+ // Linux
+ {regexp.MustCompile(`-linux-arm64-glibc$`), "linux", archARM64},
+ {regexp.MustCompile(`-linux-arm64-musl$`), "linux", archARM64},
+ {regexp.MustCompile(`-linux-arm-glibc$`), "linux", archARM},
+ {regexp.MustCompile(`-linux-arm-musl$`), "linux", archARM},
+ {regexp.MustCompile(`-linux-x64-glibc$`), "linux", "x64"},
+ {regexp.MustCompile(`-linux-x64-musl$`), "linux", "x64"},
+ {regexp.MustCompile(`-linux-x64$`), "linux", "x64"},
+ {regexp.MustCompile(`-linux-arm64$`), "linux", archARM64},
+ {regexp.MustCompile(`-linux-arm$`), "linux", archARM},
+
+ // Windows
+ {regexp.MustCompile(`-win32-arm64$`), "windows", archARM64},
+ {regexp.MustCompile(`-win32-ia32$`), "windows", "ia32"},
+ {regexp.MustCompile(`-win32-x64$`), "windows", "x64"},
+
+ // FreeBSD
+ {regexp.MustCompile(`-freebsd-x64$`), "freebsd", "x64"},
+}
+
type NpmResolver struct {
Resolver
}
@@ -87,6 +141,8 @@ func (resolver *NpmResolver) Resolve(pkgFile string, config
*ConfigDeps, report
for _, pkg := range pkgs {
if result := resolver.ResolvePackageLicense(pkg.Name, pkg.Path,
config); result.LicenseSpdxID != "" {
report.Resolve(result)
+ } else if result.IsCrossPlatform {
+ logger.Log.Warnf("Skipping cross-platform package %s
(not for current platform %s %s)", pkg.Name, runtime.GOOS, runtime.GOARCH)
} else {
result.LicenseSpdxID = Unknown
report.Skip(result)
@@ -198,6 +254,12 @@ func (resolver *NpmResolver)
ResolvePackageLicense(pkgName, pkgPath string, conf
result := &Result{
Dependency: pkgName,
}
+
+ if !resolver.isForCurrentPlatform(pkgName) {
+ result.IsCrossPlatform = true
+ return result
+ }
+
// resolve from the package.json file
if err := resolver.ResolvePkgFile(result, pkgPath, config); err != nil {
result.ResolveErrors = append(result.ResolveErrors, err)
@@ -318,3 +380,82 @@ func (resolver *NpmResolver) ParsePkgFile(pkgFile string)
(*Package, error) {
}
return &packageInfo, nil
}
+
+// normalizeArch converts various architecture aliases into Go's canonical
naming.
+func normalizeArch(arch string) string {
+ // Convert to lowercase to handle case variations (e.g., "AMD64").
+ arch = strings.ToLower(arch)
+ switch arch {
+ // x86-64 family (64-bit Intel/AMD)
+ case "x64", "x86_64", "amd64", "x86-64":
+ return archAMD64
+ // x86 32-bit family (legacy)
+ case "ia32", "x86", "386", "i386", "i686":
+ return "386"
+ // ARM 64-bit
+ case "arm64", "aarch64":
+ return archARM64
+ // ARM 32-bit
+ case "arm", "armv7", "armhf", "armv7l", "armel":
+ return archARM
+ // Unknown architecture: return as-is (alternatively, could return
empty to indicate incompatibility).
+ default:
+ return arch
+ }
+}
+
+// analyzePackagePlatform extracts the target OS and architecture from a
package name.
+func (resolver *NpmResolver) analyzePackagePlatform(pkgName string) (pkgOS,
pkgArch string, partial bool) {
+ for _, pattern := range platformPatterns {
+ if pattern.regex.MatchString(pkgName) {
+ return pattern.os, pattern.arch, false
+ }
+ }
+
+ // Detect OS-only suffixes like "-linux", "-darwin", "-win32"
+ osOnlyPatterns := []string{
+ "-linux",
+ "-darwin",
+ "-win32",
+ "-windows",
+ "-android",
+ "-freebsd",
+ }
+ for _, osSuffix := range osOnlyPatterns {
+ if strings.HasSuffix(pkgName, osSuffix) {
+ return strings.TrimPrefix(osSuffix, "-"), "", true
+ }
+ }
+
+ return "", "", false
+}
+
+// isForCurrentPlatform checks whether the package is intended for the current
OS and architecture.
+func (resolver *NpmResolver) isForCurrentPlatform(pkgName string) bool {
+ pkgOS, pkgArch, partial := resolver.analyzePackagePlatform(pkgName)
+
+ // OS-only package: explicitly reject with friendly error
+ if partial {
+ logger.Log.Warnf(
+ "Package %q declares a platform without architecture. "+
+ "Please use a full platform-arch suffix (e.g.
-linux-x64, -darwin-arm64).",
+ pkgName,
+ )
+ return false
+ }
+
+ // Universal package
+ if pkgOS == "" && pkgArch == "" {
+ return true
+ }
+
+ currentOS := runtime.GOOS
+ currentArch := runtime.GOARCH
+
+ return pkgOS == currentOS && resolver.isArchCompatible(pkgArch,
currentArch)
+}
+
+// isArchCompatible determines whether the package's architecture is
compatible with the current machine's architecture.
+func (resolver *NpmResolver) isArchCompatible(pkgArch, currentArch string)
bool {
+ return normalizeArch(pkgArch) == normalizeArch(currentArch)
+}
diff --git a/pkg/deps/npm_resolver_cross_platform_test.go
b/pkg/deps/npm_resolver_cross_platform_test.go
new file mode 100644
index 0000000..4c363a5
--- /dev/null
+++ b/pkg/deps/npm_resolver_cross_platform_test.go
@@ -0,0 +1,216 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package deps_test
+
+import (
+ "os"
+ "path/filepath"
+ "runtime"
+ "strings"
+ "testing"
+
+ "github.com/apache/skywalking-eyes/pkg/deps"
+)
+
+const (
+ npmLicenseMIT = "MIT"
+ npmLicenseApache20 = "Apache-2.0"
+)
+
+// TC-NEW-001
+// Regression test: cross-platform npm binary packages must be skipped safely.
+func TestResolvePackageLicense_SkipCrossPlatformPackages(t *testing.T) {
+ resolver := &deps.NpmResolver{}
+ cfg := &deps.ConfigDeps{}
+
+ var crossPlatformPkgs []string
+ switch runtime.GOOS {
+ case "linux":
+ crossPlatformPkgs = []string{
+ "@parcel/watcher-darwin-arm64",
+ "@parcel/watcher-win32-x64",
+ }
+ case "darwin":
+ crossPlatformPkgs = []string{
+ "@parcel/watcher-linux-x64",
+ "@parcel/watcher-win32-x64",
+ }
+ default: // windows
+ crossPlatformPkgs = []string{
+ "@parcel/watcher-linux-x64",
+ }
+ }
+
+ for _, pkg := range crossPlatformPkgs {
+ pkg := pkg // capture loop variable
+
+ t.Run(pkg+"/path-not-exist", func(t *testing.T) {
+ // 001-A: cross-platform + path not exist
+ result := resolver.ResolvePackageLicense(pkg,
"/non/existent/path", cfg)
+ if result.LicenseSpdxID != "" {
+ t.Fatalf(
+ "expected empty license for
cross-platform package %q, got %q",
+ pkg,
+ result.LicenseSpdxID,
+ )
+ }
+ })
+
+ t.Run(pkg+"/package-json-exists", func(t *testing.T) {
+ // 001-B: cross-platform + package.json exists
+ tmp := t.TempDir()
+ err := os.WriteFile(
+ filepath.Join(tmp, "package.json"),
+
[]byte("{\"name\":\"fake-cross-platform\",\"license\":\""+npmLicenseMIT+"\"}"),
+ 0o600,
+ )
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ result := resolver.ResolvePackageLicense(pkg, tmp, cfg)
+ if result.LicenseSpdxID != "" {
+ t.Fatalf(
+ "expected empty license for
cross-platform package %q, got %q",
+ pkg,
+ result.LicenseSpdxID,
+ )
+ }
+ })
+
+ t.Run(pkg+"/valid-license", func(t *testing.T) {
+ // 001-C: cross-platform + valid SPDX license
+ tmp := t.TempDir()
+ err := os.WriteFile(
+ filepath.Join(tmp, "package.json"),
+
[]byte("{\"name\":\"fake-cross-platform\",\"license\":\""+npmLicenseApache20+"\"}"),
+ 0o600,
+ )
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ result := resolver.ResolvePackageLicense(pkg, tmp, cfg)
+ if result.LicenseSpdxID != "" {
+ t.Fatalf(
+ "expected empty license for
cross-platform package %q, got %q",
+ pkg,
+ result.LicenseSpdxID,
+ )
+ }
+ })
+ }
+}
+
+// TC-NEW-002
+// Functional test: current-platform packages should be resolved normally.
+func TestResolvePackageLicense_CurrentPlatformPackages(t *testing.T) {
+ resolver := &deps.NpmResolver{}
+ cfg := &deps.ConfigDeps{}
+
+ t.Run("normal package with license field", func(t *testing.T) {
+ tmp := t.TempDir()
+ err := os.WriteFile(
+ filepath.Join(tmp, "package.json"),
+
[]byte("{\"name\":\"normal-pkg\",\"license\":\""+npmLicenseApache20+"\"}"),
+ 0o600,
+ )
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ result := resolver.ResolvePackageLicense("normal-pkg", tmp, cfg)
+ if result.LicenseSpdxID != npmLicenseApache20 {
+ t.Fatalf(
+ "expected license %s, got %q",
+ npmLicenseApache20,
+ result.LicenseSpdxID,
+ )
+ }
+ })
+
+ t.Run("package without license field", func(t *testing.T) {
+ tmp := t.TempDir()
+ err := os.WriteFile(
+ filepath.Join(tmp, "package.json"),
+ []byte("{\"name\":\"no-license-pkg\"}"),
+ 0o600,
+ )
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ result := resolver.ResolvePackageLicense("no-license-pkg", tmp,
cfg)
+ if result.LicenseSpdxID != "" {
+ t.Fatalf(
+ "expected empty license, got %q",
+ result.LicenseSpdxID,
+ )
+ }
+ })
+}
+
+// TC-NEW-003
+// Stability & defensive tests: malformed inputs must never cause panic.
+func TestResolvePackageLicense_DefensiveScenarios(t *testing.T) {
+ resolver := &deps.NpmResolver{}
+ cfg := &deps.ConfigDeps{}
+
+ t.Run("non-existent path", func(_ *testing.T) {
+ _ = resolver.ResolvePackageLicense("some-pkg",
"/definitely/not/exist", cfg)
+ })
+
+ t.Run("malformed package.json", func(t *testing.T) {
+ tmp := t.TempDir()
+ err := os.WriteFile(
+ filepath.Join(tmp, "package.json"),
+ []byte("{\"name\":\"bad-json\",\"license\":"),
+ 0o600,
+ )
+ if err != nil {
+ t.Fatal(err)
+ }
+ _ = resolver.ResolvePackageLicense("bad-json", tmp, cfg)
+ })
+
+ t.Run("invalid license field type", func(t *testing.T) {
+ tmp := t.TempDir()
+ err := os.WriteFile(
+ filepath.Join(tmp, "package.json"),
+ []byte("{\"name\":\"weird-pkg\",\"license\":123}"),
+ 0o600,
+ )
+ if err != nil {
+ t.Fatal(err)
+ }
+ _ = resolver.ResolvePackageLicense("weird-pkg", tmp, cfg)
+ })
+
+ t.Run("empty package name", func(_ *testing.T) {
+ _ = resolver.ResolvePackageLicense("", "/not/exist", cfg)
+ })
+
+ t.Run("overly long package name", func(_ *testing.T) {
+ longName := strings.Repeat("a", 10_000)
+ _ = resolver.ResolvePackageLicense(longName, "/not/exist", cfg)
+ })
+
+ t.Run("path traversal-like package name", func(_ *testing.T) {
+ _ = resolver.ResolvePackageLicense("../../../../etc/passwd",
"/not/exist", cfg)
+ })
+}
diff --git a/pkg/deps/result.go b/pkg/deps/result.go
index 66f87df..1c3f645 100644
--- a/pkg/deps/result.go
+++ b/pkg/deps/result.go
@@ -38,6 +38,7 @@ type Result struct {
LicenseSpdxID string
ResolveErrors []error
Version string
+ IsCrossPlatform bool
}
// Report is a collection of resolved Result.