Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Execution Environments #7

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 102 additions & 0 deletions text/0003-execution-environment.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# Execution Environments

## Summary

This RFC introduces settings for controlling what execution environment (Node.js, Bun, Deno) will be used for a package during:

* runnings its lifecycle scripts

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thing to note is that lifecycle scripts can technically choose to execute node, deno, and bun in the same command because they are shell scripts.

Copy link
Member Author

@zkochan zkochan Aug 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, you are right. So, should we support specifying all of them?

Copy link

@KSXGitHub KSXGitHub Aug 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should still be a primary runtime for installing and running CLI app.

Lifecycles OTOH can take advantage of executionEnv.nodeVersion, executionEnv.denoVersion, executionEnv.bunVersion.

I don't quite understand the building item, is it lifecycle?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The building part is when the package is installed as a dependency. If it has a "postinstall" script, it will be executed to build the package. Or if it has a binding.gyp file, then node-gyp will run to build the package (it can still run node under the hood).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lifecycles OTOH can take advantage of executionEnv.nodeVersion, executionEnv.denoVersion, executionEnv.bunVersion.

ok, so you suggest to keep the nodeVersion field, add [runtime]Version fields and a jsRuntime field. In that case, I guess jsRuntime will always be used, when the package is installed as a dependency (so the localOnly field is not needed).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

@zkochan zkochan Aug 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the referenced issue they have also pointed out that there is an existing field for specifying runtime environments: engines.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am fine with your suggestion to have nodeVersion, denoVersion, bunVersion. I am not sure about the rest of the suggestion though. Especially as having a cliRuntime should be optional, so automatically generating it doesn't makes sense.

Something like an object with setting could work too:

{
  "pnpm": {
    "executionEnv": {
      "nodeRuntime": {
        "version": "20.16.0",
        "cli": true
      }
    }
  }
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am fine with your suggestion to have nodeVersion, denoVersion, bunVersion. I am not sure about the rest of the suggestion though. Especially as having a cliRuntime should be optional, so automatically generating it doesn't makes sense.

Something like an object with setting could work too:

{
  "pnpm": {
    "executionEnv": {
      "nodeRuntime": {
        "version": "20.16.0",
        "cli": true
      }
    }
  }
}

I'm not sure about adding another nesting level. Besides that, this also creates an invalid state where nodeRuntime.cli, denoRuntime.cli, and bunRuntime.cli are all defined, compared to cliRuntime which doesn't have invalid state.

Especially as having a cliRuntime should be optional, so automatically generating it doesn't makes sense.

We can improve it a bit: cliRuntime is only required when the package define bin and there are more than 1 {x}Version.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, but even when there is a bin field, this is optional. The nesting can be not required. Like in the dependencies in Cargo.toml. For instance:

{
  "pnpm": {
    "executionEnv": {
      "runtime": {
        "deno": "1.1.0",
        "node": {
          "version": "20.16.0",
          "cli": true
        }
    }
  }
}

* building
* running it as a CLI app

## Motivation

Running multiple versions of Node.js on the same computer isn't easy. Also, there is currently no way for a package to tell the package manager that it needs to be executed with a specific version of Node.js. Node.js versions should be locked the same way as other dependencies of projects are locked for reproducibility.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is already a way: engines

https://docs.npmjs.com/cli/v10/configuring-npm/package-json#engines

Perhaps this RFC could explain why engines is not sufficient and why we need a new feature.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As you can see in the alternatives section:

Instead of introducing a new field, we could use the engines field for detecting what Node.js version should be used for running the bin file or building the package. However, the engines field is already used by other package managers and it is usually just sets a range with the lowest supported Node.js version. If we will use it for specifying exact versions, installations of the package with other package managers will fail, when engine-strict is set to true.


## Detailed Explanation

We will support a new field in `package.json`: `pnpm.executionEnv.js`. This field will be similar to the `packageManager` field introduced by Corepack but will feature `<js runtime>@<version>` instead. For example:
zkochan marked this conversation as resolved.
Show resolved Hide resolved

```json
{
"pnpm": {
"executionEnv": {
"js": "[email protected]"
}
}
}
```

or

```json
{
"pnpm": {
"executionEnv": {
"js": "[email protected]"
}
}
}
```

or

```json
{
"pnpm": {
"executionEnv": {
"js": "[email protected]"
}
}
}
```

When pnpm sees this setting, it will load the specified runtime and use it for:

* running scripts locally (via the `pnpm run` and `pnpm exec` command)
* running build scripts, when installed as a dependency. If the package has a postinstall script, it will be executed by the specified runtime.
* running the package, when it is executed as a CLI.

If we don't want to control the execution env of the published package, set the optional `localOnly` setting to `true`. For instance:

```json
{
"name": "cowsay",
"version": "1.0.0",
"bin": "bin.js",
"pnpm": {
"executionEnv": {
"js": "[email protected]",
"localOnly": true
}
}
}
```

In this case, pnpm will remove the `executionEnv` setting from the `package.json` file on publish and the binary of the package will be executed with whatever runtime will be installed globally on the target machine.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't pnpm already remove the "pnpm" field from package.json before publishing a package?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It does remove the field currently. But this new subfield should not be removed.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Effectively, you are creating a pnpm-specific extension for the whole JS packages ecosystem.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Those packages will work with other package managers just fine. But with pnpm they will be more stable as the runtime will be locked.


Some environments might not want to allow pnpm to control the js runtime. For thes cases we need to support a setting that will instruct pnpm to ignore all the execution env settings: `ignore-execution-env=true`.

## Rationale and Alternatives

The alternative would be to use a third party tool for this (like Volta) but then we would have one more prerequisite for using pnpm.

## Implementation

The implementation can leverage the logic that is already present in pnpm for the `pnpm env` command, the `pnpm.executionEnv.nodeVersion` setting, the `use-node-version` setting.

Binding CLI apps to specific Node.js versions can be done via command shims. This currently works for globally installed packages. pnpm links globally installed packages to the active Node.js version.

## Prior Art

We already have some functionality for managing Node.js versions:

* the [pnpm env](https://pnpm.io/cli/env) command
* the [use-node-version](https://pnpm.io/npmrc#use-node-version) setting
* the [pnpm.executionEnv.nodeVersion](https://pnpm.io/package_json#pnpmexecutionenvnodeversion) setting.

## Unresolved Questions and Bikeshedding

{{Write about any arbitrary decisions that need to be made (syntax, colors, formatting, minor UX decisions), and any questions for the proposal that have not been answered.}}

{{THIS SECTION SHOULD BE REMOVED BEFORE RATIFICATION}}