PackageJson

The missing gem for managing package.json files, without having to know about package managers (mostly).

It provides an interface for easily modifying the properties of package.json files, along with a "middle-level" abstraction over JavaScript package mangers to make it easy to manage dependencies without needing to know the specifics of the underlying package manager (and potentially without even knowing the manager itself!).

This is not meant to provide the exact same functionality and behaviour regardless of what package manager is being used, but rather make it easier to perform common general tasks that are supported by all package managers like adding new dependencies, installing existing ones, and running scripts without having to know the actual command a specific package manager requires for that action (and other such nuances).

Installation

Install the gem and add to the application's Gemfile by executing:

$ bundle add package_json

If bundler is not being used to manage dependencies, install the gem by executing:

$ gem install package_json

Usage

# represents $PWD/package.json, creating it if it does not exist
package_json = PackageJson.new

# adds eslint, eslint-plugin-prettier, and prettier as development dependencies
package_json.manager.add(%w[eslint prettier], :dev)

# adds the "lint" and "format" scripts, preserving any existing scripts
package_json.merge! do |pj|
  {
    "scripts" => pj.fetch("scripts", {}).merge({
      "lint" => "eslint . --ext js",
      "format" => "prettier --check ."
    })
  }
end

# deletes the "babel" property, if it exists
package_json.delete!("babel")

# runs the "lint" script with the "--fix" argument
package_json.manager.run("lint", ["--fix"])

The PackageJson class represents a package.json on disk within a directory; because it is expected that the package.json might be changed by external sources such as package managers, PackageJson reads and writes to and from the package.json as needed rather than representing it in memory.

If you expect the package.json to already exist, you can use read instead which will raise an error instead of implicitly creating the file if it doesn't exist.

A PackageJson also comes with a manager that can be used to manage dependencies and run scripts.

Specifying a package manager

You can specify which package manager should be used with the packageManager property in the package.json.

Note

Only the name of the package manager is used; the version (if present) is not checked, nor is codepack used to ensure that the package manager is installed.

The manager will be invoked by its name in the directory of the package.json, and it is up to the developer to ensure that results in the desired package manager actually running.

If the packageManager property is not present, then the fallback manager will be used; this defaults to the value of the PACKAGE_JSON_FALLBACK_MANAGER environment variable or otherwise npm. You can also provide a specific fallback manager:

PackageJson.read(fallback_manager: :pnpm)
PackageJson.new(fallback_manager: :yarn_classic)

Supported package managers are :npm, :yarn_berry, :yarn_classic, :pnpm, and :bun.

If the package.json does not exist, then the packageManager property will be included based on this value, but it will not be updated if the file already exists without the property.

Managers are provided a reference to the PackageJson when they're initialized, are run in the same directory as that PackageJson.

Using the package manager

Each package manager supports a set of common methods which are covered below. Unless otherwise noted for a particular method, each method:

  • Behaves like system, returning either true, false, or nil based on if the package manager exited with a non-zero error code; each method has a bang-equivalent if you wish an exception to be thrown instead
  • Does not attempt to capture or intercept the output; using Kernel.system under the hood, output is sent directly to stdout and stderr
  • Will run in the directory of the package.json; for methods that generate native commands, it is up to the caller to ensure the working directory is correct

Get the version of the package manager

package_json.manager.version

This is suitable for checking that the package manager is actually available before performing other operations. Unlike other non-bang methods, this will error if the underlying command exits with a non-zero code.

Installing dependencies

# install all dependencies
package_json.manager.install

# install all dependencies, erroring if the lockfile is outdated
package_json.manager.install(frozen: true)
Option Description
frozen Fail if the lockfile needs to be updated

Generating the install command for native scripts and advanced calls

# returns an array of strings that make up the desired operation
native_install_command = package_json.manager.native_install_command

# runs the command with extra environment variables
Kernel.system({ "HELLO" => "WORLD" }, *native_install_command)

append_to_file "bin/ci-run" do
  <<~CMD
    echo "* ******************************************************"
    echo "* Installing JS dependencies"
    echo "* ******************************************************"
    #{native_install_command.join(" ")}
  CMD
end
Option Description
frozen Fail if the lockfile needs to be updated

Adding dependencies

# adds axios as a production dependency
package_json.manager.add(["axios"])

# adds eslint and prettier as dev dependencies
package_json.manager.add(["eslint", "prettier"], type: :dev)

# adds dotenv-webpack v6 as a production dependency
package_json.manager.add(["dotenv-webpack@^6"])
Option Description
type The type to add the dependencies as; either :production (default), :dev, or :optional

Removing dependencies

# removes the axios package
package_json.manager.remove(["axios"])

Run a script

# runs the "test" script
package_json.manager.run("test")

# runs the "test" script, passing it "--coverage path/to/my/test.js" as the argument
package_json.manager.run("test", ["--coverage", "path/to/my/test.js"])

# runs the "lint" script, passing it "--fix" as the argument and telling the package manager to be silent
package_json.manager.run("lint", ["--fix"], silent: true)
Option Description
silent Suppress output from the package manager

Generating a run command for native scripts and advanced calls

native_run_command = package_json.manager.native_run_command("test", ["--coverage"])

# runs the command with extra environment variables
Kernel.system({ "HELLO" => "WORLD" }, *native_run_command)

append_to_file "bin/ci-run" do
  <<~CMD
    echo "* ******************************************************"
    echo "* Running JS tests"
    echo "* ******************************************************"
    #{native_run_command.join(" ")}
  CMD
end
Option Description
silent Suppress output from the package manager

Generating a exec command for native scripts and advanced calls

native_exec_command = package_json.manager.native_exec_command("webpack", ["serve"])

# runs the command with extra environment variables
Kernel.system({ "HELLO" => "WORLD" }, *native_exec_command)

append_to_file "bin/webpack-webpack" do
  <<~CMD
    echo "* ******************************************************"
    echo "* Serving assets via webpack
    echo "* ******************************************************"
    #{native_exec_command.join(" ")}
  CMD
end

Note

Since Yarn Classic doesn't provide a native exec command, yarn bin is used instead to identify where the package command should be within node_modules.

For other package managers, their native exec command is used with the flags necessary to enforce the package command is only executed if the package is installed locally.

Development

After checking out the repo, run bin/setup to install dependencies. Then, run rake spec to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.

To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and the created tag, and push the .gem file to rubygems.org.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/package_json. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the code of conduct.

License

The gem is available as open source under the terms of the MIT License.

Code of Conduct

Everyone interacting in the PackageJson project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.