Reorganize Package Exports / Imports #5403
Replies: 5 comments 1 reply
-
In Remix Auth I had to import from the unique shared package, server-runtime, same for Remix Utils Also in Remix Utils Typed Cookie and Typed Session I had to wrap the Cookie or Session Storage objects because each runtime provide an implementation and server-runtime only the base |
Beta Was this translation helpful? Give feedback.
-
Your meme game is on point! 😍 I worked around this import circus by creating a script that re-exports from the Remix packages. This way I simply |
Beta Was this translation helpful? Give feedback.
-
This is going to make it even easier for folks coming into the web/js ecosystem for the first time for Remix to use it 💿 |
Beta Was this translation helpful? Give feedback.
-
Right now the server runtime packages use a sort of classical inheritance model. Although packages don't actually "extend" one another, that's the current model. What you're proposing @ryanflorence is that we ditch inheritance for the composition model, which is also a valid way to do things. I'd like to outline the two mental models and the trade-offs of switching to a different model. InheritanceIn this model (the current model), CompositionIn this model we expose basically everything in (or most of) What you're proposing (e.g. Trade-offsThe main advantage of the inheritance model is that you have one less dependency in the app's package.json. This is a BIG one, not because of the number of entries in
Conversely, the main advantage of the composition model is that it's easier to write a portable In talking with @pcattori about this he noted that the composition model is less of a maintenance burden on us, since we don't have to remember to re-export everything from each of the concrete implementations. But we should be able to write a unit test for this to eliminate this burden. It's important to note that in the composition model we lose all the benefits of the inheritance model, which is a pretty big loss IMO. I'm doing my best to be as objective as possible here about the trade-offs. Please let me know if I've missed any. |
Beta Was this translation helpful? Give feedback.
-
@sergiodxa I think this is ok. We should recommend to 3rd party Remix-y libs to import directly from |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
tl;dr
Prior art
Background
When we created Remix we had never seen anybody build a web framework that supported multiple JavaScript runtimes. To do it, we built our code around the Web Fetch API (:hat-tip: Cloudflare Workers) instead of node.js APIs
We had two goals:
We failed at (2). The types have always just been the DOM types. If I remember correctly, we anticipated needing to pass the polyfill into Remix runtime packages which influenced our current design. We ended up just adding the polyfills to
global.*
instead. I think this also influnced our decision to "import from the runtime" cause we didn't expect to have universally useful functions likejson
.Magic Exports
For (1) we first did magic exports with some node_module shenanigans (detected the runtime in package.json, created fake node_modules/remix to import everything from). That turned out to be a bad idea and messed up yarn, pnpm, lerna, turborepo, kiwi (loljk) and friends.
Today: Adapter + React + Runtime
After bailing on magic exports, our goal was to minimize the number of
@remix-run/*
dependencies in package.jsonMost app code would only need to import from two places:
@remix-run/{rendering-lib}
react, and future others@remix-run/{js-runtime}
node, cloudflare, etc.And then
@remix-run/dev
as a dev dependency@remix-run/{adapter}
to set up a serverWhile you still have four dang packages, at least the code in
app/
only imports from two.While this has been fine (alright, it's been mid) it also introduces undue friction where there is technically no reason to have the friction. We also do extra work in Remix itself to add the friction!
We create our own demons.
Problems
Abstractions don't know where to import things
Imagine an auth abstraction that is published to npm or in a big project's monorepo that's shared across multiple Remix apps deployed to various places (one on node, one on oxygen, one on cloudflare, etc.).
Now imagine it simply wants to redirect. Where does the abstraction import
redirect
from?There is no technical reason
redirect
comes from@remix-run/node
or@remix-run/cloudflare
, etc. The runtime is irrelevant,redirect
is universally useful and built aroundResponse
.Today, folks use
server-runtime
but that's undocumented, considered internal, and has got a bunch of abstractions for creating runtime packages that should be irrelevant to apps.Multi-runtime apps
Jacob and others are deploying Remix apps that are multi-runtime! Some routes are handled by an edge runtime while others go to a node runtime. This is likely to become a common architecture.
In these apps the whole idea of "pick a runtime" doesn't even make sense anymore. Consider a simple cookie session:
Not only does this not make sense because cookie sessions have nothing to do with node or cloudflare, but it will break the routes that run at the edge runtime because
@remix-run/node
importsfs/promises
.The only option today is to import from the actual source of
createCookieSession
which isserver-runtime
.I think we did this because we expected to be injecting the fetch API polyfill into the runtimes so you'd need to the import these things from the package with the right polyfill. But we put them on
global.*
instead, so there's no reason to import them from the runtime. It's accidental, artificial friction we added as we blazed this multi-runtime trail.Documentation and Demos
While this is not the reason for this proposal, it is a symptom of the problem. Our docs do this everywhere:
Re organizing our exports to simply export what is unique to the package would clean this up.
Types
Types are also strange:
Why does that come from the server runtime? Meanwhile this function comes from the react package?
LinksFunction
is has nothing to do with the runtime, andShouldRevalidateFunction
has nothing to do with React.These should both be coming from the same package as they are Remixisms, not react, node, or cloudflare.
Proposal
While we can bikeshed this, I think the best package name to use is the
"remix"
package.Let's consider the use cases previously outlined, first is the authentication library that simply wants to redirect.
It's obvious where to get
redirect
. The multi-runtime app (deployed to both an origin node server and an edge runtime) also has no issues with importing a Remixism:All of our docs can simply import from "remix":
And the types clean up too:
After Pedros audit, we found that only a handful of APIs aren't universally useful. So instead of us doing extra work with our packages and adding artificial friction by re-exporting everything from runtime packages, consumers can simply import those handful of APIs directly from the package that implements it.
No magic exports shenanigans, no package.json module/export field shenanigans, simply import the package that defined it, and put universally useful things into a package to be used universally.
Summary
Again, we had never seen a multi-runtime framework before, we did our best and it worked great! But now we know the constraints better and can simplify both internal and consuming code.
In the end a typical Remix app won't have more packages than it did before:
Most app code will continue to only need to import from two places:
@remix-run/react
remix
And then:
@remix-run/dev
as a dev dependency@remix-run/{adapter}
to set up a serverIf an app wants something specific to a runtime like
createKVSessionStorage
, they'll add it:@remix-run/cloudflare
I think this is totally worth it.
Upgrade path:
In v1 the runtime packages will re-export stuff from "remix", in v2 the won't. A codemod should be pretty easy too.
Beta Was this translation helpful? Give feedback.
All reactions