Conversation
| 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. |
There was a problem hiding this comment.
Doesn't pnpm already remove the "pnpm" field from package.json before publishing a package?
There was a problem hiding this comment.
It does remove the field currently. But this new subfield should not be removed.
There was a problem hiding this comment.
Effectively, you are creating a pnpm-specific extension for the whole JS packages ecosystem.
There was a problem hiding this comment.
Those packages will work with other package managers just fine. But with pnpm they will be more stable as the runtime will be locked.
|
|
|
|
|
You should also mention how it interacts with |
|
Thank you about thinking on this 🙂
|
|
We should explore one more scenario. What if the consumer of the package wants to use a specific node.js version? They should be able to tell pnpm to run a specific dependency's postinstall script with a given node.js version and to run a specific CLI with the given node.js version. Also, how will we cleanup unused Node.js versions from the global cache? We currently have the pnpm env rm command for removing Node.js from the global cache and we don't check if that node.js version is used by anything. Also, if we will support other js runtimes, should we change the
Yes, I think we will replace it with this more powerful alternative.
I agree that it is confusing. Some settings are in package.json, some in |
|
|
||
| This RFC introduces settings for controlling what execution environment (Node.js, Bun, Deno) will be used for a package during: | ||
|
|
||
| * runnings its lifecycle scripts |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
yes, you are right. So, should we support specifying all of them?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
For reference, there is also this RFC: openjs-foundation/package-metadata-interoperability-working-group#15
There was a problem hiding this comment.
In the referenced issue they have also pointed out that there is an existing field for specifying runtime environments: engines.
There was a problem hiding this comment.
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
}
}
}
}There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
}
}
}
}Co-authored-by: Khải <[email protected]>
|
|
||
| ## 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. |
There was a problem hiding this comment.
There is already a way: engines
Perhaps this RFC could explain why engines is not sufficient and why we need a new feature.
There was a problem hiding this comment.
As you can see in the alternatives section:
Instead of introducing a new field, we could use the
enginesfield for detecting what Node.js version should be used for running the bin file or building the package. However, theenginesfield 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, whenengine-strictis set totrue.
There was a problem hiding this comment.
I missed that, thanks!
Though now that I think about it, maybe devEngines is more appropriate for this feature since you probably want to lock down development (like we do with a lockfile) to a specific node version. But once you publish, you want consumers to have more flexibility via engines semver range for the node version.
There was a problem hiding this comment.
For a CLI tool it is OK if we will lock down the prod version too. A CLI tool is executed separately from the app. I agree that if we are talking about libraries that you import/require to your app, then those should support a wide range of node.js versions.
There was a problem hiding this comment.
A CLI could use devEngines, right?
Unless you publish the CLI to npm registry because installing it would end up in node_modules and not respect devEngines 🤔
Though if you install it from the registry, you’ll probably execute the bin directly, not through pnpm, so not sure the correct version of node would be used anyway 🤔
There was a problem hiding this comment.
No, it can't use devEngines. devEngines is for local development. It isn't event returned in the abbreviated package metadata by the registry. engines is returned
There was a problem hiding this comment.
Though if you install it from the registry, you’ll probably execute the bin directly, not through pnpm, so not sure the correct version of node would be used anyway 🤔
pnpm creates a command shim and can include whichever node.js version is required. So, the command shim will point to the specific node.js version, which is needed.
|
So, in the openjs-foundation/package-metadata-interoperability-working-group#15 RFC it was suggested that we leverage the already existing "engines" field. Which I don't know if it is a good idea but I think we can do it for globally installed CLI tools. I would put an exact version to the pnpm CLI package's engines field for Node.js and it would make pnpm more stable, when installed with pnpm. The good thing about using the engines field is that it is already used by the ecosystem, so all the existing CLI tools would benefit from it. But all the tools use ranges currently, so the "stability" bonus wouldn't be so effective. Nevertheless, it could be a good starting point to start with "engines". |
|
👋 Hey there, first let me say that I love this RFC and the fact that At the company i'm working at we are already using Some background of my thinking
Actually I think the naming of this can be a bit misleading. Yes it is a JS runtime but browsers are also JS runtimes. And many npm packages are design to ultimately run in the browser. However 🙂 their dev lifecycle will happen through CLI tools for things like build, test, lint, So in this regard the issue @zkochan posted openjs-foundation/package-metadata-interoperability-working-group#15 suggest an interesting name: Bringing in a thread from a recent pnpm PRContinuing the same logic I want to also reply to a comment by @zkochan of a message I had in another PR pnpm/pnpm#8277, regarding lifecycles with
For me the answer is obvious: all dev-related processes of a package like build, postinstall, its The runtime for when the package is used should be controlled by whoever is using it, and the requirements can be defined (currently) in the The case of cli commands exposed by a packageFor the scripts defined in the Example setup{
"name": "packageAUsingNode14",
"engines": {
"node": "> 14"
},
"bin": {
"package-a-bin-cmd": "./myCliCmd.js"
},
"scripts": {
"package-a-bin-com:from-external": "cd $INIT_CWD && ./myCliCmd.js"
},
"pnpm": {
"executionEnv": {
"nodeVersion": "14.21.3"
}
}
}{
"name": "appBUsingNode18",
"pnpm": {
"executionEnv": {
"nodeVersion": "18.2.0"
}
},
"dependencies": {
"packageAUsingNode14": "workspace:^"
},
"scripts": {
"runPackageAUsingNode18": "package-a-bin-cmd",
"runPackageAUsingNode14": "pnpm --filter='packageAUsingNode14' run package-a-bin-com:from-external"
}
}
So by default You can see Suggestion for this RFCIn my mind i can see something like: {
"name": "my-package-name",
"engines": {
"node": ">18",
"bun": ">0.5"
},
"scripts": {
"postinstall": "... using bun 1.2.0...",
"preinstall": "... using bun 1.2.0...",
"any other script": "... using bun 1.2.0...",
"preany other script": "... using bun 1.2.0...",
"postany other script": "... using bun 1.2.0..."
},
"bin": {
"my-cli-cmd": "... using whatever devEngine the consumer package has by default...",
},
"pnpm": {
"devEngines": {
"bun": "1.2.0"
}
}
}
So this allows the package to live in its own isolated world when developed or when its scripts are run but also allow to define its consumer-restrictions using the |
|
PS: Our implementation is a bit different from the examples I've given above regarding CLI commands since we have a similar case but not exactly that one: we execute So instead |
|
As you can see in this post from past core npm maintainer, he suggests to not allow build scripts to pick the runtime. |
|
@zkochan I'm not sure I fully understand the comment. It's referring to |
|
he says they consider postinstall scripts a 'legacy feature' and don't want us to make it easier using it. |
|
@zkochan ok but what does that mean for all existing packages and their older versions that will not just disappear? I thought the whole idea of making isolated packages have their own processing environment is to allow gradual isolated changes and not force upgrades of everything to the new standard of having dependencies that don't use post/pre install scripts |
|
I want to add this feature to pnpm v10 because I want it to be able to install pnpm v11 with the specified node.js version. That would allow us to use v8 for serializing data in the cache: pnpm/pnpm#9965. One thing that I didn't consider, when writing the RFC was that any new field that we will add will not be included in the abbreviated version of the metadata unless the npm team will update the registry. As a result, using the "engines" field sounds like a good idea. If we are going to use the "engines" field, I see two problems:
Edit: Looks like you can put anything to the "engines" field and it will be returned by the abbreviated metadata. So, we can put something like {
"engines": {
"node": "20.0.0",
"nodeInstall": true
}
}Tested via publishing I guess we could just use the same format as in devEngine: {
"engines": {
"runtime": {
"name": "node",
"version": "^24.4.0",
"onFail": "download"
}
}
} |
|
The proposed solution with engines.runtime was released in pnpm v10.21 |
TODO: