Subject: [PATCH 2/2] Introduce (modify-json), (delete-fields), and (replace-fields) to node-build-system

From: Daniel Khodabakhsh <[email-masked]>
Date: Sun, 29 Dec 2024 21:58:35 +0000

This change introduces helper procedures (modify-json) which takes in lambdas
 which modify the target json #:file which defaults to package.json
This change also includes (delete-fields) and (replace-fields) to help deleting
 and replacing the value of fields in a package.json file.

Signed-off-by: Daniel Khodabakhsh <[email-masked]>
Change-Id: I957f7ca814078d2136d5261985174820235f1369
---
 gnu/packages/node-xyz.scm        | 324 ++++++++++++++-----------------
 gnu/packages/node.scm            |  86 ++++----
 guix/build/node-build-system.scm | 198 +++++++++++++++----
 3 files changed, 355 insertions(+), 253 deletions(-)

diff --git a/gnu/packages/node-xyz.scm b/gnu/packages/node-xyz.scm
index ec3f9fbfb1..cdc69de0c1 100644
--- a/gnu/packages/node-xyz.scm
+++ b/gnu/packages/node-xyz.scm
@@ -69,18 +69,7 @@ (define-public node-acorn
              ;; We need to remove the prepare script from "package.json", as
              ;; it would try to use the build environment and would block the
              ;; automatic building by other packages making use of node-acorn.
-             ;; TODO: Add utility function
-             (with-atomic-json-file-replacement (lambda (pkg-meta-alist)
-               (map
-                 (match-lambda
-                   (("scripts" . scripts-alist)
-                     (cons "scripts" (filter
-                       (match-lambda
-                         (("prepare" . _) #f)
-                         (_ #t))
-                       scripts-alist)))
-                   (other other))
-                 pkg-meta-alist)))))
+             (modify-json (delete-fields '(("scripts" "prepare"))))))
          (replace 'build
            (lambda* (#:key inputs native-inputs #:allow-other-keys)
              (let ((esbuild (search-input-file (or native-inputs inputs)
@@ -136,38 +125,26 @@ (define-public node-addon-api
        (modify-phases %standard-phases
          (add-after 'patch-dependencies 'delete-dependencies
            (lambda args
-             (delete-dependencies
-              `("benchmark"
-                "bindings"
-                "clang-format"
-                "eslint"
-                "eslint-config-semistandard"
-                "eslint-config-standard"
-                "eslint-plugin-import"
-                "eslint-plugin-node"
-                "eslint-plugin-promise"
-                "fs-extra"
-                "neostandard"
-                "path"
-                "pre-commit"
-                "semver"))))
-         (add-after 'unpack 'skip-js-tests
-           ;; We can't run the js-based tests,
-           ;; but we can still do the C++ parts
-           (lambda args
-             (define new-test-script
-               "echo stopping after pretest on Guix")
-             (with-atomic-json-file-replacement (lambda (pkg-meta-alist)
-               (map
-                 (match-lambda
-                   (("scripts" . scripts-alist)
-                     (cons "scripts" (map
-                       (match-lambda
-                         (("test" . _) (cons "test" new-test-script))
-                         (other other))
-                       scripts-alist)))
-                   (other other))
-                 pkg-meta-alist))))))))
+             (modify-json
+               (delete-dependencies `(
+                 "benchmark"
+                 "bindings"
+                 "clang-format"
+                 "eslint"
+                 "eslint-config-semistandard"
+                 "eslint-config-standard"
+                 "eslint-plugin-import"
+                 "eslint-plugin-node"
+                 "eslint-plugin-promise"
+                 "fs-extra"
+                 "neostandard"
+                 "path"
+                 "pre-commit"
+                 "semver"))
+               ;; We can't run the js-based tests,
+               ;; but we can still do the C++ parts
+               (replace-fields (list (cons
+                 "scripts.test" "echo stopping after pretest on Guix")))))))))
     (home-page "https://github.com/nodejs/node-addon-api")
     (synopsis "Node.js API (Node-API) header-only C++ wrappers")
     (description "This module contains header-only C++ wrapper classes which
@@ -232,7 +209,7 @@ (define-public node-buffer-crc32
        #:phases (modify-phases %standard-phases
                   (add-after 'patch-dependencies 'delete-dependencies
                     (lambda _
-                      (delete-dependencies '("tap")))))))
+                      (modify-json (delete-dependencies '("tap"))))))))
     (home-page "https://github.com/brianloveswords/buffer-crc32")
     (synopsis "CRC32 implementation in Javascript")
     (description
@@ -288,14 +265,15 @@ (define-public node-crx3
                          "minimist"))))
                   (add-after 'patch-dependencies 'delete-dependencies
                     (lambda _
-                      (delete-dependencies '("c8"
-                                             "docdash"
-                                             "eslint"
-                                             "eslint-plugin-jsdoc"
-                                             "jsdoc"
-                                             "tap-diff"
-                                             "tape"
-                                             "tape-catch")))))))
+                      (modify-json (delete-dependencies
+                        '("c8"
+                          "docdash"
+                          "eslint"
+                          "eslint-plugin-jsdoc"
+                          "jsdoc"
+                          "tap-diff"
+                          "tape"
+                          "tape-catch"))))))))
     (inputs (list node-minimist node-pbf node-yazl))
     (home-page "https://github.com/ahwayakchih/crx3")
     (synopsis "Create CRXv3 browser extensions with Javascript")
@@ -325,18 +303,19 @@ (define-public node-debug
        (modify-phases %standard-phases
          (add-after 'patch-dependencies 'delete-dependencies
            (lambda args
-             (delete-dependencies `("brfs"
-                                    "browserify"
-                                    "coveralls"
-                                    "istanbul"
-                                    "karma"
-                                    "karma-browserify"
-                                    "karma-chrome-launcher"
-                                    "karma-mocha"
-                                    "mocha"
-                                    "mocha-lcov-reporter"
-                                    "xo"
-                                    "supports-color")))))
+             (modify-json (delete-dependencies
+               `("brfs"
+                 "browserify"
+                 "coveralls"
+                 "istanbul"
+                 "karma"
+                 "karma-browserify"
+                 "karma-chrome-launcher"
+                 "karma-mocha"
+                 "mocha"
+                 "mocha-lcov-reporter"
+                 "xo"
+                 "supports-color"))))))
        #:tests? #f))
     (home-page "https://github.com/debug-js/debug")
     (synopsis "Debugging utility for Node.js")
@@ -421,21 +400,22 @@ (define-public node-file-uri-to-path
        (modify-phases %standard-phases
          (add-after 'patch-dependencies 'delete-dependencies
            (lambda args
-             (delete-dependencies `("@types/mocha"
-                                    "@types/node"
-                                    "@typescript-eslint/eslint-plugin"
-                                    "@typescript-eslint/parser"
-                                    "cpy-cli"
-                                    "eslint"
-                                    "eslint-config-airbnb"
-                                    "eslint-config-prettier"
-                                    "eslint-import-resolver-typescript"
-                                    "eslint-plugin-import"
-                                    "eslint-plugin-jsx-a11y"
-                                    "eslint-plugin-react"
-                                    "mocha"
-                                    "rimraf"
-                                    "typescript"))))
+             (modify-json (delete-dependencies
+               `("@types/mocha"
+                 "@types/node"
+                 "@typescript-eslint/eslint-plugin"
+                 "@typescript-eslint/parser"
+                 "cpy-cli"
+                 "eslint"
+                 "eslint-config-airbnb"
+                 "eslint-config-prettier"
+                 "eslint-import-resolver-typescript"
+                 "eslint-plugin-import"
+                 "eslint-plugin-jsx-a11y"
+                 "eslint-plugin-react"
+                 "mocha"
+                 "rimraf"
+                 "typescript")))))
          (replace 'build
            (lambda* (#:key inputs native-inputs #:allow-other-keys)
              (copy-recursively "src" "dist")
@@ -497,7 +477,8 @@ (define-public node-ieee754
        #:phases (modify-phases %standard-phases
                   (add-after 'patch-dependencies 'delete-dependencies
                     (lambda _
-                      (delete-dependencies '("airtap" "standard" "tape")))))))
+                      (modify-json (delete-dependencies
+                        '("airtap" "standard" "tape"))))))))
     (home-page "https://github.com/feross/ieee754")
     (synopsis "Read/write IEEE754 floating point numbers in Javascript")
     (description "This package can read and write IEEE754 floating point
@@ -524,7 +505,7 @@ (define-public node-inherits
        (modify-phases %standard-phases
          (add-after 'patch-dependencies 'delete-dependencies
            (lambda args
-             (delete-dependencies '("tap")))))
+             (modify-json (delete-dependencies '("tap"))))))
        ;; FIXME: Tests depend on node-tap
        #:tests? #f))
     (home-page "https://github.com/isaacs/inherits")
@@ -553,8 +534,8 @@ (define-public node-irc
        (modify-phases %standard-phases
          (add-after 'patch-dependencies 'delete-dependencies
            (lambda args
-             (delete-dependencies
-              `("ansi-color" "faucet" "jscs" "tape")))))
+             (modify-json (delete-dependencies
+              `("ansi-color" "faucet" "jscs" "tape"))))))
        #:tests? #f))
     (inputs
      (list node-irc-colors))
@@ -583,7 +564,7 @@ (define-public node-irc-colors
        (modify-phases %standard-phases
          (add-after 'patch-dependencies 'delete-dependencies
            (lambda args
-             (delete-dependencies `("istanbul" "vows")))))
+             (modify-json (delete-dependencies `("istanbul" "vows"))))))
        #:tests? #f))
     (home-page "https://github.com/fent/irc-colors.js")
     (synopsis "Node.js module providing color and formatting for IRC")
@@ -657,7 +638,8 @@ (define-public node-minimist
        #:phases (modify-phases %standard-phases
                   (add-after 'patch-dependencies 'delete-dependencies
                     (lambda _
-                      (delete-dependencies '("covert" "tap" "tape")))))))
+                      (modify-json (delete-dependencies
+                        '("covert" "tap" "tape"))))))))
     (home-page "https://github.com/substack/minimist")
     (synopsis "Parse CLI arguments in Javascript")
     (description "This package can scan for CLI flags and arguments in
@@ -683,12 +665,13 @@ (define-public node-ms
        (modify-phases %standard-phases
          (add-after 'patch-dependencies 'delete-dependencies
            (lambda args
-             (delete-dependencies `("eslint"
-                                    "expect.js"
-                                    "husky"
-                                    "lint-staged"
-                                    "mocha"
-                                    "prettier")))))
+             (modify-json (delete-dependencies
+               `("eslint"
+                 "expect.js"
+                 "husky"
+                 "lint-staged"
+                 "mocha"
+                 "prettier"))))))
        #:tests? #f))
     (home-page "https://github.com/vercel/ms")
     (synopsis "Convert time to milliseconds")
@@ -716,14 +699,14 @@ (define-public node-nan
        (modify-phases %standard-phases
          (add-after 'patch-dependencies 'delete-dependencies
            (lambda args
-             (delete-dependencies
+             (modify-json (delete-dependencies
               '("bindings"
                 "commander"
                 "glob"
                 "request"
                 "node-gyp" ;; would be needed for tests
                 "tap"
-                "xtend")))))
+                "xtend"))))))
        ;; tests need tap and other dependencies
        #:tests? #f))
     (inputs
@@ -755,7 +738,8 @@ (define-public node-normalize-path
        #:phases (modify-phases %standard-phases
                   (add-after 'patch-dependencies 'delete-dependencies
                     (lambda _
-                      (delete-dependencies '("gulp-format-md" "mocha")))))))
+                      (modify-json (delete-dependencies
+                        '("gulp-format-md" "mocha"))))))))
     (native-inputs (list node-minimist))
     (home-page "https://github.com/jonschlinkert/normalize-path")
     (synopsis "Normalize slashes in a file path")
@@ -784,7 +768,7 @@ (define-public node-once
        (modify-phases %standard-phases
          (add-after 'patch-dependencies 'delete-dependencies
            (lambda args
-             (delete-dependencies '("tap")))))
+             (modify-json (delete-dependencies '("tap"))))))
        ;; FIXME: Tests depend on node-tap
        #:tests? #f))
     (inputs
@@ -841,7 +825,8 @@ (define-public node-path-key
        #:phases (modify-phases %standard-phases
                   (add-after 'patch-dependencies 'delete-dependencies
                     (lambda _
-                      (delete-dependencies '("@types/node" "ava"
"tsd" "xo")))))))
+                      (modify-json (delete-dependencies
+                        '("@types/node" "ava" "tsd" "xo"))))))))
     (home-page "https://github.com/sindresorhus/path-key")
     (synopsis "Cross-platform utility to compute the PATH environment
variable key")
     (description "@code{path-key} provides an implementation to compute the
@@ -867,7 +852,7 @@ (define-public node-pbf
        #:phases (modify-phases %standard-phases
                   (add-after 'patch-dependencies 'delete-dependencies
                     (lambda _
-                      (delete-dependencies
+                      (modify-json (delete-dependencies
                        '("benchmark"
                          "browserify"
                          "eslint"
@@ -877,7 +862,7 @@ (define-public node-pbf
                          "protocol-buffers"
                          "tap"
                          "tile-stats-runner"
-                         "uglify-js")))))))
+                         "uglify-js"))))))))
     (inputs (list node-ieee754 node-resolve-protobuf-schema))
     (home-page "https://github.com/mapbox/pbf")
     (synopsis "Decode and encode protocol buffers in Javascript")
@@ -908,7 +893,8 @@ (define-public node-protocol-buffers-schema
        #:phases (modify-phases %standard-phases
                   (add-after 'patch-dependencies 'delete-dependencies
                     (lambda _
-                      (delete-dependencies '("standard" "tape")))))))
+                      (modify-json (delete-dependencies
+                        '("standard" "tape"))))))))
     (home-page "https://github.com/mafintosh/protocol-buffers-schema")
     (synopsis "Protocol buffers schema parser written in Javascript")
     (description "This package provides a protocol buffers schema parser
@@ -935,26 +921,27 @@ (define-public node-readable-stream
        (modify-phases %standard-phases
          (add-after 'patch-dependencies 'delete-dependencies
            (lambda args
-             (delete-dependencies `("@babel/cli"
-                                    "@babel/core"
-                                    "@babel/polyfill"
-                                    "@babel/preset-env"
-                                    "airtap"
-                                    "assert"
-                                    "bl"
-                                    "deep-strict-equal"
-                                    "events.once"
-                                    "glob"
-                                    "gunzip-maybe"
-                                    "hyperquest"
-                                    "lolex"
-                                    "nyc"
-                                    "pump"
-                                    "rimraf"
-                                    "tap"
-                                    "tape"
-                                    "tar-fs"
-                                    "util-promisify")))))
+             (modify-json (delete-dependencies
+               `("@babel/cli"
+                 "@babel/core"
+                 "@babel/polyfill"
+                 "@babel/preset-env"
+                 "airtap"
+                 "assert"
+                 "bl"
+                 "deep-strict-equal"
+                 "events.once"
+                 "glob"
+                 "gunzip-maybe"
+                 "hyperquest"
+                 "lolex"
+                 "nyc"
+                 "pump"
+                 "rimraf"
+                 "tap"
+                 "tape"
+                 "tar-fs"
+                 "util-promisify"))))))
        #:tests? #f))
     (inputs (list node-util-deprecate node-string-decoder node-inherits))
     (home-page "https://github.com/nodejs/readable-stream")
@@ -983,7 +970,8 @@ (define-public node-resolve-protobuf-schema
        #:phases (modify-phases %standard-phases
                   (add-after 'patch-dependencies 'delete-dependencies
                     (lambda _
-                      (delete-dependencies '("standard" "tape")))))))
+                      (modify-json (delete-dependencies
+                        '("standard" "tape"))))))))
     (inputs (list node-protocol-buffers-schema))
     (home-page "https://github.com/mafintosh/resolve-protobuf-schema")
     (synopsis "Resolve protobuf imports")
@@ -1012,7 +1000,7 @@ (define-public node-safe-buffer
        (modify-phases %standard-phases
          (add-after 'patch-dependencies 'delete-dependencies
            (lambda args
-             (delete-dependencies '("tape" "standard")))))
+             (modify-json (delete-dependencies '("tape" "standard"))))))
        #:tests? #f))
     (home-page "https://github.com/feross/safe-buffer")
     (synopsis "Buffer creation with explicit semantics")
@@ -1040,20 +1028,21 @@ (define-public node-safe-stable-stringify
        #:phases (modify-phases %standard-phases
                   (add-after 'patch-dependencies 'delete-dependencies
                     (lambda _
-                      (delete-dependencies '("benchmark" "clone"
-                                             "fast-json-stable-stringify"
-                                             "fast-safe-stringify"
-                                             "fast-stable-stringify"
-                                             "faster-stable-stringify"
-                                             "fastest-stable-stringify"
-                                             "json-stable-stringify"
-                                             "json-stringify-deterministic"
-                                             "json-stringify-safe"
-                                             "standard"
-                                             "tap"
-                                             "typescript"
-                                             "@types/node"
-
"@types/json-stable-stringify")))))))
+                      (modify-json (delete-dependencies
+                        '("benchmark" "clone"
+                          "fast-json-stable-stringify"
+                          "fast-safe-stringify"
+                          "fast-stable-stringify"
+                          "faster-stable-stringify"
+                          "fastest-stable-stringify"
+                          "json-stable-stringify"
+                          "json-stringify-deterministic"
+                          "json-stringify-safe"
+                          "standard"
+                          "tap"
+                          "typescript"
+                          "@types/node"
+                          "@types/json-stable-stringify"))))))))
     (home-page "https://github.com/BridgeAR/safe-stable-stringify")
     (synopsis "Serialization of javascript objects")
     (description
@@ -1111,7 +1100,7 @@ (define-public node-semver
        (modify-phases %standard-phases
          (add-after 'patch-dependencies 'delete-dependencies
            (lambda args
-             (delete-dependencies '("tap")))))
+             (modify-json (delete-dependencies '("tap"))))))
        ;; FIXME: Tests depend on node-tap
        #:tests? #f))
     (home-page "https://github.com/npm/node-semver")
@@ -1151,7 +1140,8 @@ (define-public node-serialport
        (modify-phases %standard-phases
          (add-after 'patch-dependencies 'delete-dependencies
            (lambda args
-             (delete-dependencies `("@serialport/binding-mock"))))
+             (modify-json (delete-dependencies
+               `("@serialport/binding-mock")))))
          (add-after 'unpack 'chdir
            (lambda args
              (chdir "packages/serialport"))))
@@ -1210,25 +1200,10 @@ (define-public node-serialport-bindings
              (chdir "packages/bindings")))
          (add-after 'patch-dependencies 'delete-dependencies
            (lambda args
-             (delete-dependencies `("prebuild-install"
-                                    ;; devDependencies
-                                    "@serialport/binding-mock"
-                                    "node-abi"))))
-         (add-after 'chdir 'avoid-prebuild-install
-           (lambda args
-             (with-atomic-json-file-replacement (lambda (pkg-meta-alist)
-               (map
-                 (match-lambda
-                   (("scripts" . scripts-alist)
-                     (cons "scripts" (filter
-                       (match-lambda
-                         (("install" . _) #f)
-                         (_ #t))
-                       scripts-alist)))
-                   (("gypfile" . _)
-                     (cons "gypfile" #f))
-                   (other other))
-                 pkg-meta-alist))))))
+             (modify-json
+               (delete-dependencies `(
+                 ;; devDependencies
+                 "node-abi"))))))
        #:tests? #f))
     (synopsis "Abstract base class for Node SerialPort bindings")
     (description "Node SerialPort is a modular suite of Node.js packages for
@@ -1409,8 +1384,9 @@ (define-public node-serialport-stream
        (modify-phases %standard-phases
          (add-after 'patch-dependencies 'delete-dependencies
            (lambda args
-             (delete-dependencies `(;; devDependencies
-                                    "@serialport/binding-mock"))))
+             (modify-json
+               (delete-dependencies ;; devDependencies
+                 `("@serialport/binding-mock")))))
          (add-after 'unpack 'chdir
            (lambda args
              (chdir "packages/stream"))))
@@ -1461,7 +1437,7 @@ (define-public node-sqlite3
        (modify-phases %standard-phases
          (add-after 'patch-dependencies 'delete-dependencies
            (lambda args
-             (delete-dependencies
+             (modify-json (delete-dependencies
               `(;; Normally, this is "built" using @mapbox/node-pre-gyp,
                 ;; which publishes or downloads pre-built binaries or
                 ;; falls back to building from source.  Here, we patch out
@@ -1480,7 +1456,7 @@ (define-public node-sqlite3
                 "node-gyp"
                 ;; These we'd like, we just don't have them yet:
                 "eslint"
-                "mocha"))))
+                "mocha")))))
          (add-before 'configure 'npm-config-sqlite
            ;; We need this step even if we do replace @mapbox/node-pre-gyp
            ;; because the package expects to build its bundled sqlite
@@ -1514,8 +1490,9 @@ (define-public node-sqlite3
              (substitute* ".npmignore"
                (("lib/binding")
                 "#lib/binding # <- patched for Guix"))
-             (with-atomic-json-file-replacement (lambda (pkg-meta-alist)
-               (let ((binary-alist (assoc-ref pkg-meta-alist "binary")))
+             (modify-json
+               (lambda (pkg-meta-alist)
+                 (let ((binary-alist (assoc-ref pkg-meta-alist "binary")))
                  ;; When it builds from source, node-pre-gyp supplies
                  ;; module_name and module_path based on the entries under
                  ;; "binary" from "package.json", so this package's
@@ -1528,19 +1505,11 @@ (define-public node-sqlite3
                    (assoc-ref binary-alist "module_name")
                    " module_path="
                    (assoc-ref binary-alist "module_path"))))
+                 pkg-meta-alist)
                ;; We need to remove the install script from "package.json",
                ;; as it would try to use node-pre-gyp and would block the
                ;; automatic building performed by `npm install`.
-               (map
-                 (match-lambda
-                   (("scripts" . scripts-alist)
-                     (cons "scripts" (filter
-                       (match-lambda
-                         (("install" . _) #f)
-                         (_ #t))
-                       scripts-alist)))
-                   (other other))
-                 pkg-meta-alist))))))))
+               (delete-fields `(("scripts" "install")))))))))
     (home-page "https://github.com/mapbox/node-sqlite3")
     (synopsis "Node.js bindings for SQLite3")
     (description
@@ -1623,8 +1592,8 @@ (define-public node-string-decoder
        (modify-phases %standard-phases
          (add-after 'patch-dependencies 'delete-dependencies
            (lambda args
-             (delete-dependencies
-              '("tap" "core-util-is" "babel-polyfill")))))
+             (modify-json (delete-dependencies
+              '("tap" "core-util-is" "babel-polyfill"))))))
        ;; FIXME: Tests depend on node-tap
        #:tests? #f))
     (inputs (list node-safe-buffer node-inherits))
@@ -1678,7 +1647,7 @@ (define-public node-wrappy
        (modify-phases %standard-phases
          (add-after 'patch-dependencies 'delete-dependencies
            (lambda args
-             (delete-dependencies '("tap")))))))
+             (modify-json (delete-dependencies '("tap"))))))))
     (home-page "https://github.com/npm/wrappy")
     (synopsis "Callback wrapping utility")
     (description "@code{wrappy} is a utility for Node.js to wrap callbacks.")
@@ -1703,7 +1672,8 @@ (define-public node-yazl
        #:phases (modify-phases %standard-phases
                   (add-after 'patch-dependencies 'delete-dependencies
                     (lambda _
-                      (delete-dependencies '("airtap" "bl" "istanbul"
"yauzl")))))))
+                      (modify-json (delete-dependencies
+                        '("airtap" "bl" "istanbul" "yauzl"))))))))
     (inputs (list node-buffer-crc32))
     (home-page "https://github.com/thejoshwolfe/yazl")
     (synopsis "Yet another zip library for node")
diff --git a/gnu/packages/node.scm b/gnu/packages/node.scm
index 20acffb3df..a13ec5d077 100644
--- a/gnu/packages/node.scm
+++ b/gnu/packages/node.scm
@@ -13,6 +13,7 @@
 ;;; Copyright © 2021, 2022 Philip McGrath <[email-masked]>
 ;;; Copyright © 2022 Hilton Chain <[email-masked]>
 ;;; Copyright © 2024 Efraim Flashner <[email-masked]>
+;;; Copyright © 2024 Daniel Khodabakhsh <[email-masked]>
 ;;;
 ;;; This file is part of GNU Guix.
 ;;;
@@ -366,7 +367,7 @@ (define-public node-semver-bootstrap
        (modify-phases %standard-phases
          (add-after 'patch-dependencies 'delete-dependencies
            (lambda args
-             (delete-dependencies '("tap")))))))
+             (modify-json (delete-dependencies '("tap"))))))))
     (home-page "https://github.com/npm/node-semver")
     (properties '((hidden? . #t)))
     (synopsis "Parses semantic versions strings")
@@ -397,11 +398,12 @@ (define-public node-ms-bootstrap
        (modify-phases %standard-phases
          (add-after 'patch-dependencies 'delete-dependencies
            (lambda args
-             (delete-dependencies '("eslint"
-                                    "expect.js"
-                                    "husky"
-                                    "lint-staged"
-                                    "mocha")))))))
+             (modify-json (delete-dependencies
+               '("eslint"
+                 "expect.js"
+                 "husky"
+                 "lint-staged"
+                 "mocha"))))))))
     (home-page "https://github.com/zeit/ms#readme")
     (properties '((hidden? . #t)))
     (synopsis "Tiny millisecond conversion utility")
@@ -431,7 +433,7 @@ (define-public node-binary-search-bootstrap
        (modify-phases %standard-phases
          (add-after 'patch-dependencies 'delete-dependencies
            (lambda args
-             (delete-dependencies `("chai" "mocha")))))))
+             (modify-json (delete-dependencies `("chai" "mocha"))))))))
     (home-page "https://github.com/darkskyapp/binary-search#readme")
     (properties '((hidden? . #t)))
     (synopsis "Tiny binary search function with comparators")
@@ -460,17 +462,18 @@ (define-public node-debug-bootstrap
        (modify-phases %standard-phases
          (add-after 'patch-dependencies 'delete-dependencies
            (lambda args
-             (delete-dependencies `("brfs"
-                                    "browserify"
-                                    "coveralls"
-                                    "istanbul"
-                                    "karma"
-                                    "karma-browserify"
-                                    "karma-chrome-launcher"
-                                    "karma-mocha"
-                                    "mocha"
-                                    "mocha-lcov-reporter"
-                                    "xo")))))))
+             (modify-json (delete-dependencies
+               `("brfs"
+                 "browserify"
+                 "coveralls"
+                 "istanbul"
+                 "karma"
+                 "karma-browserify"
+                 "karma-chrome-launcher"
+                 "karma-mocha"
+                 "mocha"
+                 "mocha-lcov-reporter"
+                 "xo"))))))))
     (inputs (list node-ms-bootstrap))
     (home-page "https://github.com/visionmedia/debug#readme")
     (properties '((hidden? . #t)))
@@ -526,12 +529,13 @@ (define-public node-llparse-builder-bootstrap
        (modify-phases %standard-phases
          (add-after 'patch-dependencies 'delete-dependencies
            (lambda _
-             (delete-dependencies `("@types/mocha"
-                                    "@types/node"
-                                    "mocha"
-                                    "ts-node"
-                                    "tslint"
-                                    "typescript"))))
+             (modify-json (delete-dependencies
+               `("@types/mocha"
+                 "@types/node"
+                 "mocha"
+                 "ts-node"
+                 "tslint"
+                 "typescript")))))
          (replace 'build
            (lambda* (#:key inputs #:allow-other-keys)
              (let ((esbuild (search-input-file inputs "/bin/esbuild")))
@@ -587,13 +591,14 @@ (define-public node-llparse-frontend-bootstrap
        (modify-phases %standard-phases
          (add-after 'patch-dependencies 'delete-dependencies
            (lambda args
-             (delete-dependencies `("@types/debug"
-                                    "@types/mocha"
-                                    "@types/node"
-                                    "mocha"
-                                    "ts-node"
-                                    "tslint"
-                                    "typescript"))))
+             (modify-json (delete-dependencies
+               `("@types/debug"
+                 "@types/mocha"
+                 "@types/node"
+                 "mocha"
+                 "ts-node"
+                 "tslint"
+                 "typescript")))))
          (replace 'build
            (lambda* (#:key inputs #:allow-other-keys)
              (let ((esbuild (search-input-file inputs "/bin/esbuild")))
@@ -648,15 +653,16 @@ (define-public node-llparse-bootstrap
        (modify-phases %standard-phases
          (add-after 'patch-dependencies 'delete-dependencies
            (lambda args
-             (delete-dependencies `("@types/debug"
-                                    "@types/mocha"
-                                    "@types/node"
-                                    "esm"
-                                    "llparse-test-fixture"
-                                    "mocha"
-                                    "ts-node"
-                                    "tslint"
-                                    "typescript"))))
+             (modify-json (delete-dependencies
+               `("@types/debug"
+                 "@types/mocha"
+                 "@types/node"
+                 "esm"
+                 "llparse-test-fixture"
+                 "mocha"
+                 "ts-node"
+                 "tslint"
+                 "typescript")))))
          (replace 'build
            (lambda* (#:key inputs #:allow-other-keys)
              (let ((esbuild (search-input-file inputs "/bin/esbuild")))
diff --git a/guix/build/node-build-system.scm b/guix/build/node-build-system.scm
index df7ea7774c..ea23d92a05 100644
--- a/guix/build/node-build-system.scm
+++ b/guix/build/node-build-system.scm
@@ -31,18 +31,14 @@ (define-module (guix build node-build-system)
   #:use-module (srfi srfi-1)
   #:use-module (srfi srfi-71)
   #:export (%standard-phases
-            with-atomic-json-file-replacement
             delete-dependencies
-            node-build))
-
-(define* (with-atomic-json-file-replacement proc
-  #:optional (file "package.json"))
-  "Like 'with-atomic-file-replacement', but PROC is called with a single
-argument---the result of parsing FILE's contents as json---and should a value
-to be written as json to the replacement FILE."
-  (with-atomic-file-replacement file
-    (lambda (in out)
-      (scm->json (proc (json->scm in)) out))))
+            delete-dev-dependencies
+            delete-fields
+            modify-json
+            modify-json-fields
+            node-build
+            replace-fields
+            with-atomic-json-file-replacement))

 (define* (assoc-ref* alist key #:optional default)
   "Like assoc-ref, but return DEFAULT instead of #f if no value exists."
@@ -72,6 +68,161 @@ (define* (alist-update alist key proc #:optional (= equal?))
       (acons key (proc (cdr pair)) rest)
       alist)))

+;;;
+;;; package.json modification procedures
+;;;
+
+(define* (with-atomic-json-file-replacement proc
+  #:optional (file "package.json"))
+  "Like 'with-atomic-file-replacement', but PROC is called with a single
+argument---the result of parsing FILE's contents as json---and should a value
+to be written as json to the replacement FILE."
+  (with-atomic-file-replacement file
+    (lambda (in out)
+      (scm->json (proc (json->scm in)) out))))
+
+(define* (modify-json #:key (file "package.json") #:rest all-arguments)
+  "Provide package.json modifying callbacks such as (delete-dependencies ...)"
+  (let
+    (
+      (modifications
+        (let loop ((arguments all-arguments))
+          (cond
+            ((null? arguments) '())
+            ((keyword? (car arguments)) (loop (cddr arguments)))
+            (else (cons (car arguments) (loop (cdr arguments))))))))
+    (with-atomic-json-file-replacement
+      (lambda (package)
+        (fold
+          (lambda (modification package)
+            (modification package))
+          package
+          modifications))
+      file)))
+
+(define (delete-dependencies dependencies-to-remove)
+  "Rewrite 'package.json' to allow the build to proceed without packages
+listed in 'dependencies-to-remove', a list of strings naming npm packages.
+
+To prevent the deleted dependencies from being reintroduced, use this function
+only after the 'patch-dependencies' phase."
+  (lambda (pkg-meta)
+    (fold
+      (lambda (dependency-key pkg-meta)
+        (alist-update
+          pkg-meta
+          dependency-key
+          (lambda (dependencies)
+            (remove
+              (lambda (dependency)
+                (member (car dependency) dependencies-to-remove))
+              dependencies))))
+      pkg-meta
+      (list
+        "devDependencies"
+        "dependencies"
+        "peerDependencies"
+        "optionalDependencies"))))
+
+(define* (modify-json-fields
+    fields
+    field-modifier
+    #:key
+      (field-path-mapper (lambda (field) field))
+      (insert? #f)
+      (strict? #t))
+  "Provides a lambda to supply to modify-json which modifies the specified
+ json file.
+- `fields` is a list procedure-specific data structures which should include
+ the definition of a `field-path` in one of two syntaxes: dot-syntax string
+ such as `\"devDependencies.esbuild\"`, or a list of strings such as
+ `(list \"devDependencies\" \"esbuild\")`.
+- `field-modifier` is a lambda which is invoked at the position of the field.
+ It is supplied with the current field definition, the association list (alist)
+ at the field location in the json file, and the field name, also called `key`.
+- `field-path-mapper` is a lambda which instructs where the field-path is
+ located within the field structure.
+- `insert?` allows the creation of the field and any missing intermediate
+ fields.
+- `strict?` causes an error to be thrown if the exact field-path is not found
+ in the data"
+  (lambda (package)
+    (fold
+      (lambda (field package)
+        (let*
+          (
+            (field-path (field-path-mapper field))
+            (
+              field-path
+              (cond
+                ((string? field-path)
+                  (string-split field-path #\.))
+                ((and (list? field-path) (every string? field-path))
+                  field-path)
+                (else
+                  (error
+                    (string-append
+                      "Invalid field value provided, expecting a string or a "
+                      "list of string but instead got: "
+                      (with-output-to-string (lambda _ (display
field-path))))))
+              )))
+          (let loop
+            (
+              (data package)
+              (field-path field-path))
+            (let*
+              (
+                (key (car field-path))
+                (data
+                  (if (and (not (assoc key data)) insert?)
+                    (acons key '() data)
+                    data)))
+              (if (not (assoc key data))
+                (if strict?
+                  (error (string-append
+                    "Key '" key "' was not found in data: "
+                    (with-output-to-string (lambda _ (display data)))))
+                  data)
+                (if (= (length field-path) 1)
+                  (field-modifier field data key)
+                  (assoc-set!
+                    data
+                    key
+                    (loop (assoc-ref data key) (cdr field-path)))))))))
+      package
+      fields)))
+
+(define* (delete-fields fields #:key (strict? #t))
+  "Provides a lambda to supply to modify-json which deletes the specified
+ `fields` which is a list of field-paths as mentioned in `modify-json-fields`.
+ Examples:
+  (delete-fields '(
+    (\"path\" \"to\" \"field\")
+    \"path.to.other.field\"))"
+  (modify-json-fields
+    fields
+    (lambda (_ data key)
+      (assoc-remove! data key))
+    #:strict? strict?))
+
+(define* (replace-fields fields #:key (strict? #t))
+  "Provides a lambda to supply to modify-json which replaces the value of the
+ supplied field. `fields` is a list of pairs, where the first element is the
+ field-path and the second element is the value to replace the target with.
+ Examples:
+  (replace-fields '(
+    ((\"path\" \"to\" \"field\") \"new field value\")
+    (\"path.to.other.field\" \"new field value\")))"
+  (modify-json-fields
+    fields
+    (lambda (field data key)
+      (assoc-set! data key (cdr field)))
+    #:field-path-mapper (lambda (field) (car field))
+    #:strict? strict?))
+
+(define (delete-dev-dependencies)
+  (delete-fields (list "devDependencies") #:strict #f))
+
 ;;;
 ;;; Phases.
 ;;;
@@ -144,31 +295,6 @@ (define* (patch-dependencies #:key inputs
#:allow-other-keys)
                   (assoc-ref* pkg-meta "dependencies" '())))))))))
   #t)

-(define (delete-dependencies dependencies-to-remove)
-  "Rewrite 'package.json' to allow the build to proceed without packages
-listed in 'dependencies-to-remove', a list of strings naming npm packages.
-
-To prevent the deleted dependencies from being reintroduced, use this function
-only after the 'patch-dependencies' phase."
-  (with-atomic-json-file-replacement
-    (lambda (pkg-meta)
-      (fold
-        (lambda (dependency-key pkg-meta)
-          (alist-update
-            pkg-meta
-            dependency-key
-            (lambda (dependencies)
-              (remove
-                (lambda (dependency)
-                  (member (car dependency) dependencies-to-remove))
-                dependencies))))
-        pkg-meta
-        (list
-          "devDependencies"
-          "dependencies"
-          "peerDependencies"
-          "optionalDependencies")))))
-
 (define* (delete-lockfiles #:key inputs #:allow-other-keys)
   "Delete 'package-lock.json', 'yarn.lock', and 'npm-shrinkwrap.json', if they
 exist."
-- 
2.46.0
From ac54433a8aae4cd6e6c660263503b96660a35fa8 Mon Sep 17 00:00:00 2001
Message-ID: <ac544[email-masked]>
In-Reply-To: <e4ea7[email-masked]>
References: <e4ea7[email-masked]>
From: Daniel Khodabakhsh <[email-masked]>
Date: Sun, 29 Dec 2024 10:24:23 -0800
Subject: [PATCH 2/2] Introduce (modify-json), (delete-fields), and
 (replace-fields) to node-build-system

This change introduces helper procedures (modify-json) which takes in lambdas
 which modify the target json #:file which defaults to package.json
This change also includes (delete-fields) and (replace-fields) to help deleting
 and replacing the value of fields in a package.json file.

Signed-off-by: Daniel Khodabakhsh <[email-masked]>
Change-Id: I957f7ca814078d2136d5261985174820235f1369
---
 gnu/packages/node-xyz.scm        | 324 ++++++++++++++-----------------
 gnu/packages/node.scm            |  86 ++++----
 guix/build/node-build-system.scm | 198 +++++++++++++++----
 3 files changed, 355 insertions(+), 253 deletions(-)

diff --git a/gnu/packages/node-xyz.scm b/gnu/packages/node-xyz.scm
index ec3f9fbfb1..cdc69de0c1 100644
--- a/gnu/packages/node-xyz.scm
+++ b/gnu/packages/node-xyz.scm
@@ -69,18 +69,7 @@ (define-public node-acorn
              ;; We need to remove the prepare script from "package.json", as
              ;; it would try to use the build environment and would block the
              ;; automatic building by other packages making use of node-acorn.
-             ;; TODO: Add utility function
-             (with-atomic-json-file-replacement (lambda (pkg-meta-alist)
-               (map
-                 (match-lambda
-                   (("scripts" . scripts-alist)
-                     (cons "scripts" (filter
-                       (match-lambda
-                         (("prepare" . _) #f)
-                         (_ #t))
-                       scripts-alist)))
-                   (other other))
-                 pkg-meta-alist)))))
+             (modify-json (delete-fields '(("scripts" "prepare"))))))
          (replace 'build
            (lambda* (#:key inputs native-inputs #:allow-other-keys)
              (let ((esbuild (search-input-file (or native-inputs inputs)
@@ -136,38 +125,26 @@ (define-public node-addon-api
        (modify-phases %standard-phases
          (add-after 'patch-dependencies 'delete-dependencies
            (lambda args
-             (delete-dependencies
-              `("benchmark"
-                "bindings"
-                "clang-format"
-                "eslint"
-                "eslint-config-semistandard"
-                "eslint-config-standard"
-                "eslint-plugin-import"
-                "eslint-plugin-node"
-                "eslint-plugin-promise"
-                "fs-extra"
-                "neostandard"
-                "path"
-                "pre-commit"
-                "semver"))))
-         (add-after 'unpack 'skip-js-tests
-           ;; We can't run the js-based tests,
-           ;; but we can still do the C++ parts
-           (lambda args
-             (define new-test-script
-               "echo stopping after pretest on Guix")
-             (with-atomic-json-file-replacement (lambda (pkg-meta-alist)
-               (map
-                 (match-lambda
-                   (("scripts" . scripts-alist)
-                     (cons "scripts" (map
-                       (match-lambda
-                         (("test" . _) (cons "test" new-test-script))
-                         (other other))
-                       scripts-alist)))
-                   (other other))
-                 pkg-meta-alist))))))))
+             (modify-json
+               (delete-dependencies `(
+                 "benchmark"
+                 "bindings"
+                 "clang-format"
+                 "eslint"
+                 "eslint-config-semistandard"
+                 "eslint-config-standard"
+                 "eslint-plugin-import"
+                 "eslint-plugin-node"
+                 "eslint-plugin-promise"
+                 "fs-extra"
+                 "neostandard"
+                 "path"
+                 "pre-commit"
+                 "semver"))
+               ;; We can't run the js-based tests,
+               ;; but we can still do the C++ parts
+               (replace-fields (list (cons
+                 "scripts.test" "echo stopping after pretest on Guix")))))))))
     (home-page "https://github.com/nodejs/node-addon-api")
     (synopsis "Node.js API (Node-API) header-only C++ wrappers")
     (description "This module contains header-only C++ wrapper classes which
@@ -232,7 +209,7 @@ (define-public node-buffer-crc32
        #:phases (modify-phases %standard-phases
                   (add-after 'patch-dependencies 'delete-dependencies
                     (lambda _
-                      (delete-dependencies '("tap")))))))
+                      (modify-json (delete-dependencies '("tap"))))))))
     (home-page "https://github.com/brianloveswords/buffer-crc32")
     (synopsis "CRC32 implementation in Javascript")
     (description
@@ -288,14 +265,15 @@ (define-public node-crx3
                          "minimist"))))
                   (add-after 'patch-dependencies 'delete-dependencies
                     (lambda _
-                      (delete-dependencies '("c8"
-                                             "docdash"
-                                             "eslint"
-                                             "eslint-plugin-jsdoc"
-                                             "jsdoc"
-                                             "tap-diff"
-                                             "tape"
-                                             "tape-catch")))))))
+                      (modify-json (delete-dependencies
+                        '("c8"
+                          "docdash"
+                          "eslint"
+                          "eslint-plugin-jsdoc"
+                          "jsdoc"
+                          "tap-diff"
+                          "tape"
+                          "tape-catch"))))))))
     (inputs (list node-minimist node-pbf node-yazl))
     (home-page "https://github.com/ahwayakchih/crx3")
     (synopsis "Create CRXv3 browser extensions with Javascript")
@@ -325,18 +303,19 @@ (define-public node-debug
        (modify-phases %standard-phases
          (add-after 'patch-dependencies 'delete-dependencies
            (lambda args
-             (delete-dependencies `("brfs"
-                                    "browserify"
-                                    "coveralls"
-                                    "istanbul"
-                                    "karma"
-                                    "karma-browserify"
-                                    "karma-chrome-launcher"
-                                    "karma-mocha"
-                                    "mocha"
-                                    "mocha-lcov-reporter"
-                                    "xo"
-                                    "supports-color")))))
+             (modify-json (delete-dependencies
+               `("brfs"
+                 "browserify"
+                 "coveralls"
+                 "istanbul"
+                 "karma"
+                 "karma-browserify"
+                 "karma-chrome-launcher"
+                 "karma-mocha"
+                 "mocha"
+                 "mocha-lcov-reporter"
+                 "xo"
+                 "supports-color"))))))
        #:tests? #f))
     (home-page "https://github.com/debug-js/debug")
     (synopsis "Debugging utility for Node.js")
@@ -421,21 +400,22 @@ (define-public node-file-uri-to-path
        (modify-phases %standard-phases
          (add-after 'patch-dependencies 'delete-dependencies
            (lambda args
-             (delete-dependencies `("@types/mocha"
-                                    "@types/node"
-                                    "@typescript-eslint/eslint-plugin"
-                                    "@typescript-eslint/parser"
-                                    "cpy-cli"
-                                    "eslint"
-                                    "eslint-config-airbnb"
-                                    "eslint-config-prettier"
-                                    "eslint-import-resolver-typescript"
-                                    "eslint-plugin-import"
-                                    "eslint-plugin-jsx-a11y"
-                                    "eslint-plugin-react"
-                                    "mocha"
-                                    "rimraf"
-                                    "typescript"))))
+             (modify-json (delete-dependencies
+               `("@types/mocha"
+                 "@types/node"
+                 "@typescript-eslint/eslint-plugin"
+                 "@typescript-eslint/parser"
+                 "cpy-cli"
+                 "eslint"
+                 "eslint-config-airbnb"
+                 "eslint-config-prettier"
+                 "eslint-import-resolver-typescript"
+                 "eslint-plugin-import"
+                 "eslint-plugin-jsx-a11y"
+                 "eslint-plugin-react"
+                 "mocha"
+                 "rimraf"
+                 "typescript")))))
          (replace 'build
            (lambda* (#:key inputs native-inputs #:allow-other-keys)
              (copy-recursively "src" "dist")
@@ -497,7 +477,8 @@ (define-public node-ieee754
        #:phases (modify-phases %standard-phases
                   (add-after 'patch-dependencies 'delete-dependencies
                     (lambda _
-                      (delete-dependencies '("airtap" "standard" "tape")))))))
+                      (modify-json (delete-dependencies
+                        '("airtap" "standard" "tape"))))))))
     (home-page "https://github.com/feross/ieee754")
     (synopsis "Read/write IEEE754 floating point numbers in Javascript")
     (description "This package can read and write IEEE754 floating point
@@ -524,7 +505,7 @@ (define-public node-inherits
        (modify-phases %standard-phases
          (add-after 'patch-dependencies 'delete-dependencies
            (lambda args
-             (delete-dependencies '("tap")))))
+             (modify-json (delete-dependencies '("tap"))))))
        ;; FIXME: Tests depend on node-tap
        #:tests? #f))
     (home-page "https://github.com/isaacs/inherits")
@@ -553,8 +534,8 @@ (define-public node-irc
        (modify-phases %standard-phases
          (add-after 'patch-dependencies 'delete-dependencies
            (lambda args
-             (delete-dependencies
-              `("ansi-color" "faucet" "jscs" "tape")))))
+             (modify-json (delete-dependencies
+              `("ansi-color" "faucet" "jscs" "tape"))))))
        #:tests? #f))
     (inputs
      (list node-irc-colors))
@@ -583,7 +564,7 @@ (define-public node-irc-colors
        (modify-phases %standard-phases
          (add-after 'patch-dependencies 'delete-dependencies
            (lambda args
-             (delete-dependencies `("istanbul" "vows")))))
+             (modify-json (delete-dependencies `("istanbul" "vows"))))))
        #:tests? #f))
     (home-page "https://github.com/fent/irc-colors.js")
     (synopsis "Node.js module providing color and formatting for IRC")
@@ -657,7 +638,8 @@ (define-public node-minimist
        #:phases (modify-phases %standard-phases
                   (add-after 'patch-dependencies 'delete-dependencies
                     (lambda _
-                      (delete-dependencies '("covert" "tap" "tape")))))))
+                      (modify-json (delete-dependencies
+                        '("covert" "tap" "tape"))))))))
     (home-page "https://github.com/substack/minimist")
     (synopsis "Parse CLI arguments in Javascript")
     (description "This package can scan for CLI flags and arguments in
@@ -683,12 +665,13 @@ (define-public node-ms
        (modify-phases %standard-phases
          (add-after 'patch-dependencies 'delete-dependencies
            (lambda args
-             (delete-dependencies `("eslint"
-                                    "expect.js"
-                                    "husky"
-                                    "lint-staged"
-                                    "mocha"
-                                    "prettier")))))
+             (modify-json (delete-dependencies
+               `("eslint"
+                 "expect.js"
+                 "husky"
+                 "lint-staged"
+                 "mocha"
+                 "prettier"))))))
        #:tests? #f))
     (home-page "https://github.com/vercel/ms")
     (synopsis "Convert time to milliseconds")
@@ -716,14 +699,14 @@ (define-public node-nan
        (modify-phases %standard-phases
          (add-after 'patch-dependencies 'delete-dependencies
            (lambda args
-             (delete-dependencies
+             (modify-json (delete-dependencies
               '("bindings"
                 "commander"
                 "glob"
                 "request"
                 "node-gyp" ;; would be needed for tests
                 "tap"
-                "xtend")))))
+                "xtend"))))))
        ;; tests need tap and other dependencies
        #:tests? #f))
     (inputs
@@ -755,7 +738,8 @@ (define-public node-normalize-path
        #:phases (modify-phases %standard-phases
                   (add-after 'patch-dependencies 'delete-dependencies
                     (lambda _
-                      (delete-dependencies '("gulp-format-md" "mocha")))))))
+                      (modify-json (delete-dependencies
+                        '("gulp-format-md" "mocha"))))))))
     (native-inputs (list node-minimist))
     (home-page "https://github.com/jonschlinkert/normalize-path")
     (synopsis "Normalize slashes in a file path")
@@ -784,7 +768,7 @@ (define-public node-once
        (modify-phases %standard-phases
          (add-after 'patch-dependencies 'delete-dependencies
            (lambda args
-             (delete-dependencies '("tap")))))
+             (modify-json (delete-dependencies '("tap"))))))
        ;; FIXME: Tests depend on node-tap
        #:tests? #f))
     (inputs
@@ -841,7 +825,8 @@ (define-public node-path-key
        #:phases (modify-phases %standard-phases
                   (add-after 'patch-dependencies 'delete-dependencies
                     (lambda _
-                      (delete-dependencies '("@types/node" "ava" "tsd" "xo")))))))
+                      (modify-json (delete-dependencies
+                        '("@types/node" "ava" "tsd" "xo"))))))))
     (home-page "https://github.com/sindresorhus/path-key")
     (synopsis "Cross-platform utility to compute the PATH environment variable key")
     (description "@code{path-key} provides an implementation to compute the
@@ -867,7 +852,7 @@ (define-public node-pbf
        #:phases (modify-phases %standard-phases
                   (add-after 'patch-dependencies 'delete-dependencies
                     (lambda _
-                      (delete-dependencies
+                      (modify-json (delete-dependencies
                        '("benchmark"
                          "browserify"
                          "eslint"
@@ -877,7 +862,7 @@ (define-public node-pbf
                          "protocol-buffers"
                          "tap"
                          "tile-stats-runner"
-                         "uglify-js")))))))
+                         "uglify-js"))))))))
     (inputs (list node-ieee754 node-resolve-protobuf-schema))
     (home-page "https://github.com/mapbox/pbf")
     (synopsis "Decode and encode protocol buffers in Javascript")
@@ -908,7 +893,8 @@ (define-public node-protocol-buffers-schema
        #:phases (modify-phases %standard-phases
                   (add-after 'patch-dependencies 'delete-dependencies
                     (lambda _
-                      (delete-dependencies '("standard" "tape")))))))
+                      (modify-json (delete-dependencies
+                        '("standard" "tape"))))))))
     (home-page "https://github.com/mafintosh/protocol-buffers-schema")
     (synopsis "Protocol buffers schema parser written in Javascript")
     (description "This package provides a protocol buffers schema parser
@@ -935,26 +921,27 @@ (define-public node-readable-stream
        (modify-phases %standard-phases
          (add-after 'patch-dependencies 'delete-dependencies
            (lambda args
-             (delete-dependencies `("@babel/cli"
-                                    "@babel/core"
-                                    "@babel/polyfill"
-                                    "@babel/preset-env"
-                                    "airtap"
-                                    "assert"
-                                    "bl"
-                                    "deep-strict-equal"
-                                    "events.once"
-                                    "glob"
-                                    "gunzip-maybe"
-                                    "hyperquest"
-                                    "lolex"
-                                    "nyc"
-                                    "pump"
-                                    "rimraf"
-                                    "tap"
-                                    "tape"
-                                    "tar-fs"
-                                    "util-promisify")))))
+             (modify-json (delete-dependencies
+               `("@babel/cli"
+                 "@babel/core"
+                 "@babel/polyfill"
+                 "@babel/preset-env"
+                 "airtap"
+                 "assert"
+                 "bl"
+                 "deep-strict-equal"
+                 "events.once"
+                 "glob"
+                 "gunzip-maybe"
+                 "hyperquest"
+                 "lolex"
+                 "nyc"
+                 "pump"
+                 "rimraf"
+                 "tap"
+                 "tape"
+                 "tar-fs"
+                 "util-promisify"))))))
        #:tests? #f))
     (inputs (list node-util-deprecate node-string-decoder node-inherits))
     (home-page "https://github.com/nodejs/readable-stream")
@@ -983,7 +970,8 @@ (define-public node-resolve-protobuf-schema
        #:phases (modify-phases %standard-phases
                   (add-after 'patch-dependencies 'delete-dependencies
                     (lambda _
-                      (delete-dependencies '("standard" "tape")))))))
+                      (modify-json (delete-dependencies
+                        '("standard" "tape"))))))))
     (inputs (list node-protocol-buffers-schema))
     (home-page "https://github.com/mafintosh/resolve-protobuf-schema")
     (synopsis "Resolve protobuf imports")
@@ -1012,7 +1000,7 @@ (define-public node-safe-buffer
        (modify-phases %standard-phases
          (add-after 'patch-dependencies 'delete-dependencies
            (lambda args
-             (delete-dependencies '("tape" "standard")))))
+             (modify-json (delete-dependencies '("tape" "standard"))))))
        #:tests? #f))
     (home-page "https://github.com/feross/safe-buffer")
     (synopsis "Buffer creation with explicit semantics")
@@ -1040,20 +1028,21 @@ (define-public node-safe-stable-stringify
        #:phases (modify-phases %standard-phases
                   (add-after 'patch-dependencies 'delete-dependencies
                     (lambda _
-                      (delete-dependencies '("benchmark" "clone"
-                                             "fast-json-stable-stringify"
-                                             "fast-safe-stringify"
-                                             "fast-stable-stringify"
-                                             "faster-stable-stringify"
-                                             "fastest-stable-stringify"
-                                             "json-stable-stringify"
-                                             "json-stringify-deterministic"
-                                             "json-stringify-safe"
-                                             "standard"
-                                             "tap"
-                                             "typescript"
-                                             "@types/node"
-                                             "@types/json-stable-stringify")))))))
+                      (modify-json (delete-dependencies
+                        '("benchmark" "clone"
+                          "fast-json-stable-stringify"
+                          "fast-safe-stringify"
+                          "fast-stable-stringify"
+                          "faster-stable-stringify"
+                          "fastest-stable-stringify"
+                          "json-stable-stringify"
+                          "json-stringify-deterministic"
+                          "json-stringify-safe"
+                          "standard"
+                          "tap"
+                          "typescript"
+                          "@types/node"
+                          "@types/json-stable-stringify"))))))))
     (home-page "https://github.com/BridgeAR/safe-stable-stringify")
     (synopsis "Serialization of javascript objects")
     (description
@@ -1111,7 +1100,7 @@ (define-public node-semver
        (modify-phases %standard-phases
          (add-after 'patch-dependencies 'delete-dependencies
            (lambda args
-             (delete-dependencies '("tap")))))
+             (modify-json (delete-dependencies '("tap"))))))
        ;; FIXME: Tests depend on node-tap
        #:tests? #f))
     (home-page "https://github.com/npm/node-semver")
@@ -1151,7 +1140,8 @@ (define-public node-serialport
        (modify-phases %standard-phases
          (add-after 'patch-dependencies 'delete-dependencies
            (lambda args
-             (delete-dependencies `("@serialport/binding-mock"))))
+             (modify-json (delete-dependencies
+               `("@serialport/binding-mock")))))
          (add-after 'unpack 'chdir
            (lambda args
              (chdir "packages/serialport"))))
@@ -1210,25 +1200,10 @@ (define-public node-serialport-bindings
              (chdir "packages/bindings")))
          (add-after 'patch-dependencies 'delete-dependencies
            (lambda args
-             (delete-dependencies `("prebuild-install"
-                                    ;; devDependencies
-                                    "@serialport/binding-mock"
-                                    "node-abi"))))
-         (add-after 'chdir 'avoid-prebuild-install
-           (lambda args
-             (with-atomic-json-file-replacement (lambda (pkg-meta-alist)
-               (map
-                 (match-lambda
-                   (("scripts" . scripts-alist)
-                     (cons "scripts" (filter
-                       (match-lambda
-                         (("install" . _) #f)
-                         (_ #t))
-                       scripts-alist)))
-                   (("gypfile" . _)
-                     (cons "gypfile" #f))
-                   (other other))
-                 pkg-meta-alist))))))
+             (modify-json
+               (delete-dependencies `(
+                 ;; devDependencies
+                 "node-abi"))))))
        #:tests? #f))
     (synopsis "Abstract base class for Node SerialPort bindings")
     (description "Node SerialPort is a modular suite of Node.js packages for
@@ -1409,8 +1384,9 @@ (define-public node-serialport-stream
        (modify-phases %standard-phases
          (add-after 'patch-dependencies 'delete-dependencies
            (lambda args
-             (delete-dependencies `(;; devDependencies
-                                    "@serialport/binding-mock"))))
+             (modify-json
+               (delete-dependencies ;; devDependencies
+                 `("@serialport/binding-mock")))))
          (add-after 'unpack 'chdir
            (lambda args
              (chdir "packages/stream"))))
@@ -1461,7 +1437,7 @@ (define-public node-sqlite3
        (modify-phases %standard-phases
          (add-after 'patch-dependencies 'delete-dependencies
            (lambda args
-             (delete-dependencies
+             (modify-json (delete-dependencies
               `(;; Normally, this is "built" using @mapbox/node-pre-gyp,
                 ;; which publishes or downloads pre-built binaries or
                 ;; falls back to building from source.  Here, we patch out
@@ -1480,7 +1456,7 @@ (define-public node-sqlite3
                 "node-gyp"
                 ;; These we'd like, we just don't have them yet:
                 "eslint"
-                "mocha"))))
+                "mocha")))))
          (add-before 'configure 'npm-config-sqlite
            ;; We need this step even if we do replace @mapbox/node-pre-gyp
            ;; because the package expects to build its bundled sqlite
@@ -1514,8 +1490,9 @@ (define-public node-sqlite3
              (substitute* ".npmignore"
                (("lib/binding")
                 "#lib/binding # <- patched for Guix"))
-             (with-atomic-json-file-replacement (lambda (pkg-meta-alist)
-               (let ((binary-alist (assoc-ref pkg-meta-alist "binary")))
+             (modify-json
+               (lambda (pkg-meta-alist)
+                 (let ((binary-alist (assoc-ref pkg-meta-alist "binary")))
                  ;; When it builds from source, node-pre-gyp supplies
                  ;; module_name and module_path based on the entries under
                  ;; "binary" from "package.json", so this package's
@@ -1528,19 +1505,11 @@ (define-public node-sqlite3
                    (assoc-ref binary-alist "module_name")
                    " module_path="
                    (assoc-ref binary-alist "module_path"))))
+                 pkg-meta-alist)
                ;; We need to remove the install script from "package.json",
                ;; as it would try to use node-pre-gyp and would block the
                ;; automatic building performed by `npm install`.
-               (map
-                 (match-lambda
-                   (("scripts" . scripts-alist)
-                     (cons "scripts" (filter
-                       (match-lambda
-                         (("install" . _) #f)
-                         (_ #t))
-                       scripts-alist)))
-                   (other other))
-                 pkg-meta-alist))))))))
+               (delete-fields `(("scripts" "install")))))))))
     (home-page "https://github.com/mapbox/node-sqlite3")
     (synopsis "Node.js bindings for SQLite3")
     (description
@@ -1623,8 +1592,8 @@ (define-public node-string-decoder
        (modify-phases %standard-phases
          (add-after 'patch-dependencies 'delete-dependencies
            (lambda args
-             (delete-dependencies
-              '("tap" "core-util-is" "babel-polyfill")))))
+             (modify-json (delete-dependencies
+              '("tap" "core-util-is" "babel-polyfill"))))))
        ;; FIXME: Tests depend on node-tap
        #:tests? #f))
     (inputs (list node-safe-buffer node-inherits))
@@ -1678,7 +1647,7 @@ (define-public node-wrappy
        (modify-phases %standard-phases
          (add-after 'patch-dependencies 'delete-dependencies
            (lambda args
-             (delete-dependencies '("tap")))))))
+             (modify-json (delete-dependencies '("tap"))))))))
     (home-page "https://github.com/npm/wrappy")
     (synopsis "Callback wrapping utility")
     (description "@code{wrappy} is a utility for Node.js to wrap callbacks.")
@@ -1703,7 +1672,8 @@ (define-public node-yazl
        #:phases (modify-phases %standard-phases
                   (add-after 'patch-dependencies 'delete-dependencies
                     (lambda _
-                      (delete-dependencies '("airtap" "bl" "istanbul" "yauzl")))))))
+                      (modify-json (delete-dependencies
+                        '("airtap" "bl" "istanbul" "yauzl"))))))))
     (inputs (list node-buffer-crc32))
     (home-page "https://github.com/thejoshwolfe/yazl")
     (synopsis "Yet another zip library for node")
diff --git a/gnu/packages/node.scm b/gnu/packages/node.scm
index 20acffb3df..a13ec5d077 100644
--- a/gnu/packages/node.scm
+++ b/gnu/packages/node.scm
@@ -13,6 +13,7 @@
 ;;; Copyright © 2021, 2022 Philip McGrath <[email-masked]>
 ;;; Copyright © 2022 Hilton Chain <[email-masked]>
 ;;; Copyright © 2024 Efraim Flashner <[email-masked]>
+;;; Copyright © 2024 Daniel Khodabakhsh <[email-masked]>
 ;;;
 ;;; This file is part of GNU Guix.
 ;;;
@@ -366,7 +367,7 @@ (define-public node-semver-bootstrap
        (modify-phases %standard-phases
          (add-after 'patch-dependencies 'delete-dependencies
            (lambda args
-             (delete-dependencies '("tap")))))))
+             (modify-json (delete-dependencies '("tap"))))))))
     (home-page "https://github.com/npm/node-semver")
     (properties '((hidden? . #t)))
     (synopsis "Parses semantic versions strings")
@@ -397,11 +398,12 @@ (define-public node-ms-bootstrap
        (modify-phases %standard-phases
          (add-after 'patch-dependencies 'delete-dependencies
            (lambda args
-             (delete-dependencies '("eslint"
-                                    "expect.js"
-                                    "husky"
-                                    "lint-staged"
-                                    "mocha")))))))
+             (modify-json (delete-dependencies
+               '("eslint"
+                 "expect.js"
+                 "husky"
+                 "lint-staged"
+                 "mocha"))))))))
     (home-page "https://github.com/zeit/ms#readme")
     (properties '((hidden? . #t)))
     (synopsis "Tiny millisecond conversion utility")
@@ -431,7 +433,7 @@ (define-public node-binary-search-bootstrap
        (modify-phases %standard-phases
          (add-after 'patch-dependencies 'delete-dependencies
            (lambda args
-             (delete-dependencies `("chai" "mocha")))))))
+             (modify-json (delete-dependencies `("chai" "mocha"))))))))
     (home-page "https://github.com/darkskyapp/binary-search#readme")
     (properties '((hidden? . #t)))
     (synopsis "Tiny binary search function with comparators")
@@ -460,17 +462,18 @@ (define-public node-debug-bootstrap
        (modify-phases %standard-phases
          (add-after 'patch-dependencies 'delete-dependencies
            (lambda args
-             (delete-dependencies `("brfs"
-                                    "browserify"
-                                    "coveralls"
-                                    "istanbul"
-                                    "karma"
-                                    "karma-browserify"
-                                    "karma-chrome-launcher"
-                                    "karma-mocha"
-                                    "mocha"
-                                    "mocha-lcov-reporter"
-                                    "xo")))))))
+             (modify-json (delete-dependencies
+               `("brfs"
+                 "browserify"
+                 "coveralls"
+                 "istanbul"
+                 "karma"
+                 "karma-browserify"
+                 "karma-chrome-launcher"
+                 "karma-mocha"
+                 "mocha"
+                 "mocha-lcov-reporter"
+                 "xo"))))))))
     (inputs (list node-ms-bootstrap))
     (home-page "https://github.com/visionmedia/debug#readme")
     (properties '((hidden? . #t)))
@@ -526,12 +529,13 @@ (define-public node-llparse-builder-bootstrap
        (modify-phases %standard-phases
          (add-after 'patch-dependencies 'delete-dependencies
            (lambda _
-             (delete-dependencies `("@types/mocha"
-                                    "@types/node"
-                                    "mocha"
-                                    "ts-node"
-                                    "tslint"
-                                    "typescript"))))
+             (modify-json (delete-dependencies
+               `("@types/mocha"
+                 "@types/node"
+                 "mocha"
+                 "ts-node"
+                 "tslint"
+                 "typescript")))))
          (replace 'build
            (lambda* (#:key inputs #:allow-other-keys)
              (let ((esbuild (search-input-file inputs "/bin/esbuild")))
@@ -587,13 +591,14 @@ (define-public node-llparse-frontend-bootstrap
        (modify-phases %standard-phases
          (add-after 'patch-dependencies 'delete-dependencies
            (lambda args
-             (delete-dependencies `("@types/debug"
-                                    "@types/mocha"
-                                    "@types/node"
-                                    "mocha"
-                                    "ts-node"
-                                    "tslint"
-                                    "typescript"))))
+             (modify-json (delete-dependencies
+               `("@types/debug"
+                 "@types/mocha"
+                 "@types/node"
+                 "mocha"
+                 "ts-node"
+                 "tslint"
+                 "typescript")))))
          (replace 'build
            (lambda* (#:key inputs #:allow-other-keys)
              (let ((esbuild (search-input-file inputs "/bin/esbuild")))
@@ -648,15 +653,16 @@ (define-public node-llparse-bootstrap
        (modify-phases %standard-phases
          (add-after 'patch-dependencies 'delete-dependencies
            (lambda args
-             (delete-dependencies `("@types/debug"
-                                    "@types/mocha"
-                                    "@types/node"
-                                    "esm"
-                                    "llparse-test-fixture"
-                                    "mocha"
-                                    "ts-node"
-                                    "tslint"
-                                    "typescript"))))
+             (modify-json (delete-dependencies
+               `("@types/debug"
+                 "@types/mocha"
+                 "@types/node"
+                 "esm"
+                 "llparse-test-fixture"
+                 "mocha"
+                 "ts-node"
+                 "tslint"
+                 "typescript")))))
          (replace 'build
            (lambda* (#:key inputs #:allow-other-keys)
              (let ((esbuild (search-input-file inputs "/bin/esbuild")))
diff --git a/guix/build/node-build-system.scm b/guix/build/node-build-system.scm
index df7ea7774c..ea23d92a05 100644
--- a/guix/build/node-build-system.scm
+++ b/guix/build/node-build-system.scm
@@ -31,18 +31,14 @@ (define-module (guix build node-build-system)
   #:use-module (srfi srfi-1)
   #:use-module (srfi srfi-71)
   #:export (%standard-phases
-            with-atomic-json-file-replacement
             delete-dependencies
-            node-build))
-
-(define* (with-atomic-json-file-replacement proc
-  #:optional (file "package.json"))
-  "Like 'with-atomic-file-replacement', but PROC is called with a single
-argument---the result of parsing FILE's contents as json---and should a value
-to be written as json to the replacement FILE."
-  (with-atomic-file-replacement file
-    (lambda (in out)
-      (scm->json (proc (json->scm in)) out))))
+            delete-dev-dependencies
+            delete-fields
+            modify-json
+            modify-json-fields
+            node-build
+            replace-fields
+            with-atomic-json-file-replacement))
 
 (define* (assoc-ref* alist key #:optional default)
   "Like assoc-ref, but return DEFAULT instead of #f if no value exists."
@@ -72,6 +68,161 @@ (define* (alist-update alist key proc #:optional (= equal?))
       (acons key (proc (cdr pair)) rest)
       alist)))
 
+;;;
+;;; package.json modification procedures
+;;;
+
+(define* (with-atomic-json-file-replacement proc
+  #:optional (file "package.json"))
+  "Like 'with-atomic-file-replacement', but PROC is called with a single
+argument---the result of parsing FILE's contents as json---and should a value
+to be written as json to the replacement FILE."
+  (with-atomic-file-replacement file
+    (lambda (in out)
+      (scm->json (proc (json->scm in)) out))))
+
+(define* (modify-json #:key (file "package.json") #:rest all-arguments)
+  "Provide package.json modifying callbacks such as (delete-dependencies ...)"
+  (let
+    (
+      (modifications
+        (let loop ((arguments all-arguments))
+          (cond
+            ((null? arguments) '())
+            ((keyword? (car arguments)) (loop (cddr arguments)))
+            (else (cons (car arguments) (loop (cdr arguments))))))))
+    (with-atomic-json-file-replacement
+      (lambda (package)
+        (fold
+          (lambda (modification package)
+            (modification package))
+          package
+          modifications))
+      file)))
+
+(define (delete-dependencies dependencies-to-remove)
+  "Rewrite 'package.json' to allow the build to proceed without packages
+listed in 'dependencies-to-remove', a list of strings naming npm packages.
+
+To prevent the deleted dependencies from being reintroduced, use this function
+only after the 'patch-dependencies' phase."
+  (lambda (pkg-meta)
+    (fold
+      (lambda (dependency-key pkg-meta)
+        (alist-update
+          pkg-meta
+          dependency-key
+          (lambda (dependencies)
+            (remove
+              (lambda (dependency)
+                (member (car dependency) dependencies-to-remove))
+              dependencies))))
+      pkg-meta
+      (list
+        "devDependencies"
+        "dependencies"
+        "peerDependencies"
+        "optionalDependencies"))))
+
+(define* (modify-json-fields
+    fields
+    field-modifier
+    #:key
+      (field-path-mapper (lambda (field) field))
+      (insert? #f)
+      (strict? #t))
+  "Provides a lambda to supply to modify-json which modifies the specified
+ json file.
+- `fields` is a list procedure-specific data structures which should include
+ the definition of a `field-path` in one of two syntaxes: dot-syntax string
+ such as `\"devDependencies.esbuild\"`, or a list of strings such as
+ `(list \"devDependencies\" \"esbuild\")`.
+- `field-modifier` is a lambda which is invoked at the position of the field.
+ It is supplied with the current field definition, the association list (alist)
+ at the field location in the json file, and the field name, also called `key`.
+- `field-path-mapper` is a lambda which instructs where the field-path is
+ located within the field structure.
+- `insert?` allows the creation of the field and any missing intermediate
+ fields.
+- `strict?` causes an error to be thrown if the exact field-path is not found
+ in the data"
+  (lambda (package)
+    (fold
+      (lambda (field package)
+        (let*
+          (
+            (field-path (field-path-mapper field))
+            (
+              field-path
+              (cond
+                ((string? field-path)
+                  (string-split field-path #\.))
+                ((and (list? field-path) (every string? field-path))
+                  field-path)
+                (else
+                  (error
+                    (string-append
+                      "Invalid field value provided, expecting a string or a "
+                      "list of string but instead got: "
+                      (with-output-to-string (lambda _ (display field-path))))))
+              )))
+          (let loop
+            (
+              (data package)
+              (field-path field-path))
+            (let*
+              (
+                (key (car field-path))
+                (data
+                  (if (and (not (assoc key data)) insert?)
+                    (acons key '() data)
+                    data)))
+              (if (not (assoc key data))
+                (if strict?
+                  (error (string-append
+                    "Key '" key "' was not found in data: "
+                    (with-output-to-string (lambda _ (display data)))))
+                  data)
+                (if (= (length field-path) 1)
+                  (field-modifier field data key)
+                  (assoc-set!
+                    data
+                    key
+                    (loop (assoc-ref data key) (cdr field-path)))))))))
+      package
+      fields)))
+
+(define* (delete-fields fields #:key (strict? #t))
+  "Provides a lambda to supply to modify-json which deletes the specified
+ `fields` which is a list of field-paths as mentioned in `modify-json-fields`.
+ Examples:
+  (delete-fields '(
+    (\"path\" \"to\" \"field\")
+    \"path.to.other.field\"))"
+  (modify-json-fields
+    fields
+    (lambda (_ data key)
+      (assoc-remove! data key))
+    #:strict? strict?))
+
+(define* (replace-fields fields #:key (strict? #t))
+  "Provides a lambda to supply to modify-json which replaces the value of the
+ supplied field. `fields` is a list of pairs, where the first element is the
+ field-path and the second element is the value to replace the target with.
+ Examples:
+  (replace-fields '(
+    ((\"path\" \"to\" \"field\") \"new field value\")
+    (\"path.to.other.field\" \"new field value\")))"
+  (modify-json-fields
+    fields
+    (lambda (field data key)
+      (assoc-set! data key (cdr field)))
+    #:field-path-mapper (lambda (field) (car field))
+    #:strict? strict?))
+
+(define (delete-dev-dependencies)
+  (delete-fields (list "devDependencies") #:strict #f))
+
 ;;;
 ;;; Phases.
 ;;;
@@ -144,31 +295,6 @@ (define* (patch-dependencies #:key inputs #:allow-other-keys)
                   (assoc-ref* pkg-meta "dependencies" '())))))))))
   #t)
 
-(define (delete-dependencies dependencies-to-remove)
-  "Rewrite 'package.json' to allow the build to proceed without packages
-listed in 'dependencies-to-remove', a list of strings naming npm packages.
-
-To prevent the deleted dependencies from being reintroduced, use this function
-only after the 'patch-dependencies' phase."
-  (with-atomic-json-file-replacement
-    (lambda (pkg-meta)
-      (fold
-        (lambda (dependency-key pkg-meta)
-          (alist-update
-            pkg-meta
-            dependency-key
-            (lambda (dependencies)
-              (remove
-                (lambda (dependency)
-                  (member (car dependency) dependencies-to-remove))
-                dependencies))))
-        pkg-meta
-        (list
-          "devDependencies"
-          "dependencies"
-          "peerDependencies"
-          "optionalDependencies")))))
-
 (define* (delete-lockfiles #:key inputs #:allow-other-keys)
   "Delete 'package-lock.json', 'yarn.lock', and 'npm-shrinkwrap.json', if they
 exist."
-- 
2.46.0