Hopefully the ecosystem as improved since then, but it was nearly impossible to get going.
Some packages had been changed and the version number overwritten with incompatible packages, and the conflicts were plenty.
If there was an option to guarantee versions could exist for X amount of years (maybe even months?) then that would greatly help the stability of projects.
It's OK. Not every language ecosystem is so busted that you can reliably expect a project not to work if someone isn't staring at it weekly and building it over and over again just in case. Now, it's always a risk, sure, no language anywhere is immune to the issue [1], but there's plenty of languages where you can encounter things from 5 years ago and your default presumption is that it's probably still working as well now as it did then. It may be wrong, but it's an OK default presumption.
[1]: Well... no language in common use anyhow. There's some really fringe stuff that uses what is basically content-based references for code dependencies, but I'm not aware of anything that I'd call "production quality" that even remotely looks like that, and is immune to someone just plain making an error with the semantic versioning or whatever.
If there's no issue tracker, you can YOLO and try it and see if it works, or you can look around at the code and see if it looks reasonable.
Even if there are unaddressed issues, you can always use it and fix it when it breaks. If it's reasonable enough, it's a good start anyway. And at least my assumption with open source is I'm going to be fixing it when it breaks, so lack of a pulse is better than churn.
Ideally, in adopting dependencies, you should be looking for a mature utility whose design was clear and implementation is complete.
If it's open source, you should be able to read and unserstand the code yourself, and you should make an earnest effort to do so, in case it has faults you wouldn't usually allow in your own code and in case you need to fork it at some point.
This lets you you build well-designed, stable, maintainable, clear things yourself.
The alternate, building your project on a random collection of "living" projects undergoing active development is how you banish yourself to perpetual maintenance, build failures and CVE warnings that have nothing to do with your work, surprise regressions when you update your referenced version (you are, at least, pinning your versions??), etc
But I would not assume that a HTTP client that has been untouched in 12 years supports SNI, for example, which means that actually it might be totally useless for a lot of modern sites (certainly Android did not support SNI 12 years ago).
Lol, my perspective is almost the opposite. If it's got a lot of commits in the last six weeks, I need to look for something that's stable. Unless there's a good reason for so many commits; I feel like that many commits means it's in active development, which implies the requirements and interfaces aren't yet determined and who wants to rely on that?
I know of one application by a large multinational that requires java in the browser to run. Almost impossible to run now because of security restrictions.
Of course when they stop working they will be phased out, but we have been expecting their death for years now and not happening yet.
Since it's just a binary though, I wound up grabbing the OpenSSL from the old box and patching the binary to just point to that instead. Thing runs fine after that.
This is all, of course, still totally stupid - but I did find myself thinking how much worse comparable events in JS have been for me over the years. What would have been easily an entire afternoon ended up taking 15 minutes - and a chunk of that was just double checking commands I'd long forgotten.
``` ldd your-binary ``` on the old host and then copy all the thing that is referenced, put into ./foo and then start like so on new host: `LD_LIBRARY_PATH=./foo ./your-binary`. (may include typos, from memory)
A great tool for this used to be https://github.com/intoli/exodus - not sure if it still works.
Disclaimer: Also please don't do this with network-facing services, security applies, etc.pp. but it's a good trick to know.
Also, if it was statically linked, he wouldn't have that one problem. (Could have others, but not that one.)
I'm stuck with a defunct Nix project I can't update, because crane and fenix flakes made breaking changes, and nix is giving me incomprehensible errors. I've spent enough time googling the errors that I'd be quicker to start over with a nix-less VM.
This has the unfortunate side-effect that while downstream apps might still build, the library itself (and any examples in the library repo) may not compile after some time...
Go is much, much better on these terms, although not perfect.
I’d venture a guess that Perl 5 is outstanding here, although it’s been a few years since I tried to run an old Perl project. CPAN was dog slow, but other than that, everything worked first try.
I’d also bet Tcl is nearly perfect on the ‘try this 10 year old repo’ test
A few weeks ago I was experimenting with a sound generation dsl/runtime called Csound and even most 30yo sources were working as long as they didn't use some obsolete UI.
It also helps that if some library dependency generated Go code using a tool, the Go source code is checked in and you don’t have to run their tool.
What's been interesting is watching these devs age 10 years, and still mostly decide it's better to start new frameworks rather than treat legacy code as an asset. That feels to me like a generational shift. And I'm not shaking my cane and saying they're wrong -- a modern LLM can parse an API document and get you 95% of the way to your goal most of the time pretty quickly -- but I propose it's truly a cultural difference, and I suspect it won't wash out as people age, just create different benefits and costs.
It's just no guarantee that those old versions work on the new system, or with the outside world as it exists by time of installation - which can be as true for Go as any other language. If the XYZ service API client still gets you version 1.2.37, that's not actually any help if 1.2.37 calls endpoints that the XYZ service has removed. Or a cgo package that binds to a version of OpenSSL that is no longer installed on your system, etc.
I used to think that people emailing screenshots of corporate dashboards were idiots. I now think that's actually genius - a frozen in time view which you can't regenerate but will be available until the end of time if you need it. (Hello, Exchange admins!)
On the other hand, the other extreme induces stuff like autoconf which is not that great either. Trying to have your code be compatible with absolutely everything is probably not good, although arguably platforms these days are generally much more stable and consistent than they were in the heydays of autoconf.
Previously: You wouldn't conflate Windows development with "C" (and completely discount UNIX along the way) just because of Win32. <https://news.ycombinator.com/item?id=41899671>
CL is still one of the nicest languages there is, and the only language that skirts the line between being some combination of dynamic and interpreted yet typed and compiled.
It is showing its age though, particularly around the edges like what you're saying.
The other problem is rate of updates, and that's a symptom of it basically being on one person's shoulders to keep it ticking over. I can't readily think of another major language ecosystem with that characteristic. It just seems really fragmented.
Edit to add: this wasn't intended as a gotcha question, so apologies if it came across as one. I have issues with a lot of details about how npm works and the ecosystem it supports. I think it's possible to avoid them, and aim for something more like a bundler or a cargo, but again there are issues there (certainly in the former's case, I have less experience of the latter). Getting to a good answer that works for CL means understanding both the problem space and the known solutions elsewhere.
It might be that "a better quicklisp" is enough?
Let me start with facts:
- npm actually downloads multiple copies of each library, when needed to satisfy conflicting version requirements. - this is only possible due toruntime features of JavaScript. In most languages like C this causes symbol collisions. - I think this is a problem in Common Lisp too due to packages being global. Maybe there is a fancy way to rebuild packages at load time. - this is why the Debian style release makes sense. Either everything loads together, or not.
Opinions - I want to know all my dependencies. I treat them as my own source, so tar downloaded is close to my mental model. - For c projects I usually have a make file with curl commands tied to exactly url. If I want to update I manually change the url. - quicklisp already has a nice way to make an isolated folder just containing your code and its dependencies to be loaded with asdf. It gets out of the way once you have downloaded your libraries.
That one design attribute of npm probably more any other feels like they did it because they could, not because it was a particularly good idea.
App::cpanminus (cpanm) is noticeably lighter, App::cpm (cpm) does parallel builds and skips tests by default.
An approach I've become quite fond of is using cpm to install fast into the local::lib I'm actually going to use, then creating a scratch setup in /home/tmp or similar and running cpanm in that under tmux/abduco/etc. to do a second install that *does* run the tests so I have those results to refer to later but don't have to wait for them right now.
(if I ever write a cpan client of my own, it's going to have a mode where it does a cpm-like install process and then backgrounds a test running process that logs somewhere well known so this approach becomes a single command, but I keep getting distracted by other projects ;)
To be fair, Python and Ruby also have this problem (for newer Pythons, popular extension modules at recent versions are more likely to Just Work due to wheels, but if you're building old code for the first time in 3+ years, all the old problems come back with a vengeance). It's more of a "scripting language that got popular enough that ordinary projects have a deep tree of transitives, many of which are compiled on-site" issue than a Perl specific problem.
https://medium.com/hackernoon/how-it-feels-to-learn-javascri... (beware the green background, I recommend reader mode.)
On the flip side, anything that uses vanilla JS without a build will most likely run just fine, probably till the end of human civilization.
There will also be someone playing Tetris, Doom and Final Fantasy VI on their neural interface, long after all modern games have been lost to time (and DRM).
Basically a lot of AI depends on a bunch of absurdly optimized numeric libraries writen in Fortran.
Fortran is well into the way at becoming a centenarian programing language at 74 years of age.
Zombie JavaScript will be reduced to being glue code and then not even that.
Despite 28 years of effort at optimization, JavaScript is outperformed by WebAssembly. There's not much coming back from that:
https://jordaneldredge.com/blog/speeding-up-winamps-music-vi...
https://www.amazon.science/blog/how-prime-video-updates-its-...
It doesn’t matter. What matters is what people put in their web pages.
With WebAssembly every language runs in the browser and runs better.
A lot of things that bring a lot of value to a lot of people are still much, much faster to build via the JS / TS ecosystem.
It absolutely makes sense that calculation-heavy workloads will be ported to WASM, but there's a lot more to building an app.
Like what? Visual UI designers? WebAssembly's got you covered: https://platform.uno/blog/uno-platform-studio-featuring-hot-...
Running Visual Basic in a C# application compiled to WebAssembly? Sure, why not: https://bandysc.github.io/AvaloniaVisualBasic6/
Better query languages than SQL could exist, but there's so much existing code and expertise out there that it's not worth the effort. Better backend languages than Java can & do exist but don't have the same enterprise popularity.
Developers, projects and companies have an immense incentives to target the most popular programming language.
JavaScript has entered its Walking Dead phase. It will gradually be displaced by all languages compiling to WebAssembly.
The English language similarly lost its position as the preeminent imperial language a long time ago, so too with Latin & Rome. It takes a long time for a popular language to die because everyone wants to speak what everybody else speaks.
It really isn't. Wasm is just a compilation target now.
I adore the idea of somewhere on the Star Trek Enterprise, underneath the isolinear chips, the EPS conduits, the warp containment field control mechanisms, somewhere, if you dig far enough, override enough, you can get an LCARS screen that is all black, with a small `$` in the upper left and a blinking text cursor, waiting for a command.
You corrected yourself, but it's worth emphasizing here: a _NodeJS_ project, you mean.
Unless you're using non-standard APIs, stuff written to run in the browser generally keeps working just as well as it did before, no matter whether it was written 2 years ago or 10.
j/k - I'm slowly removing all the Zepto code I have and it's usually a relatively quick search&replace.
The original dream for Node was that it would simply be a glue wrapper around libuv that allowed for easy packaging/sharing of modules written in C++. But everyone just started writing everything in JS, and the ecosystem ended up as a mish-mash of native/non-native. Ryan Dahl stated this was indeed his biggest mistake/regret with Node, thus we have Deno now.
Because the native written stuff breaks all the darn time and it creates cross-plat nightmares.
My stress levels are inversely proportional to how many native packages I have to try to get building within a project, be that project in Python, Java, or JS.
JS+Node runs on everything. Prepackaged C++ libraries always seem to be missing at least one target platform that I need!
But even a great build system doesn't help when old native libraries don't support newer hardware or OSs. At some point the high level -> native abstractions break and then builds break. :(
It's considered ... rude ... in most cases to write a module that needs to build against a native library without also having an Alien dist to handle making sure said library is there by the time it's trying to build against it.
Opinions on perl as a *language* ... vary, let us say ... but I wish people who didn't like writing perl would at least look at how our infrastructure works and steal more of the good parts to make dealing with their preferred language less painful.
For those reading this who don't know much about node - node-gyp is how you pull in native code libraries to Node projects, typically for performance reasons. You get the same sorts of build issues with it that you can get whenever you start having binary, or source, dependencies, and you need the entire toolchain to be "Just Right(tm)".
I run into this issue with older Node projects on ARM Mac machines (Still!), but I run into similar issues with Python projects as well. Heck some days I still find older versions of native libraries that don't have working ARM builds for MacOS!
Node used to have a lot more native modules, in newer code you typically don't see as much of that, and accordingly this is much less of an issue now days.
> I always try to remember to put the node version in my package.json
This 100x over!
I would prefer to remain blissfully ignorant, thank you!
The Node+Express backend ecosystem is also incredibly powerful. Node is light weight, the most naïve code can handle a thousand RPS on the cheapest of machines, and you can get an entire server up and running with CORS+Auth+JSON endpoints in just 5 or 6 lines of code, and none of that code has any DI magic or XML configuration files.
JS/TS is horrible for numeric stuff, but it is great for everything else.
> Two hours of my life gone...
Two hours of work after 4 years sounds ... perfectly acceptable?
And it would have run perfectly right away if the node version was specified, so a good learning, too
This feels like making a mountain out of a mole hill
I disagree. This is an easy problem to avoid with minimal due diligence, people just choose convenience and make unnecessary tradeoffs.
* Use the standard library (ironically not available for Node projects). It will be built with better backwards compatibility almost every time. What deprecations do occur will likely be VERY WELL documented with much quicker adaptions.
* Limit third party dependencies. Do you really need an ORM for your apps 40 sql queries? How long would it take you to scaffold it with GenerativeAI then make it production-worthy without the ORM? 1 hour? 5 hours? 20 hours?
* Pick technologies with better track records. Maybe don't use Beta software like Swift Data for your iOS App. Maybe choose golang for your API even though it'll take a little bit longer to build it.
There's a balance to be struck between LeftPad scenarios and "Now there are 37 competing libraries".
I'll acknowledge here that there seems to be a significant difference between Python projects and Node projects: in my experience, a small Python project has a handful of dependencies and maybe a dozen sub-dependencies, while a small Node project usually has a handful of dependencies and a few hundred sub-dependencies. That's where Python's "batteries included" motto does seem to help.
I think we're actually in agreement. My assertion is that for projects which want to avoid constant maintenance, particularly small projects, you can make architectural decisions some of which could significantly improve the maintenance outcome. Of course there are trade-offs to those, and if you make the wrong architectural decisions it can cause more harm than good.
Maybe I'm glib for calling it "easy" but for many leftpad scenarios it really is a "holy crap why did you think that was ok" scenario in my experience. Lets avoid those scenarios when we can.
Having a machine do codegen to map your queries to objects is still an ORM, except now it's nondeterministic and not updateable.
(mind you, I come from C# where you simply use LINQ+EF without worry, or occasionally Dapper for smaller cases)
???
Node.js has an extensive stdlib [1].
Contrast that with Python which practically requires a 3rd party HTTP client.
Or Java that doesn't even have JSON support.
It's absolutely because there's something wrong with Python, the package management, and also the type safety. JVM languages haven't had these problems for 20+ years.
The same problem in Python is much easier now because you can ask the uv resolver to limit itself to some earlier point in time.
You can do `uv pip install --editable . --exclude-newer=2022-01-01` and you will end up with a resolution from two years ago. Since uv can also install older python versions automatically you can easily bisect you to a newer point.
The author of the blog post is trying to run a static site generator. A static site generator doesn't need to be able to do anything that Node provides that can't be done with the World Wide Wruntime (which they're already going to use to verify the correctness of the SSG output). So use that runtime and tools that target it, not Node.
Does it, though? Node wasn't exactly new 4 years ago, and plenty of other languages would offer a better experience for even older code -- Java, C, C++ to name a few.
1.5 hours to get running again?
1?
In exchange for needing to run C? How many hours would it take to build a Node app equivalent in C, I wonder.
50% of Java developers are still regularly working in Java 8 [0], which is the same solution that the author could have arrived at immediately—when starting up an old project after 4 years, use the same version you ran last time, don't try to update before you even have the thing running.
> C, C++
Not my experience, but maybe it depends on your operating system and distro? In my experience sorting through the C libs and versions that you need to install on your system to build a new project can easily take a few hours.
I've been using Jekyll/Ruby since 2014 for my website, with a few custom plugins I wrote myself. And I've never really needed to do anything like this. It "just works".
My Go and C programs are the same: "just works". I have some that are close to a decade old.
I don't love how bundler works by the way; I think it should/could be a lot better in many different ways. Same for Jekyll. But once it works, it generally keeps working.
I don't think Jekyll (or Ruby) are a paragon of stability. I'm sure some stuff has broken over the years. It just seems to break a lot less than the JS/Node ecosystems.
There are countless gems, libraries or packages out there that make your life easier and development so much faster.
But software (in my experience) always lives longer than you expect it to, so you need to be sure that your dependencies will be maintained for that lifetime (or have enough time to do the maintenance or plug in the replacements yourself).
(Wish I was joking, but sadly I'm serious.)
When I find my shell scripts from 20+ years ago, they still just run as intended.
Any C/C++ project with even mild complexity has a good chance of being extremely difficult to build due to either missing libraries that have to be installed manually, system incompatibilities, or compiler issues.
Python has like 28 competing package managers and install options, half of which are deprecated or incompatible. I can't even run `pip install` at all anymore on Debian.
Even Rust, which is usually great and has modern packaging and built-in dependency management, often has issues building old projects due to breaking changes to the compiler.
All this is to try to say that I don't think this is some problem unique to JS at all - but rather a side effect of complex interconnected systems that change often.
A big reason Docker and containers in general became so popular was because it makes this problem a lot less difficult by bundling much of the environment into the container, and Docker is very much agnostic to the language and ecosystem running inside it.
Yeah, make sure no-one can ever fix your security vulnerabilities.
> As for compiler issues, just run it with the same compiler.
And when the same compiler doesn't exist for your new machine?
Freezing everything makes things easier in the short term, but much harder in the long term.
One can argue that this decision should be revised by debian but you should not install packages on system python installation for working into projects. Always use virtual environment.
» pip3 install supervisor error: externally-managed-environment
× This environment is externally managed ╰─> To install Python packages system-wide, try apt install python3-xyz, where xyz is the package you are trying to install.
As far as I can understand, they did this on purpose to dissuade users from installing packages globally to avoid conflicts with other Python environments.
Anyway, I'm not trying to argue about if that decision is right or not - I just wanted to use it as an example for my case that the JS ecosystem isn't alone and may even be far from the worst when it comes to this kind of issue.
Python is different here because in many linux distributions, there are many tools that rely on you system python. Python unlike node is not limited (in practice) to web applications. that's why you have to be more careful. So while I understand you are using this as an example, I don't feel that your comparison is apple to an apple.
They may or may not be running Node.js specifically, but I believe that many Linux distributions, as well as Windows, include JavaScript code in core applications. I don't see this as particularly different, except that they might choose to assume a single standard system Python that is able to be modified by standard Python development, whereas I would rarely expect that to be the case with however each component chooses to execute JavaScript.
No, they're not. I'm talking about core apps and services that are essential to a functional operating system. This is exactly the same situation. The difference is choices made by the OS and language ecosystem about how to manage dependencies in various use-cases. It is an apples to oranges comparison because of those decisions and not because of the language.
It's better all round to just assume that unless you're building something to be a part of the system itself, that the system interpreters just aren't for you. There's a special case for shells where they're actually UI, but I've seen so much effort wasted over the years trying to let system interpreters do double-duty as both system tools and development environments that I've come to the conclusion that it's simply not worth the hassle.
1. npm install
2. node my_script.js
First one downloads and installs all dependencies listed in a package.json file into a node_modules local subdir. This is equivalent to creating a pip venv, activating the venv, and running pip install against a requirements.txt file.Second one runs the interpreter with the script file and against the locally downloaded dependencies.
I.e. the local env dirs in Node.js are not a suggestion that you need to learn about, make choices, and actually use; they are just how the tool works by default, which makes the experience of using Node.js with NPM by far much better and less confusing than the default experience of using Python with PIP.
There are a load of scripts with `#!/usr/bin/python` (or similar) in `/bin`, which means package resolution can't look first in a local subdir otherwise all bets are off. Again: your system will break depending on which directory you run bits of it from. The system-provided process would be loading dependencies that it was not tested against. In case this is unclear, you do not want that to happen. It would be bad. On my system I can see python scripts involved in driver management. I do not want them to do unexpected things. It would be bad.
Python package management is a mess in lots of ways, but this particular choice isn't one of them.
Sure it can, it's just a matter of examining where it's being run from.
I wrote python-wool to load packages from a venv if it finds one.
I love Python but it has a terrible package ecosystem with mediocre tooling that has only gotten worse with time.
JavaScript has gotten better but it seems they are just re-learning things that were long figured out.
When I see new package managers, I just see a list of problems that they forgot to account for. Which I find strange when there have been many package managers that you can learn from. Why are you re-inventing the wheel?
We just had to workaround breaking changes in a patch version update of Spring Boot. Maybe it was true in 2005, but certainly not the case today. I know of products that are stuck in Java 1.8 and not because they are too lazy to upgrade.
However, some other projects around here using different application frameworks are stuck since the frameworks aren’t maintained or upgraded in ways that aren’t compatible anymore.
Looking into old Java code, it is hard to remember a time before enums and what a pain that is to deal with int constants instead of typed constants.
Node does not promise indefinite backwards compatibility, which is a design choice that they've made that allows them to shed old baggage, the same way that the Java developers chose to shed baggage in 8->9. Neither choice is inherently better, but you do have to understand which choice a language's designers were making during the time window in question when you go to run it later.
That's not the only reason. :)
Horrible syntax full of inconsistencies, bolted on type system with TypeScript helps but will always be bolted on, quirks everywhere, as if `null` was not bad enough they also have `undefined`, I can go on.
I simply avoid it for anything but small enhancements scripts on otherwise static HTML pages.
Likewise.
> I know [...] C, C++, C#, Python, Go, various flavors of Assembly
That's good. But these are all languages that either lack strong typing and or are themselves rather quirky.
Only C# and Go stand out, IMHO, as languages that are recently designed. Even Python did not have user defined classes in the first versions, and some things thus feel off (__len__, __init__, etc.).
Also C# and Go still have implicit nulls all over the place. Their designs show ignorance for modern language design. Sum-types, explicit null, immutability, sound type systems -- all lacking in all langs you mention.
So what languages do have these IMHO "Game changers"? OCaml/ReScript/ReasonML, Haskell, Elm, Rust, Gleam, F#, Scala, Kotlin, ...
Those languages really showed _me_ something important: how it could be better.
There is another group of languages that also sits on a unique place in the solution space: the LISPs (incl. Racket, Schemes and Clojure). I found it very worth while to learn to program with them as well.
If you do use C#, you may also want to add <WarningsAsErrors>nullable</WarningsAsErrors> to .csproj too.
The entirety of standard library is annotated since long time ago. All new and not so new projects are also null-aware. Pretty much either completely legacy libraries or libraries that explicitly removed Nullable: enable that is set by default for all new project templates do not have those.
As I mentioned previously - it isn't perfect, but the level of "good enough" of NRTs in .NET is such that the nullability is a solved problem.
And there's a reason practically nobody uses the languages you mentioned, and Javascript is so wildly popular. Most people don't really like or need type nagging systems. Sure, if you're trying to launch a rocket or doing something like building medical equipment or something else that requires covering your ass, then yeah, sure, go ahead and type the hell out of it. But for most programming tasks the languages you mentioned are overkill and frankly too obscure to use.
Also: TS popularity shows that not everyone in the JS community agree with you.
For server side web dev you will find that statically typed langs (Java, Kotlin, C#, Go) are a big chunk of the pie. Sure it also comes down to taste, but if you work in a large team, having stronger types can greatly help to keep the codebase in shape. (better IDE refactor tools, clearer for noob, harder to hide/ check in rubbish to git)
If you are afraid by obscurity, have a look at Kotlin!
TS is to the rescue when you have a big JS project, because that's what JS is not good at: big projects and large teams.
I just got burned by an old js (vue 2) project. I ended up re writing it using good old ssr in Django with htmx and alpine when necessary. Now it’ll run until the end of time. It doesn’t even have a build step.
It seems luck of the draw. My old React projects (old as in 2018) still work great with class components. I guess the Vue guy did say he would be more revolutionary, when he launched it.
and dare i say this is the lucky case. i had problems reactivating some older project because some dependencies were not version locked and newer versions of those were incompatible with other versioned ones. then it took forever to figure out the right versions. and i think i even had situations where the old version wasn't even available anymore (perhaps because of a linux dependency that wasn't available anymore on my newer linux system)
OTOH with Node I always find myself in dependency hell with dealing with old projects.
For some definition of "without issues"...
You can open and build a back-end application that targets e.g. netcoreapp2.1 (6 years old target) just fine, it might require installing you an archived SDK for the build to succeed (which you can download regardless, it will complain that it is EOL though) but it's an otherwise simple procedure.
For library code it's even easier - if you have netstandard2.0 target, it will work anywhere from .NET Framework 4.6.1 to the latest version without requiring any maintenance effort whatsoever.
On Windows, Visual Studio will happily work with .NET Framework 3.0 (which is ancient) and more.
I know for sure that I can clone a random project on Github, hit `dotnet build` and usually expect it to work on the first try. The rate of bitrot for average unmaintained JS project cannot be compared. The average dependency graph of a .NET project is going to be 10 times smaller too.
If this isn't enough, there are tools like Yarn which encourage vendoring dependencies to ensure that as long as you have a copy of the repository, the yarn CLI, and a valid version of NodeJS, you can always run the project.
It was open much the same in both. If you're happy using outdated and unsupported components with security issues AND you can get hold of the right version of the dev tools and plugins AND your hiding environment still supports the old platform, you can maintain the old version with minimal change. But should any professional developer do this?
In domains where software is expected to run for years, if not decades, it's common to archive the entire toolset, along with libraries and SDK's in case you need to fix a bug/add a feature 10 years later. Obviously, in this case you can't have dependencies sitting somewhere on a server you don't control.
There are also situations where this is forced by regulation: you need to recreate build xxx.yyy.zzz 7 years after release because a customer reported a serious bug and it needs to be reported to the relevant regulatory agency and investigated.
Can remember trying to update a Unity project so it'd be still buildable for 64-bit Mac devices. The very first version bump I done resulted in several critical libraries no longer working and there being no clear alternatives to swap in.
It's arguable whether this is the correct decision, but it makes things slightly harder than they used to be
This depends highly on what dependencies are in your C# solution.
Same for his node project. If he'd stuck with dependencies that are just plain js - I'd bet money his project would have installed & built just fine.
By the time you're hitting the native addon apis and doing things like compiling python and C/C++ code... you're going to feel all the pains of those ecosystems too.
Pefectly acceptable? Perfectly? Really? I have 10 year old C and Go projects that build and run fine as if nothing has changed. I can upgrade the dependencies if I want to but that's on me. The projects themselves have no problem building and running fine.
How complex is the project? I know ML utilities like that can be as simple as 50-100 lines.
Locking env version is important.
Double points for using experimental / POC technology like gatsby or nextjs. They are expected to burn and fail
There are a very small number of projects that specifically make it their goal to be backwards-compatible effectively indefinitely, and Maven is one of those. It's part of what people who hate it hate about it, but for others it's the main selling point.
And Maven is a package manager for Java. The main difference IMO? The usual way to do things in Maven is to always use exact versions for the dependencies. When I specify I want some dependency at version 1.2.3, Maven will use version 1.2.3 of that dependency even if 1.2.4 or later already exists.
It will not build all old projects out of the box, of course. Specific versions of plugins may not be compatible with it or some dependencies may break on modern JDK. But chances of hitting this issue are much lower than in JS/NPM ecosystem.
That’s the usual way to do things in most teams working on app code I’ve been a part of (as opposed to library code where version ranges are preferable).
Try any old Spring project, where anything newer than JDK 8 will be incompatible. The only saving grace is that JDK 8 is still available, but even it will eventually reach EOL. And then you look at JDK 11 projects and realize that they won't run on anything newer due to Lombok issues, so that's another thing to update and fix.
I think the experience of code rot is universal and increases with the amount of dependencies you have.
This is true but there's also a factor from the language/framework in use. Node is especially bad because of it generates huge package dependency trees. Go is especially good because of the large stdlib (which I use to minimize deps in https://github.com/contribsys/faktory) and excellent backwards compatibility.
Come on… compare it with what java and python have.
They do have syslog though https://pkg.go.dev/log/syslog
Tell me a single thing that Oracle has added to the standard library. As far as I can tell, more and more of what was once standard is now getting offloaded and trademark-washed with the eclipse and apache foundations.
The company I work at has a ton of projects stuck on ancient spring versions and Java 8 (or Java 6 in one instance). They still insist on Spring despite being essentially unable to upgrade to a version newer than a decade old.
That's the reality of using the language in production projects.
Same as how you're likely to see more than just Python or Node being used in projects that list them in the tech stack, because there's native dependencies and tooling used.
My son found a disk with some of my old java project from college 20 years ago and that's about what it took to run them, first figuring out how to even download java and then making some minor changes to get them running. I think we gave up trying to get the actual applet based ones to run.
You also need a browser that has NPAPI. IE 11 was the most modern browser I am aware of that still supported applets.
The old GUI framework mentioned in the GP might have been Swing. It is still included in most JDKs and allows for cross platform desktop GUI application development with no other dependencies outside the JDK. Finding documentation on how to do GUIs in Swing is getting increasingly difficult though.
Haven't used javaFX for a while but this is worth a shot
`cmake ..` otoh, tends not to.
Most who are here saying that X, Y, or Z ecosystem "compiles and runs" fine after 4 years are talking about the time it takes to resume an old project in a language they're very familiar with running the same dependency versions, not the time it takes to version bump a project on a language that you don't know well without actually having it running first on the old version.
I can open my 4-year-old Node projects and run them just fine, but that's because I use the tools that the ecosystem provides for ensuring that I can do so (nvm, .nvmrc, engines field in package.json).
Npm at this point is probably the most used and most worked on package manager in history. If it is still one of the worst ones that is actually kind of interesting philosophically.
This whole thread just reeks of JS hate. I'm not perfectly happy with the language either, but nor am I perfectly happy with any other. If my employer wanted me to use C# or Go or whatever instead I'd be perfectly okay with that as well.
It's really not. I'm in the same situation with a Go + React project I haven't really touched in 3 years. The Go part is just `go build`, it still works after 3 years and I have a build in a fraction of a second. The React project doesn't build anymore at all. I used Parcel 2 at the time, and it turns out it's incompatible with a Mac M1 for some reason, and it's hard to update in my case. I also used Antdesign and some components (icons specifically) apparently disappeared. It should not be so hard.
This was with zero awareness of Svelte 5 (I only knew it was released — I wanted to upgrade because Node 14 is EOL now and I’d noticed some npm audit warnings.) So I feel I also learned something in the process.
When I was managing teams, whatever the language, I would ban any new dependencies which I didn't personally agree with. A lack of control just creates a nightmare.
Normally when I'm the one with that power we rapidly get to a general understanding of what's small enough that I (a) probably won't care (b) will take responsibility for tweaking the schedule to makre time to get rid of it if I do.
And 'big' dependencies are generally best discussed amongst the entire team until consensus is reached before introducing one anyway.
It is systemic. Part of it is due to too many people creating systems on the fly with too little forethought, but also because there aren't enough "really smart people" working on long term solutions. Just hacks, done by hacks. What did you expect when the people writing the systems don't have long term experience?
If you must depend on node-gyp, perhaps use dev containers so at least every developer in your team can work most of the time.
I don't even develop against Node, it has just crept into our front-end build toolchain.
If upgrading is difficult because of 4 years of breaking changes, blame Gatsby for not being backwards compatible. Also blame your original choice of going with a hokey framework.
Speaking of hokey framework: 167 dependencies and 3000 versions of Gatsby in npm.
I disagree completely. Regardless of what you think of Gatsby, Node versioning is a simple problem that affects nearly every javascript project. It should always be the first thing you check.
I made this dumb obvious mistake again just last week, looking for a little time to audit all my `package.json` files for `engines`.
That's a quick fix, upgrading a framework is a guaranteed min hour of poking around before the system is even running.
I dunno why `engines` isn't in every `package.json` file, would certainly have saved me hours of nonsense.
I had a similar experience with emberJS when it was still young. Every time I picked the project up I had one to two hours of upgrade work to get it to run again, and I just had a couple hours to work on it. So half my time went to maintenance and it wasn’t sustainable.
I’m trying a related idea now in elixir and José may be a saint. Though I fear a Java 5 moment in their future, where the levee breaks and a flood of changes come at once.
On the other hand, there's a reason I regularly get annoyed enough at it to call it nope.js.
On the gripping hand, I mostly write perl, which argues for a different but unique set of masochistic tendencies on my part.
(you just have to remember that what 'perl' *really* stands for is 'Perenially Eclectic Rubbish Lister' and then you will have appropriate expectations and can settle back and have fun ;)
for the latter i get around the problem by avoiding build tools altogether. i use a frontend framework that i can load directly into the browser, and use without needing any tools to manage dependencies. the benefit from that is that it will ensure that my site will keep running for years to come, even if i leave it dormant for some time. the downside is that it is probably less optimized. but for smaller sites that aren't under continuous maintenance this is a reasonable tradeoff. i built all my recent sites that way using a prebuilt version of the aurelia framework.
incidentally just today i tried to research if i could build a site with svelte that way. well, it turns out that although it should theoretically be possible, i was unable to find a prebuilt version to do so after a few hours of searching. for vuejs i found one within minutes. i'll be learning vuejs now.
see this thread for a discussion on going buildless: https://news.ycombinator.com/item?id=41479365
I haven't yet decided if/how I want to include a prebuilt version of it in the repo, I *think* I may go the approach of having a commit that modifies libs.js and/or the lockfile and then an immediately following one that commits an updated prebuild ... oh, huh, actually, I should probably also consider doing those two commits on a branch, then forcing a merge commit so they land on master atomically but it's easy to tease out the human changes and the regen changes by poking inside said merge commit ... yeah, like I say, still thinking about exactly how to do this, don't mind me.
Also for even simpler cases I've been using the preact-htm prebuild directly, since htm gives a lit-style html() tagged literal consuming function that can produce vnodes for preact so I can mess around without needing something that understands jsx between my editor and my browser window.
vue's component system is IIRC noticeably less nice to work with if you don't have a compile step, but it's still pretty nice even without that so please don't think I'm trying to dissuade you here :)
saving a prebuilt version of code that needs building is of course also helpful, and much better than having to rely on keeping your build tools working. but when you want to make changes to the site you have to either deal with the build tools anyways or work with the prebuilt version which may not be as practical.
either way i would simply save the prebuilt version to a branch and if any changes are made in that branch cherrypick them over to the dev branch if they even can be use, which i am not sure about. i'd probably rather avoid making changes to the prebuilt code in the first place
how does lack of a compile step affect the code? are there things i won't be able to do if i don't compile? i haven't started yet, and my website is not very complex so i think i'll manage either way, but i am curious. can you link to an example?
However you said you're using a prebuilt version of the aurelia framework - and I'm using only using a prebuilt version of the frameworks I'm using.
Which seems pretty equivalent to me, except that your framework prebuild is created by somebody else running a script and my framework prebuild is created by me running a script. Either way the result is a single framework file to load that gets treated as basically a black box from then on.
My actual application sources are being direct loaded unchanged just like yours are.
The difference is basically syntax - vue recommends (and most example and/or real world vue apps I've seen use) their Single File Component syntax, and *that* requires a build step - see here for the difference in that and the build-free definition syntax: https://vuejs.org/guide/essentials/component-basics.html#def...
As I hope I managed to make clear, not a big deal at all in the grander scheme of things, just something to be aware of.
you are right, i didn't read it that way, but i get it now.
i forgot about that aspect of vue, but if i read that correctly the joke is on them because the single file component syntax is one reason i initially rejected vue. i felt that it would make editing harder because editors would need special support to handle the file format. although looking at it now, it doesn't look so bad. it's just html with inline javascript.
anyways it looks like instead of inline templates i can also reference external templates and that would let me structure the code the way i want.
you are right, it's not a big deal in the grand scheme of things, and even from someone being prejudiced against the component syntax, it is a tiny issue compared to all the other good or bad choices a framework can make.
thank you for the link.
Given the javascript is still inside a <script> tag I would presume any editor that can handle a normal HTML page with some inline javascript wouldn't notice the difference, yeah. Hadn't honestly though of that since one of the first things I do in any editor is turn all the file format handling stuff off because I'm a curmudgeon who thinks in https://github.com/n-t-roff/heirloom-ex-vi
Yes re external templates; my usual approach to 'keeping the code and template close together' is to have them open in adjacent 80x24 xterms.
You might find mobx of interest - I tend to use that for state modeling no matter the framework doing the rendering - everybody seems to be getting very excited about 'reactive signals' these days and ... they basically all have their own implementation that feels, to me, like Yet Another NIH Of A Tiny Subset Of MobX ... except invariably missing at least one feature that I really wanted.
Which is how I ended up with my own libs.js on a current project, the sum total of which is
export { render, createElement, options as 'preactOptions', Fragment } from 'preact';
export { observable, action, computed, flow, createAtom, Reaction } from 'mobx';
export { observer } from 'mobx-preact';
and then on the fairly sporadic occasions that I need to adjust the exports I have a shell script that does a tiny bit of bookkeeping and then runs bun build --format=esm src/web/libs.js >bundle/web/libs.js
so I run that, and then go back to forgetting that the build process (and the node_modules directory it's sourcing those libraries from) exists.I also have about 20 lines' worth of custom dev server that will serve the bundle/ file preferentially over the src/ file if it exists, plus a couple other minor things.
But this is, for me, all about keeping things as simple as possible barring some slight effort towards ergonomics, plus knowing that I understand every part of what's going on so I don't end up stuck in a "one of my abstractions is leaking and I've no -ing idea how or why" type situation (hence also my very minimalist choice of editor, any time I try a more clever one I fairly rapidly end up in a situation where the tab completion does the wrong thing so I just type everything out anyway, or where the syntax hilighting produces colours that give me a headache, or ... just colour me a curmudgeon who learned his chops on ancient BSDi and Solaris systems, I don't expect anybody else to want to use my dev environment but it works for me).
Anyway. None of this is to try and convince you of anything much, I just thought you might find my setup vaguely interesting. I'll stop waffling now :D
i agree. it's been a while that i looked at vuejs, and i don't know why i came away with a different impression before. must have not looked closely enough.
i haven't seen mobx before, but when i read the description i wonder why i need it. my preferred framework is aurelia and as far as i can tell aurelia already does what mobx claims to solve, in particular this part (from the mobx github page):
Trying to update a record field? Simply use a normal JavaScript assignment — the reactivity system will detect all your changes and propagate them out to where they are being used
it is actually the primary reason why i like aurelia. with aurelia i don't even have to mark properties as observable. it figures that out on its own based on the bindings i make in the html template.
though mobx may be interesting when i work with other frameworks that don't do that. i'll have to keep that in mind. (edit: it looks like it may come in handy when statemanagement becomes complex: https://stackoverflow.com/questions/39454579/best-practice-u... )
keeping things as simple as possible barring some slight effort towards ergonomics, plus knowing that I understand every part of what's going on
yeah, i like that too.
I just thought you might find my setup vaguely interesting
i do indeed. thanks a lot for that. my own setup is actually also quite simplistic, but not deliberately so. it's mostly lazyness. i simply don't want to be bothered to put a lot of effort into a better dev setup. i'd rather work on actual code. so i start with plain vim without any addons, and only slowly change stuff when i run into a problem that really bothers me. solaris, AIX, irix is where i started.
I took a quick look at aurelia after you mentioned it and am clearly going to have to take a deeper look at some point, I'm curious how it's handling all that under the hood, and for cases it handles well it does indeed look really rather nice (though being me I'll need to take it apart before considering using it, automagic reactivity is really cool but only when I can reliably dry-run its path through the framework in my head as I'm writing the code that (ab)uses it).
(so, cheers for mentioning aurelia, all the best conversations involve both/all people involved coming away with extra things/ideas to poke at :)
Anyway. To mobx:
I think the best may to understand mobx before you've actually used it is roughly "it not only provides simple reactivity stuff, when stuff starts getting more complicated you'll find that the more powerful tools you wanted are already there, implemented, and will show you what they're doing in the devtools out of the box."
As an example, computed() is very handy (and the one mobx feature other than just "reacting to changes" that most frameworks' reactivity implementations *do* copy, although I have vague memories of them not always copying it as thoroughly as I'd like) - so this is a bit of a "too simple" example but we're in HN comments so
class SomeData {
@observable rawData = [];
@observable sortBy = 'someField';
@computed get data () {
let { sortBy, rawData } = this;
return rawData.toSorted((a, b) => (a[sortBy] - b[sortBy]));
}
}
(sort function designed for numerics only, dumbass example is dumbass)and then assuming your display component is tracking its dependencies somehow, a change to the `someData.sortBy` field will automatically expire the cached `data` element and notify the component so it can re-render, at which point `data` gets recalculated and re-cached (and all this will show up as events in the devtools if you've stuck them into your page somewhere).
Since aurelia is basically taking apart your bindings to figure out what to observe, I think it wouldn't be able to track that, and you'd instead have to make sure that when `sortBy` gets changed it fires a trigger to recalculate the `data` field (and then aurelia *would* notice that getting set to a new value).
(aurelia may be smarter than I think, but the only way I can think of for it to *be* the necessary amount of smarter would basically be to implement a subset of mobx's makeAutoObservable and I don't think it does that and I don't think given aurelia's (clearly consciously chosen) aesthetic it *should* do that)
I'm tending towards having viewstate kinda objects as a layer and then model objects behind them, so `rawData` would delegate to the model objects in the above example - then the code that modifies the data I'd be persisting to a backend doesn't have to think/know about how it's going to be displayed, but if you modify it - `addTodo` or an `editTodo` or similar - the change notifications will propagate outwards, expiring things in the viewstate, causing things in the view to notice and re-render themselves as required.
This is especially noticeable to me when e.g. I'm writing a data viewer onto a table of log entries (or something in that vague area, *handwaves vigorously*) and I want to have a 'Refresh' button - that triggers something in the model layer that does a `fetch()` call or whatever, and then sets the new model data to whatever the backend sends back ... and then everything re-renders, keeping all my display choices intact because the viewstate layer didn't get touched.
My experience of learning mobx included a number of "oh for crying out loud, I've implemented half of this feature the hard way in three projects before now" moments - and *that* was what made me fall in love with it.
You might also want to bookmark (and then mostly ignore for the moment) mobx-keystone, which is Even More Complicated under the hood, but provides fairly simple syntax for declaring a tree of reactive state classes that provides (typed if you're using typescript) constructors for you, tree snapshots if you want to be able to save your session state to the backend to resume later, and mutation event logging as JSON-able objects that also includes reverse versions so you can get full undo with a fairly minimal amount of effort (I never, ever want to write undo functionality by hand again, that always required a significant amount of bourbon). Not necessarily to actually *use*, mind you, but seeing what keystone is capable of was useful to my understanding of what mobx itself was capable of.
I ... there's definitely a "would rather be writing code than yak shaving my setup" aspect to my choices, but it's definitely also a question of having as few layers as possible between me and What Actually Happens so things reliably behave how I expect them to behave. Debugging is much more fun for me when I can be confident I'm debugging *my* code rather than there being a decent chance I'm actually debugging how I've misunderstood something else two layers down.
(also, if you're not doing anything particularly strenuous, I'd suggest having a look at bun as an alternative to node, all my 'real world' and/or work stuff is node and I'm not particularly offended by it, but bun is definitely a smoother and more DWIM experience for me, at least so far)
i am really curious what you make of aurelia once you take it apart. please ping me if it is not to much trouble. i discovered aurelia when angularjs 1 was being redeveloped into angular 2. rob eisenberg was invited to the angular team based on his work on durandal but when his ideas were not accepted he left again and built aurelia instead. aurelia is not perfect either, but it was way better than angular 2 or vue at the time.
on your mobx example, you are right, aurelia doesn't track everything. it may be able to handle some level of complexity but i certainly had situations where i had to explicitly send a signal to get aurelia to update values.
It's a python codebase, largely abandoned by google. They used to use it for building Chrome.
Gave up.
Then I ran `devbox init` and installed whatever it told me that was needed. `devbox shell`.
We're in ... let's call it a transitional period at work. I've got something like a dozen versions of node being managed by asdf. And in half of the projects I work on regularly, I consistently get warnings about this particular project failing to build.
One day, I'll actually look up what it actually is, and what it does, and why it's being built, but is apparently optional.
Everybody complains about it, and understandably so, but if it didn't exist you'd probably instead have one set of potential similar problems *per* native module which has a good chance of not actually being better overall.
The counterargument is, I guess, "well, only people who can write their own high quality build setup in-tree should be writing things that bind to external code," and I do sometimes dream of that, but it's not hard to see the downsides of living in *that* world instead either.
Anyways, npm ci should have been the first attempt, not npm install so that it installs the same package versions defined in the package-lock.json. Then as others have mentioned, pin your node versions. If you're afraid of this happening again, npm pack is your friend.
In the end, op could have done a bit more. BUT I'll give it to him that when bindings are involved, these things take more time than they should
Also, do not run shit on a node version that is years out of date and out of service. Also, update your damn packages. I know I sound cranky, but running anything internet facing with god knows how many vulnerabilities in is an exceedingly bad idea.
If Nix is too heavy, the learning curve for tools like asdf-vm and mise is much lower and offers similar benefits.
I really wish there was a good equivalent for Windows.
"go mod init" + identify a working dependency version was all I had to do on any of those 10+ year old projects (5 minute work tops)
it's absolutely the expectation for C++ projects to support building on GCC, Clang and MSVC
https://dubroy.com/blog/cold-blooded-software/
Sibling comments say in so many words, it's no big deal bro, just update. But it is a big deal over time if you have dozens of cold-blooded projects to deal with.
CI solves it because it proves that it can build in the pipeline, using a well defined environment.
No guessing at which node version you need or any other dependencies that may be required.
It's not hard. The explanation is simply that the dart version of SASS (the sass npm package) is much slower than node-sass.
At one point it was like 20x slower and I believe right now it's about 2-3x slower.
If I had only wasted two hours every time I had to use npm for some reason I'd be significantly ahead of where I am now.
> run command xyz
I run xyz and I'm on to the next step in an average of ten or fifteen minutes (because yeah, stuff still goes wrong some times).
But if the instruction is to invoke npm, there's always some crazy side quest involved. It doesn't get any better with time because next time around it's a totally different side quest. I can't even offer any criticism towards fixing it, it just sucks for unique new reason every time.
My favorite is the way that Python projects rot. Not only does Python's setuptools give you all the fun that node-gyp does, the common practice of versioning packages with packagename>=1.25.5 means you're almost guaranteed breakages as pip installs newer versions of packages than what the project was built with.
What really pissed me off about it was that Python 2 was already known to be nearing EOL when our project was started, so node-gyp should have been upgraded to work with Python 3 by then. And even more annoying was that node-gyp already had Node to run on, so why in the world was it coded to depend on Python at all!?
Most of the times it will not work with the newest shiny python, which I only notice after already installing it and then having searched search the Github issues.
Granted, my experience is mostly from the GPT-2 era, so I'm not sure if it's still this painful.
That way, the project has just the dependencies it needs, and I know I can rebuild it at some point in the future and will be unlikely to run into problems when I do.
I personally use Nix these days, but the complexity is too high for me to recommend it to everyone for every software project.
Fortunately this mindset has been changing in the node ecosystem with projects like https://hono.dev/ (koa/express successor) and https://github.com/porsager/postgres having zero deps.
- bump patch or minor version from a react package but the maintainer rewrote the entire project breaking a lot of things, following semver it's bad to expect things don't break like that for such version; - another example, the ruby gem is removed/yanked from rubygems.org and you'd to find a fork available
On the end, we need to ensure the good practices from software engineering about tests and good release management, the last btw is decades old
In the JS ecosystem, I'm aware that Meteor is one major framework that takes backwards-compatibility seriously. Updating a project on an ancient version to a less-ancient version usually is not too hard. They try to keep APIs the same and introduce compatibility packages where possible.
Meteor 2.16 to Meteor 3 introduced major breaking changes due to an underlying technical issue that had no workaround. They had to refactor the whole project from using Fibers-based concurrency to typical async/await.
node-gyp in general has also been a source of issues in the past for me as well.
More recently, ESLint changed their configuration file format and all existing tutorials suddenly became outdated.
I firmly believe the ecosystem does not have to be like this, and we would save a lot of man-hours by being more committed to API stability where possible.
That's when I picked up the Node/React ecosystem...
> node-gyp errors > downgrade to 12.2
This is what I did until Vercel decided to not support Node 12 anymore...
"Not Invented Here" is whats going on. Developers of this age need to learn this.
A recent exmaple is the RPI Foundation nullifying thousands of internet tutorials renaming "/boot" vs "/bootfs". Ask yourself a serious question, did that actually improve anything? No it did not.
Node is an active project. If you build against the native API and don't pin your version to avoid breaking changes between versions, this is what happens. In my experience, JS very rarely breaks between major Node versions, but almost every native package requires a new major update.
This isn't a Node specific problem. Go ahead and upgrade your Go or Python version.
Experienced programmers will not pick up a "built on shifting sand" stack, because they can acutely perceive the pain and suffering before it happens, generally from past experience. With fast-crumbling stacks, you need to execute quickly and move on, and treat the whole codebase as an expiring entity. Stacks I personally try to avoid: anything node/javascripty, anything Androidy, anything iDevicey.
Those who don't understand Unix are condemned to reinvent it, poorly. - Henry Spencer
Beat SSG I've found, and all from a medium or dev article on a SSG in 40 lines or less.