aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
Diffstat (limited to 'tools/licensegen/scan_npm.go')
-rw-r--r--tools/licensegen/scan_npm.go238+0 −238
1 files changed, 0 insertions, 238 deletions
diff --git a/tools/licensegen/scan_npm.go b/tools/licensegen/scan_npm.go
deleted file mode 100644
--- a/tools/licensegen/scan_npm.go
+++ /dev/null
@@ -1,238 +0,0 @@
-// SPDX-FileCopyrightText: 2026 Nikolay Govorov
-// SPDX-License-Identifier: AGPL-3.0-or-later
-
-package main
-
-import (
- "encoding/json"
- "fmt"
- "os"
- "path/filepath"
- "slices"
- "strings"
-)
-
-const webDir = "cmd/mirum-server/web"
-
-// scanNPM reads package-lock.json (v3+), filters to runtime packages, reads
-// each package's LICENSE verbatim (or synthesizes a copyright notice when
-// none ships — canonical SPDX text is never substituted), and collapses the
-// scopes listed in collapsedScopes into single "@scope" rows.
-func scanNPM(root string) ([]Dep, error) {
- var lock struct {
- LockfileVersion int `json:"lockfileVersion"`
- Packages map[string]struct {
- Version string `json:"version"`
- License any `json:"license"`
- Dev, DevOptional, Link, Peer bool
- } `json:"packages"`
- }
- raw, err := os.ReadFile(filepath.Join(root, webDir, "package-lock.json"))
- if err != nil {
- return nil, err
- }
- if err := json.Unmarshal(raw, &lock); err != nil {
- return nil, err
- }
- if lock.LockfileVersion < 3 {
- return nil, fmt.Errorf("lockfileVersion %d unsupported, regenerate with npm v7+", lock.LockfileVersion)
- }
-
- // npm hoists identical name@version under multiple paths; skip duplicates
- // before the expensive LICENSE read.
- seen := map[string]bool{}
- var deps []Dep
- for path, pkg := range lock.Packages {
- if path == "" || pkg.Link || pkg.Dev || pkg.DevOptional {
- continue
- }
- name := npmName(path)
- key := name + "@" + pkg.Version
- if seen[key] {
- continue
- }
- seen[key] = true
-
- expr, err := npmSPDX(pkg.License)
- if err != nil {
- return nil, fmt.Errorf("%s: %w", key, err)
- }
-
- atoms, err := validateSPDX(expr)
- if err != nil {
- return nil, fmt.Errorf("%s declares %q: %w", key, expr, err)
- }
-
- pkgDir := filepath.Join(root, webDir, path)
- deps = append(deps, Dep{
- Name: name,
- Version: pkg.Version,
- SPDX: expr,
- URL: "https://www.npmjs.com/package/" + name + "/v/" + pkg.Version,
- atoms: atoms,
- text: npmText(pkgDir, name),
- })
- }
-
- scopeOf := func(name string) string {
- if !strings.HasPrefix(name, "@") {
- return ""
- }
- scope, _, ok := strings.Cut(name, "/")
- if !ok {
- return ""
- }
- return scope
- }
-
- buckets := map[string][]Dep{}
- var out []Dep
- for _, d := range deps {
- if s := scopeOf(d.Name); slices.Contains(collapsedScopes, s) {
- buckets[s] = append(buckets[s], d)
- continue
- }
- out = append(out, d)
- }
-
- for scope, items := range buckets {
- spdx := items[0].SPDX
- version := items[0].Version
- for _, d := range items[1:] {
- if d.SPDX != spdx {
- return nil, fmt.Errorf("scope %s: mixed SPDX %q vs %q (%s)", scope, spdx, d.SPDX, d.Name)
- }
- if d.Version != version {
- version = ""
- }
- }
-
- // Prefer umbrella LICENSE, then any sub-package's own.
- text, _, err := readLicenseFile(filepath.Join(root, webDir, "node_modules", strings.TrimPrefix(scope, "@")))
- if err != nil {
- text = items[0].text
- for _, d := range items {
- if !strings.HasPrefix(d.text, "Copyright (c) contributors to ") {
- text = d.text
- break
- }
- }
- }
-
- out = append(out, Dep{
- Name: strings.TrimPrefix(scope, "@"),
- Version: version,
- SPDX: spdx,
- URL: "https://www.npmjs.com/~" + strings.TrimPrefix(scope, "@"),
- Count: len(items),
- atoms: items[0].atoms,
- text: text,
- })
- }
- return out, nil
-}
-
-// npmName extracts the package name from an npm lockfile key like
-// "node_modules/foo" or "node_modules/foo/node_modules/@scope/bar".
-func npmName(path string) string {
- i := strings.LastIndex(path, "node_modules/")
- if i < 0 {
- return ""
- }
- n := path[i+len("node_modules/"):]
- if strings.HasPrefix(n, "@") {
- return n // scoped "@scope/name" is one name
- }
- head, _, _ := strings.Cut(n, "/")
- return head
-}
-
-// npmSPDX normalises package.json's `license` field. Modern packages use a
-// string; we accept legacy array-of-objects too. Outer parens are stripped
-// so "(MIT OR Apache-2.0)" displays as "MIT OR Apache-2.0".
-func npmSPDX(v any) (string, error) {
- var s string
- switch x := v.(type) {
- case string:
- s = strings.TrimSpace(x)
- case []any:
- var ids []string
- for _, it := range x {
- if m, ok := it.(map[string]any); ok {
- if t, ok := m["type"].(string); ok && t != "" {
- ids = append(ids, t)
- }
- }
- }
- s = strings.Join(ids, " OR ")
- }
- if s == "" {
- return "", fmt.Errorf("no license field")
- }
- for strings.HasPrefix(s, "(") && strings.HasSuffix(s, ")") {
- s = strings.TrimSpace(s[1 : len(s)-1])
- }
- return s, nil
-}
-
-// npmText returns the LICENSE text shipped with a package, or a copyright
-// notice derived from package.json when no LICENSE file exists. Canonical
-// SPDX text is never substituted — doing so would claim the author wrote
-// something they didn't ship.
-func npmText(pkgDir, name string) string {
- if text, _, err := readLicenseFile(pkgDir); err == nil {
- return text
- }
- if c := copyrightFromPackageJSON(pkgDir); c != "" {
- return c + "\n"
- }
- return "Copyright (c) contributors to " + name + "\n"
-}
-
-// copyrightFromPackageJSON builds a "Copyright (c) ..." line from the
-// package's author/contributors fields. Returns empty if neither is present.
-func copyrightFromPackageJSON(pkgDir string) string {
- data, err := os.ReadFile(filepath.Join(pkgDir, "package.json"))
- if err != nil {
- return ""
- }
- var pj struct {
- Author any `json:"author"`
- Contributors []any `json:"contributors"`
- }
- if err := json.Unmarshal(data, &pj); err != nil {
- return ""
- }
- var names []string
- for _, v := range append([]any{pj.Author}, pj.Contributors...) {
- if s := personName(v); s != "" {
- names = append(names, s)
- }
- }
- if len(names) == 0 {
- return ""
- }
- return "Copyright (c) " + strings.Join(names, ", ")
-}
-
-// personName renders an npm author/contributor entry (string or {name,email})
-// as "Name <email>" or just "Name".
-func personName(v any) string {
- switch x := v.(type) {
- case string:
- return strings.TrimSpace(x)
- case map[string]any:
- name, _ := x["name"].(string)
- email, _ := x["email"].(string)
- name = strings.TrimSpace(name)
- email = strings.TrimSpace(email)
- if name == "" {
- return ""
- }
- if email == "" {
- return name
- }
- return name + " <" + email + ">"
- }
- return ""
-}