#monorepo setup

54 messages · Page 1 of 1 (latest)

shadow basalt
#

what's the recommended setup for working in a monorepo, given that TanStack Router uses module augmentation?

we have a monorepo with a composite setup, so each package emits its own .d.ts files. Now our setup is roughly like this:

- apps
  - app-one
    - routes
- libs
  - some-shared-lib

now app-one has the routes, the generated route-tree and the module augmentation of @tanstack/react-router. but inside libs/some-shared-lib, I would like to have a component that renders a <Link> from the router. As far as I can see, the module augmentation is not "seen" inside that lib, so I don't have type-safety for anything there.

How could we solve this?

shadow basalt
#

would it maybe work to have the generation output multiple routeTree.gen.ts files - one per package ?

nimble musk
#

We can discuss more here. Apart from the cyclic dependency that only imports the type. You could use paths. Maybe that will help because only TS knows about the dependency, not your bundler.

I was thinking about a separate package like packages/routes where you would run the generator and both the app and shared lib would import from it. You would need to fill in the component and loader with update in app. But I'm not sure if I like this solution either because you might end up pushing all your code into packages/routes in the end and I don't think it will pick up the loader types actually

nimble musk
#

Or we need some new concept in router. Like an abstract router but this might be beyond my API design skills

vital parrot
shadow basalt
#

I found both the answers on SO too, but they weren't really helpful :/

#

I also wanted to explore the separate package, but couldn't get it to work. The routeTree.gen also does module augmentation and it imports all the routes, so the shared package needs to know about all the route files, and likely even create the router instance itself and export it

vital parrot
#

why did both not work for you? can you share some more insights here?

vital parrot
#

can this be solved by generating different output in the router generator? if yes, what would you need?

shadow basalt
#

A coworker mentioned yesterday that paths can work and we'll talk about it today. If we get this done I'd like to add a file-based monorepo example to the docs

dreamy violet
#

That would be awesome. I had previously tinkered with using file-based routing in both the root app and the underlying lib, which "worked" (rendered the right components with correct nesting) except I wasn't able to figure out how to attach the route tree in the library to a branch of the route tree in the root, which meant the current URL was never updated, behavior was a bit odd, and there were type errors since the root app wasn't aware of the sub app routes.
Being able to import and attach route trees to existing route trees to extend them send like it should be possible, but I wasn't able to get the types to line up.

analog jetty
#

Did anyone ever figure out a viable, type-safe monorepo solution for TSR?

shadow basalt
#

Yes, I did 🙂 will try to write about it soon

mint crest
#

@shadow basalt Any progress on this? Or a working solution/example that you could share? We're facing the same issue and can't figure it out properly.

shadow basalt
#

Yes, there's an open source repro by @languid sonnet

mint crest
#

Thank you for sharing 🙏 I wanted to avoid this solution because of the giant routeMap - we have ~1000 routes so I don't want to import all the components and loaders in 1 place. You also lose the ability to click-through from the route definition directly to the component.

Another option would be for each domain/feature to register its own components 🤔 I'm just wondering if it's somehow possible for the route package to depend on the feature packages and provide the types back to them without circular dependencies... Could virtual routes help here or it doesn't change anything type-wise?

vital parrot
mint crest
#

@vital parrot I saw that one as well, but it's the same solution - define a skeleton router and dynamically inject all components etc, which doesn't really scale for hundreds of routes 🫤

shadow basalt
#

@mint crest do you have a better suggestion on how to avoid the circularity? I mean if you expect to have routes that render components where the components depend on the route definition, and you have that in different packages, that is a circularity you have to break.

my suggestion is to break it up between router and component: to create a router, with everything it needs to infer the types (paths, loaders, search param schemas, ...) but without components, then later go into your app (that needs to depend on everything anyways) and add the components, likely with lazy loading.

we have ~1000 routes so I don't want to import all the components and loaders in 1 place

why loaders? only components need to be imported imo.

And right now, this is the best trade-off I have to offer. If you know something better, I'd be happy to hear it 🙂

languid sonnet
#

If you want to trade flexibility for less type safety, you could imagine a plugin like architecture where you define the connected component to a bunch of route, defined in a plugin close to the feature libraries, and then from the list of plugins update the router, however this has a huge drawback: you could have a route without components, because you don't enforce route = component, and you will have no way of knowing it

mint crest
restive nest
#

We have a similar monorepo setup. Currently handling this by adding a “export routes” command to the package that contains the router. “Export routes” makes a type only build and outputs the types to the root of the monorepo. Then individual packages that need this add the root file to their includes config

#

Every package that includes the outputted types can just import tanstack router and have it work as expected.

sturdy frigate
#

@restive nest Do you mind sharing this setup with us?

#

@languid sonnet I had a look at your example repo, thank you for taking the time to create it! While i appreciate the effort, I think the setup introduces a lot of complexity, basically to please Typescript and have type-safe routes. Do you think this added complexity is a worthwhile trade-off? I’ve been wrestling with this problem for the past few days but haven’t been able to find an alternative solution.

sturdy frigate
#

I have the following setup:

- apps
  - app-one
    - routes (all routes are configured here + Vite Plugin which points to tree gen file in router lib)
      - dashboard.tsx (this route imports dashboard lib)
- libs
  - dashboard (imports router lib)
  - router (re-exports tanstack router and has module augmentation)

I created the router library purely to have type-safety and to export a custom Link component.

languid sonnet
# sturdy frigate I have the following setup: ``` - apps - app-one - routes (all routes are ...

This is like the other example I've contributed to the TanStack router docs https://tanstack.com/router/latest/docs/framework/react/examples/router-monorepo-simple

Basically, this works as it is, however if you need to share query options between the router and the dashboard lib in your example, that's where the data access kind of library make sense, or if you only use loader that works great

To answer the question above "do you think it's worth the trade off?" it depends on the size :
If you have few teams contributing to the monorepo, maybe not, and maybe that's fine for you to expose query options directly from the router lib, but if you start having 100+ contrib on the app / repo, then having the data access library make sense in terms of boundaries and contribution model, because at the end, a monorepo is a solution to a organisational issue

An example showing how to implement Router Monorepo Simple in React using TanStack Router.

sturdy frigate
#

I was actually more referring to the decision of having the routing definitions in one lib and the route mapping in the application, doesn't feel intuitive imo. So I was mostly questioning that trade-off, also you loose the ability of route splitting with this setup. You can ofc lazy load the components but things like pre-fetching when hovering a link don't work anymore.

#

The whole module augmentation thing has a huge impact on how we have to structure our mono-repo and I find that very limiting. Is this a TS limitation or TSR limitation?

languid sonnet
#

Maybe there is something we can do on the pre fetching of components on link hover when we use manually lazy in the monorepo route to component @vital parrot ?

vital parrot
languid sonnet
sturdy frigate
#

Well, it might be that I didn't configure it properly. This is what I did:

const MyComponent = React.lazy(() => import('./MyComponent'));

const routesMap = {
 "about": () => <Suspense placeholder="Loading..."><MyComponent /></<Suspense>
}

Do I have to use the .lazy suffix in the file name of the route and use createLazyFileRoute?

languid sonnet
# sturdy frigate Well, it might be that I didn't configure it properly. This is what I did: ``` c...

Hey, I've riff off a few ideas, and this is what I cam up with: https://github.com/TanStack/router/pull/3244

Feature libs expots a lazy route (createLazyRoute), that is then linked to the actual route on the end app, this supports lazy loading of pages with also pre loading with mouse hover intent

Cc @shadow basalt , I'm sure this will peak your interest, no more route.update in this case

GitHub

Key files to look at:

Where we plug the lazy routes with the routes
Example exposed route from a feature library
Re exported types and functions from the router to have type augmentation

shadow basalt
languid sonnet
shadow basalt
#

oooh, yeah that's nice

clever lava
remote rose
#

If I'm understanding correctly, to use with a monorepo setup we have to manually add each route to the routermap?

const routerMap = {
  '/': PostsListComponent,
  '/$postId': PostIdComponent,
  __root__: RootComponent,
} as const satisfies Record<RouterIds, () => React.ReactElement>

Object.entries(routerMap).forEach(([path, component]) => {
  const foundRoute = router.routesById[path as RouterIds]

  foundRoute.update({
    component: component,
  })
})
clever lava
#

Yes, that's the recommended way.

#

I really like TanStack Router, but this monorepo setup isn't good from the developer experience standpoint. I hope there will be a better way in the future

languid sonnet
#

I have a experiment with Nx sync generator to keep this map up to date based on lazy route exposed by featue libraries https://github.com/beaussan/tanstack-router-sync-experiment

I need to test it on a larger scale repo, but that removed one part of the setup

(based on https://nx.dev/concepts/sync-generators )

GitHub

Contribute to beaussan/tanstack-router-sync-experiment development by creating an account on GitHub.

Nx

Learn how to use Nx sync generators to maintain repository state and update configuration files based on the project graph before tasks are run.

red elbow
#

Hi im trying to setup a monorepo with tanstack/router and nestjs as backend as well.
The only thing im actually interested in is to have the routes shared between frontend and backend.
I thought about this setup

- apps
  - frontend
    - routes
  - backend
- libs
  - routes/routeTree

basically the frontend generates and consumes the routeTree from libs/routes and the backend can consume the routes just to have route autocompletion.
This way we can create emails/notifications etc in the backend on existing routes and we don't need to guess them.

sturdy frigate
languid sonnet
languid sonnet
sturdy frigate
languid sonnet
acoustic zodiac
#

if you think it's overengineering to use nx, then don't do a monorepo.

#

the moment you go monorepo, there are so many things you need to nail down and put gaurdrails around.