Open kievechua opened 9 years ago
Wouldn't that be OK just to bind-to-http-resolution the nvm install
(and nvm ls-remote
), using package.json[engines] and somehow keep the nvm use
local (without HTTP call) with a clever algorithm processing semver-specified release in package.json against installed releases.
I mean, after all nvm install
will definately make some HTTP roundtrips, and "processing a semver against a list of installed versions" wouldn't be so hard, would it ?
@cyrilchapon See https://github.com/nvm-sh/nvm/issues/651#issuecomment-455380459
Would it be possible to do something like eslint-plugin-react does for React version - i.e. specify in the config file that you want to use the version specified in package.json?
// .eslintrc
{
"settings": {
"react": {
"version": "detect"
}
}
}
Perhaps something like this:
// .nvmrc
engines.node // or whatever the property is in package.json
That could also avoid the problem of semver ranges by simply not allowing it if you wish to use this feature, as this would make the feature opt in.
That could also avoid the problem of semver ranges
Or you could specify the range as well, kinda like browserlist config.
just to compare....N does this already....according to their docs
the idea of having to specify my node version in two places is a non-starter. No way that won't fall out of sync.
It would be really nice to add this feature:
.nvmrc
is present and use that version if the file exists (current behavior)engines.node
config in package.jsonSupporting strict version (i.e. "12.18.3"
) should be easy to implement and already a good start.
Alternatively, it's not necessary to use "engines" property. We can have our own "nvmrc" property in package.json.
The package.json
can have a special field called "nvmrc"
which would contain the NVM's .nvmrc
file contents. It's quite easy to parse package.json
with grep.
Here is how you retrieve package version (cross platform version of grep): grep -o '"version": "[^"]*' package.json | grep -o '[^"]*$'
So, NVM could check if .nvmrc
file presents OR "nvmrc": "lts"
value is present in package.json
.
Here is bash POC:
if test -f ".nvmrc"; then
NODE_VERSION = `cat .nvmrc || ""`
elif test -f "package.json"; then
NODE_VERSION = `grep -o '"nvmrc": "[^"]*' package.json | grep -o '[^"]*$' || echo ""`
else
NODE_VERSION = ""
fi
@koresar sure, an nvmrc
property in package.json wouldn't be a range, it'd just be the same as .nvmrc
, except then nvm would still have to have a JSON parser. Why is that better than using a .nvmrc
file?
.nvmrc
to package.json
? There are answers above in this thread. My personal use case - our deployment scripts do not copy dot-files for security reasons.@koresar many legit things live in dotfiles; that seems like a huge flaw in your deployment scripts. The majority of the answers upthread are about having a single source of truth - having two fields in the same file doesn't achieve that.
re your POC, that won't work unless there's a single space after "nvmrc":
, which isn't guaranteed, and it doesn't handle a bunch of other whitespace characters that are perfectly valid in JSON. Anything that touches JSON without being a proper JSON parser is automatically broken.
@ljharb good job spotting the "space problem" in my POC. You are good with RegEx. 👍
So you probably know that this
grep -o '"version":\s*"[^"]*' package.json | grep -o '[^"]*$' || echo ""
would work with any number of space or tab characters. With some more lines of code we could make this work for new-lines as well.
I find your reply deliberately misleading and unproductive. Somehow you are looking for the proofs to tell other people are wrong, rather than collaborate effectively.
So, again, we can safely parse out single string out of a JSON file using RegEx. Moreover, I believe that there is a 99.9999% chance that this grep would work as is. Which is high enough for production usage.
We already have two sources of truth - package.json and .nvmrc files. How come joining two sources into single source of truth doesn't achieve the requested feature?
And lastly, our deployment scripts "huge flaw" is kinda more difficult than you can imagine. There is more to that. Please, avoid basing your conclusions on my rather not informative sentence. I can' disclose more information at this point. Sorry.
Have a good day.
JSON isn't regular, so no, regular expressions simply aren't safe. I appreciate that it would work 99% of the time, but if it's not 100%, it's not safe enough for nvm.
Mate, not 99%. I said 99.9999%.
Please, avoid faking other people words. It's unproductive.
Thanks.
Good news everyone! There is a JSON parser written in pure shell script.
https://github.com/dominictarr/JSON.sh
Here is how I can extract any value from a JSON file:
cat package.json | JSON.sh -b | grep '\[\"version\"\]'
This means that NVM can 100% safely keep its version in the ["nvmrc"]
(root) value of the package.json
.
Who else still wants to keep the nvmrc
value inside the package.json? I do. :)
@koresar ok, 99.99999999999% is still not sufficient. It's 100% or nothing. I wasn't "faking" any words, I was approximating/paraphrasing, which is a normal thing humans do in conversation. Please don't be overly pedantic, it's unproductive. Thanks.
Unfortunately, at a quick glance that tool isn't compatible with ksh due to its use of local declarations on the same line as a variable initialization.
I agree that using a full JSON parser is the only safe approach, and that one's pretty close to what we'd need - but it's still not clear that the complexity and bytes weight is a good tradeoff for being able to have your two sources of truth in the same file.
This is still an issue and I think many would like a solution.
In an ideal world, you specify the version range in package.json engines and nvm would try to infer the best thing to do - use the top end of the range?
If that isn't feasible, or specifying a specific version is still required for a use case, then specifying a specific version in addition to the range is fine, but I think it would still be better to colocate it with the engines range so that they are next to each other which should be easier to keep in sync.
@theoephraim "try to infer" isn't something i'd consider ideal.
A package.json field that specifies a single nvm-style version-ish would certainly work, and could live alongside engines
, but then nvm would have to add a JSON parser just to avoid a separate file.
When building libraries, one may have a very wide range, so inferring may not really make sense, and throwing an error if a specific version is not specified would probably be best.
But usually for an internal project, you would have a specific version you're running in prod, and you'd like all your environments to match that. In this case, having NVM infer that you want to use 1.2.3 from a range string of "1.2.3" or "~1.2.3" seems quite reasonable, no?
I'd think that if some extra limitations were imposed on when this feature (using package.json engines if .nvmrc file doesnt exist), it may even be quite easy to set up. For example if we assume the package.json file is properly formatted and indented, we may not even need to parse the JSON - we could probably get the version range in engines using a simple regex.
So basically, if no nvmrc file is found, try to find a version from package.json engines making assumptions about it being formatted a certain way. If we find a range and it is tight enough that we can reasonably infer the user's intent, go ahead and use it. I'd assume most folks that want this feature would be willing to comply with whatever limitations are imposed in this case.
I understand the convenience factor there, yes. I'm not convinced that that minor convenience (having one source of truth in package.json instead of two in package.json and .nvmrc) is worth the complexity of parsing JSON.
We definitely can't assume the file is properly formatted, or that it's indented at all, let alone properly.
I definitely hear you - but... why not? This would be an optional feature - if you want to use it, it's required that you format your package.json file in the standard way. If nvm finds no nvmrc file and cant reasonably figure out what to do from your package.json file, an error is still thrown.
I've explained why not - because JSON parsing in posix would be a very complex and heavyweight thing to add to nvm.sh
for a relatively insignificant benefit. It's not reasonable to have an implicit requirement that your package.json be formatted in any particular way (there is no "the" standard way), and the risk of doing the wrong thing silently is high.
A package.json field that specifies a single nvm-style version-ish would certainly work, and could live alongside
engines
, but then nvm would have to add a JSON parser just to avoid a separate file.
Could it shell out to the current Node installation to use JSON.parse
?
@nickmccurdy no, because we can't assume any node version is installed - the first time you run nvm install
you'd want the version to be read from package.json.
I do not understand the arguments "complexity of parsing JSON" and "JSON parsing in posix would be a very complex". The complexity is near 0 since there is a robust JSON.sh solution.
I do not understand the argument that JSON.sh "isn't compatible with ksh". I know that NVM gurus would solve this easily.
I do not understand the argument that JSON.sh is "heavyweight". The current NVM is 4115 lines. The JSON.sh is 180 (if you remove CLI args handling). 4115 + 180 = 4295 lines. Not heavy to me.
Weight is measured in more than lines. Inlining JSON.sh is a massive maintenance burden for me to take on - it’s vendoring a dependency, with ksh patches, replicating all their tests, and if they ever make any updates, it’s figuring out how to upstream those.
I understand the implementation time cost.
JSON.sh had no updates in 5 years. Not sure what to maintain there. I foresee 0 maintenance effort there. So I don't understand the "maintenance burden" argument.
I think even if we create a new field in package.json looks cleaner than and actual .rc file with just one setting
@ljharb is it acceptable to add a grammar parser? I mean it shouldn't be hard to wirte a specific grammar parser to find "engines" and "node" keywords. If I, or someone else, would write it, how I can test it to meet the acceptance criteria?
For example, I got a quick&dirty version of parser in bash
, but struggle with sh
. As I see in this comment, the solution should work in bash
, dash
, ksh
, and zsh
. Is this list still exhaustive? What's about semver?
The list includes those 5, yes.
To safely parse json requires a full json parser, regardless of the property names being located.
About JSON parsers. I invite everyone to read this issue starting from the comment: https://github.com/nvm-sh/nvm/issues/651#issuecomment-701360709
In other words, there is a robust JSON.sh parser written in shell (180 lines of code), it was already discussed that even having great JSON parser there is no way NVM will start reading the node version from ./package.json
.
But, well, the proposed parser doesn't fulfill criteria of nvm. It just doesn't work on all target platforms.
This is something that came across my mind at work this week... Then I saw this issue, created Feb 2015 😬
I tried to come at this from a different angle:
Rather than getting nvm
to read package.json
, why not at least automatically create an .nvmrc
file based on package.json
so things can stay in sync & nvm
can use it?
I'm not sure if there's anything else that does that out there, maybe there is... it's quite simple... but here is my effort to solve one of the issues I think people are trying to solve here (that being, not having to declare the same thing in two different places) https://www.npmjs.com/package/package-node-to-nvmrc.
From my perspective, making it an opt-in feature (by not creating an .nvmrc file) that may have some harsh limitations in order to use it (like formatting your package.json file a certain way that would allow us to detect the version using a simple regex) would be a very reasonable compromise that could help many people. It could even log a big nasty warning whenever the feature is triggered saying something like:
Autodetected node version using package.json engines - using version "X.Y.Z"
If this was not what you expected, please follow the package.json formatting restrictions
listed <here> or just define an .nvmrc file
For a huge number of projects, it would just work as expected. In a few cases, it would try to run but be unable to detect a version and throw an error. In a very small set of cases, it may do something unexpected. A developer hitting those edge cases could either then try to adjust their package.json file until it does what they were expecting, or create an .nvmrc file.
@rubengmurray that's a great approach, except that it uses husky, which is a very dangerous thing (automatically installed git hooks, i mean, not just that specific tool)
@theoephraim it would have to only ever have false negatives - iow, it might be ok if it fails wrongly, but it's not ok if it installed the wrong version wrongly.
@ljharb Interesting... I'm not so experienced with git hooks to be honest, husky
just seemed an easy way to implement the hook. Why it is they're 'very dangerous'... Is there potential for clash / overwrite with existing .git/hooks/pre-commit
?
@rubengmurray it's a security hole. there's a reason git itself doesn't allow you to ship automatically-installed git hooks with a repo.
But, well, the proposed parser doesn't fulfill criteria of nvm. It just doesn't work on all target platforms.
I think we discussed that already. We came to conclusion that it would be quite easy for Shell gurus to adopt that parser to all (actually one) target platforms.
https://itnext.io/locking-the-node-js-version-in-your-projects-70268c877421
For now, perhaps this is the "best" answer? It allows us to define our specific (not minimum) version in node.engines, have (and get benefits of) .nvmrc, but also have explicit node defined (and switched to); all while maintaining a single point of truth in package.json.
If you mean autogenerating nvmrc, sure, that’s totally viable.
@ljharb Just some food for thoughts, what about making use of the fact that Yarn and NPM can both parse package.json
?
For example here is my current solution to enforce one node version in one place only:
package.json
{
"engines": {
"node": "16"
},
"scripts": {
"nv": "echo $npm_package_engines_node"
}
}
Use the version specified in package.json
:
nvm use $(yarn --ignore-engines -s nv)
or
nvm use $(npm run -s nv)
Those package vars i believe don't work anymore, and either way, nvm has to be able to work with no node versions installed.
Ok that's fair enough!
I've done this in the end, it doesn't cover all use cases but at least will cover the one where one forgets to update .nvmrc
package.json
{
"engines": {
"node": "16"
},
"scripts": {
"nv": "echo $npm_package_engines_node",
"postinstall": "yarn -s nv > .nvmrc"
}
}
Then just run nvm use
as usual.
I propose this in package.json
. I.e. avoid using existing "engines" property:
{
// ...
"nvm": "16"
// ...
}
@koresar sure, that would solve the "range" problem but would still have two problems: needing to parse JSON, and having two sources of truth (since engines.node
still exists)
package.json
is ignored but NVM atm)@ljharb I think I debunked the argument "problem of JSON parsing" two times in this issue. It is actually not a problem. Google: JSON.sh
The only actual argument I saw here is "implementation time cost". It is indeed a downside.
That is why it is a huge problem - not because it’s impossible, but because the complexity and byte size required to do it is prohibitive massive. 100 lines of dense code is WAY too much to casually add it to nvm.
If the issue were “debunked” then nvm would have the feature; it continues to be an issue, which is why this remains open.
A few months ago I adopted a convention I personally think is preferable to having this feature as a part of nvm
because it could be used on both Windows and Mac. I use nvm-windows
which is similar but different to this particular project, but the API is similar, so your mileage may vary.
nvm install node && nvm use node
npm
, like $npm_package_engines_node
npm install -g cross-env && cross-env-shell \"nvm install $npm_package_engines_node && nvm use $npm_package_engines_node\"
"scripts": {
"setup": "npm install -g cross-env && cross-env-shell \"nvm install $npm_package_engines_node && nvm use $npm_package_engines_node\""
},
npm run setup
npm install
To recap:
The first time, you can install whatever node version you want. Any other time, you only need to run npm run setup
. Kind of weird in CI environments which are built from scratch on each run, because you install the latest node and then immediately install a different node. But if you're OK with that, it just works.
I don't know if using npm to install npm will shatter dimensions, but it hasn't caused an issue for me yet.
As an alternative to coding this directly into nvm, you can add it to the shell script located in the README
NOTE: This does not handle semantic versions in the node engine property and requires that jq be installed.
# Calling nvm use automatically in a directory with a .nvmrc or package.json file
autoload -U add-zsh-hook
load-nvmrc() {
local node_version="$(nvm version)"
local nvmrc_path="$(nvm_find_nvmrc)"
# Check for .nvmrc
if [ -n "$nvmrc_path" ]; then
local nvmrc_node_version=$(nvm version "$(cat "${nvmrc_path}")")
if [ "$nvmrc_node_version" = "N/A" ]; then
nvm install
elif [ "$nvmrc_node_version" != "$node_version" ]; then
nvm use
fi
return
fi
# Check for node engine in package.json
if [ -f package.json ]; then
local pkg_node_version=$(jq -r .engines.node package.json)
if [ "$pkg_node_version" != "null" ]; then
local nvmrc_node_version=$(nvm version "$pkg_node_version")
if [ "$nvmrc_node_version" = "N/A" ]; then
nvm install "$pkg_node_version"
elif [ "$nvmrc_node_version" != "$node_version" ]; then
nvm use "$pkg_node_version"
fi
return
fi
fi
# Default nvm version
if [ "$node_version" != "$(nvm version default)" ]; then
echo "Reverting to nvm default version"
nvm use default
fi
}
add-zsh-hook chpwd load-nvmrc
load-nvmrc
Run
nvm use
will read local package.json's engines.node and change version accordingly. Or anyway that auto switch the version.