From Ruby to Node: Overhauling Shopify’s CLI for a better developer experience
shopify.engineering
shopify.engineering
Sure, before they had basically no tooling, but then they had some basic ones that everyone was basically happy with, and then they just decided to iterate too much.
(saying this as a ~ 8 year long Partner / Expert / App Developer)
I've spoken to some Shopify employees and they all agreed that the developer teams are a bit of a mess. It seems like nobody managing the developer products are actually using the end results, otherwise they'd realize the hoops needed to work through (unless you are starting a brand new project right now --- and hoping that they don't decide to change it all, like they have 5 times in the past 3 years)
The previous designs (which worked very well for a long time) seemed to have been all thrown out/deprecated to such a large degree it has caused a lot of long time devs to be scratching their heads.
But so much has changed now. Deploying a customized frontend has a negligible cost, and the API surface that Shopify powers could be powered by a myriad of tools, with Stripe etc. doing large amounts of heavy lifting on the administrative side.
Were contractual obligations, and not wanting to bite the hand that feeds, the only thing preventing longstanding Shopify app developers from joining together and creating an industry-standard alternative API surface that could be deployed in different ways?
And whilst she appreciates all of the Shopify Apps they are secondary to the fundamental process of taking, processing and shipping orders.
The key to the whole ecosystem are non-technical users like her not the app developers.
Nowadays it's not that hard to get UX/DX right if you are actually committed to it.
As you mentioned, if they're not OK with CLI, they could refactor in Ruby. The whole "embracing functional programming" and MVC architecture (i think Rails when i hear MVC) as reasons to go to Node, and not Ruby, is nuts.
If they were worried about dependencies, they'd use Rust or Go as they mentioned.
It might not even be a top thing, if they migrated internal eng from Ruby to node internally I would not be surprised if this was dev-driven either
Just how complex is your command line app if you need to have multiple transitive versions of the same library?
Coming from the same company that is so heavily invested in Ruby that they contributed to a new JIT for it (YJIT), built a static type checking layer (sorbet) for it, and built a module system that prevents you sharing code between those modules (packwerk), etc.
Unrelated but it is reminding me of my attempt to integrate with Zapier. Zapier doesn't document an API anywhere, you have to use its 'CLI app'. The CLI app of course just calls undocumented HTTP APIs behind the scenes.
It will happen again. I believe they have terrible tech leads / managers who push/approve wrong things. They also grew exponentially during the pandemic, so that grew their manager's incompetence exponentially as well.
I've found some ok golang api clients which seem to do the trick so far.
finally, i do think it will change again because the structure for theme app extensions have changed and now i'm not even sure if my project will work if i update that section.
the migration documents are very bad and they remove old documents. for example, it used to be that you need to add a folder called `theme-app-extensions` and it's very different, and there is no migration docs for that. they just want you to use cli 3 out of the blue.
I notice this pattern of shoving nodejs / js, especially react through people's throat for the apps developed on the platform. Polaris, Session tokens and all the boilerplate apps the CLI generates are great examples of that.
I would rather having Polaris as CSS framework + some JS bits in vanilla-JS, Auth + Session tokens as just as a small JS library. But instead I am forced to use React. I know Polaris is also available as CSS only but that's almost impossible to use as it's not designed for human use really. Or Bridge exists as a library but for many cases it's just way too bloated and documentation for non-react version of those things are not so good.
If learning a language as simple as Golang is a real barrier for Shopify engineers, then I’d say they need to reevaluate their hiring processes. Especially given that they’re talking about CLI apps as opposed to something like distributed systems.
I’ve hired multiple engineers who had zero experience with Elixir at all and it’s generally just taken a couple of days to get up to speed. I’ve had the same experience on client projects, too. I don’t think Elixir is as difficult to learn as JS or Rust, but Golang might even be simpler given how small the language is.
I don't think Go has to increase more friction in the dev workflow, but it probably does require a little bit (one-time) more effort to get it right.
I suspect a part of this is that startups that use Rails (or Phoenix or Laravel) don’t need as many devs as startups doing a mix and match of many libraries instead of a full stack web framework.
The entire Node ecosystem is commonly described as a dumpster fire.
I moved the Zapier CLI to oclif a few years ago over a bespoke version, since it handles a lot of the nitty-gritty of displaying text in terminals. It's a pretty nice setup and allowed us to focus on the important things.
https://github.com/ericbeland/ruby-packer
Also, there was an issue with the most recent X-Code, so I had to actually downgrade my local X-Code to compile rubyc.
I wonder if Shopify had known they could build the ruby CLI into stand-alone binaries (no Ruby install or Node needed) if they would have still gone the Node route? Not that I blame them for not knowing--I had to fork the motor-admin fork, and update a few things to get it working. The original ruby-packer only works with ancient Rubies.
At MAANGs, Rust is taking over and Go is waning. Go allows for quick productivity from 0 but it plateaus due to an unwillingness to add features. Rust has a Haskell/Nix/(fancy tool) problem but it has a good productivity curve that keeps going. Sometimes worse is better as UNIX people might recall.
https://blog.herokuapp.com/evolution-of-heroku-cli-2008-2017 Their version 6, started with Ruby, then go/node, then pure node.
https://github.com/heroku/cli "This is the next generation Node-based Heroku CLI. The goals of this project were to make plugins more flexible, remove Ruby as a runtime dependency, and make the CLI faster. [...] It has identical functionality to the old Ruby CLI."
Is there a library that does self updating in place for binaries? Go is the langage that comes to mind when talking about single binary, so maybe a go library?
(I’m the founder of the licensing API.)
With scripting-language CLIs, you either have to deal with the module install process or use an external packaging solution.
I think there are equivalents in the Python ecosystem.
All the files needed to run a node application are in node_modules. If node is on the container, you can just rsync the scripts.
I think you're comparing distributing stand alone binaries, vs distributing the source code for a node app, and then installing its dependencies, and maybe doing some transpiling. That's not a fair comparison.
Look at this:
https://github.com/Shopify/cli/blob/main/package.json
but wait...this too:
https://github.com/Shopify/cli/blob/main/packages/app/packag...
https://github.com/Shopify/cli/blob/main/packages/cli-hydrog...
https://github.com/Shopify/cli/blob/main/packages/cli/packag...
https://github.com/Shopify/cli/blob/main/packages/create-app...
etc, etc, etc
VS
https://github.com/Shopify/shopify-cli/blob/main/Gemfile
What am i missing? Why do you have to rely on all that junk to run a CLI? It feels like people choose Node because creating a Rube Goldberg machine creates job security.
Not at Shopify there ain't.
1. dependencies for Gems are specified in the gemspec file and not the Gemfile. See https://github.com/Shopify/shopify-cli/blob/main/shopify-cli... for example. There's a few non-development dependencies.
2. since it's difficult to package up a Ruby gem for distribution, maybe dependencies were vendored directly in the codebase: https://github.com/Shopify/shopify-cli/tree/main/vendor
This isn't meant to be a comparison of the number of dependencies or anything. Just pointing out a few nuances to how the Ruby dependencies were handled.
The dependency injection pattern fits better. DI is basically passing around objects which conform to an interface. This way you can mock out those objects more easily.
Ruby has enough sharp knives that many patterns are pointless, or trivial enough that they're idioms instead, or that the downsides of the pattern are drastically limited.
(example: command pattern vs lambdas; delegate vs blocks)
Having singletons can make sense, if the abstraction has to be leaky...
(examples of abstractions that must be leaky: AWS vs GCP) (examples of abstractions that can be tight: Segment vs Rudderstack)
...because then you're never really in a position where you need multiple objects, or to swap out the implementing object, during production runtime. So you don't benefit much from DI on prod.
And in Ruby, when you're in the tests, there's a handful of nice and handy shenanigans you can pull to remove the downside / AKA make it trivial to mock the object.
--
AKA Ruby is flexible enough that you need DI like a contortionist needs a backscratcher.
They seem to be more involved with contributing to Ruby than any other company, contributing an incredible amount, including yjit and object shapes and more.
I don't know why they switched, but I haven't used ruby on rails that much.