T
TanStackâ€Ē7mo ago
deep-jade

Run start index.mjs on subpath

I have an existing express app and i want to mount the tanstack/start index.mjs on a subroute of that app. Even when setting a basepath in the router or a baseURL in app.config.ts the nitro server also responds on http://localhost:3000. The routes are correctly only available on http://localhost:3000/hub but as long as it registeres on / i can't run my existing app next to it. How can i configure tanstack start accordingly?
29 Replies
like-gold
like-goldâ€Ē7mo ago
you probably need to proxy from the express app to the start app? since only one server can bind to a single port
deep-jade
deep-jadeOPâ€Ē7mo ago
Yes that would also work 🙂 I got it experimentally working by using the "node" instead of the "node-server" preset. With "node-server" and your suggestion we would have to run a separate node server and that would cause more devops/monitoring work in production. With "node" preset, nitro exposes a handler which we can register into the existing express app like this:
app.all('/subpath-for-tanstack-start-app-only/*', (req, res, next) => {
importedIndexMjsFileBuiltWithNodePreset.handler(req, res, (err) => {....
app.all('/subpath-for-tanstack-start-app-only/*', (req, res, next) => {
importedIndexMjsFileBuiltWithNodePreset.handler(req, res, (err) => {....
deep-jade
deep-jadeOPâ€Ē7mo ago
Nitro logs that this is experimental. Do you see any issues with this approach from the tanstack side? https://nitro.build/deploy/runtimes/node#handler-advanced
Node.js - Nitro
Run Nitro apps with Node.js runtime.
like-gold
like-goldâ€Ē7mo ago
no, that's a good solution!
deep-jade
deep-jadeOPâ€Ē7mo ago
Wonderful thank you 🙂 I am running into some issues with this now. I have tried a lot of different configs but I am no longer sure how to resolve this properly. If you have any suggestions on the top of your head that would be appreciated. Otherwise I will continue to play around, I am fairly sure that I can figure this out with enough time investment. I am using "node" preset and have basepath: "/hub", in router.tsx In my express app the resulting index.mjs is included like this:
app.all('/hub/*', (req, res, next) => {
commuteHubServer.handler(req, res, (err) => {
if (err) {
log.error('Nitro handler error:', err);
return next(err);
}
if (!res.writableEnded) {
log.debug('Nitro did not end response, restoring URL and passing to next');
return next();
}
});
});
app.all('/hub/*', (req, res, next) => {
commuteHubServer.handler(req, res, (err) => {
if (err) {
log.error('Nitro handler error:', err);
return next(err);
}
if (!res.writableEnded) {
log.debug('Nitro did not end response, restoring URL and passing to next');
return next();
}
});
});
My issue is that nitro.mjs tries to preload like this: preloads: ["/_build/assets/client-CKkrWiz9.js", "/_build/assets/client-ikYd-yxj.js"]` resulting in this request which results in a 404: http://localhost:3000/_build/assets/client-CKkrWiz9.js I can modify the path that is used in preloads by adding to the app.config.ts
routers: {
client: {
base: "/hub",
},
},
routers: {
client: {
base: "/hub",
},
},
But so far no luck in configuring this correctly 😄
like-gold
like-goldâ€Ē7mo ago
what happens if you configure it like this?
deep-jade
deep-jadeOPâ€Ē7mo ago
if i do that it just changes the subfolder structure so not helpful at all. .output/public/_build/assets --> .output/public/hub/assets I kinda hoped that app.config server.baseURL: "/hub" would do the trick but it does not appear to change how the chunks are preloaded. i am still trying to understand how server.baseURL relates to router.tsx basepath. maybe a combination of the 2 will do the trick ok so even if i set baseURL: "/TESTING1asdf", the first reuqest request will be http://localhost:3000/_build/assets/client-gJCxyI7W.js but then it retries with http://localhost:3000/TESTING1asdf/_build/assets/client-gJCxyI7W.js Right now both fail but that might be due to my server setup. i think i need to serve the public directory separately. While the request for my app.css for example is http://localhost:3000/TESTING1asdf/_build/assets/app-TBb7edEX.css right away
like-gold
like-goldâ€Ē7mo ago
if you can provide a minimal reproducer as a git repo I can have a look later
deep-jade
deep-jadeOPâ€Ē7mo ago
i will try some more things as the setup is pretty overwhelming. But it might be best anyway to start with a basic starter and try to start that at a subpath 😄
like-gold
like-goldâ€Ē7mo ago
yes that's best it's absolutely possible we need to honor the base path inside of start somewhere not a common thing so might have gone unnoticed
deep-jade
deep-jadeOPâ€Ē7mo ago
I created a repo with some testing notes and instructions: https://github.com/flodaniel/tss-subdirectory-hosting-node i also found a very dirty workaround
GitHub
GitHub - flodaniel/tss-subdirectory-hosting-node
Contribute to flodaniel/tss-subdirectory-hosting-node development by creating an account on GitHub.
deep-jade
deep-jadeOPâ€Ē7mo ago
The workaround is not something i would be comfortable with putting into production so I am looking forward to your thoughts and I will continue to work on this on monday most likely 🙂
like-gold
like-goldâ€Ē7mo ago
it looks like this nitro integration does not serve static files. i asked nitro for clarification
deep-jade
deep-jadeOPâ€Ē7mo ago
Thank you! I hope this is something that could be added or we can find a cleaner workaround that is stable in production 👍 @Manuel Schiller did you create a github issue somewhere in the nitro repo or did you reach out to them directly? 🙂
like-gold
like-goldâ€Ē7mo ago
directly no response yet
deep-jade
deep-jadeOPâ€Ē7mo ago
okidoki and thanks for the update 🙂 Did you hear back from them? We are inching closer to wanting to release TSS to production and this is one of the biggest last pieces for us right now 😄
like-gold
like-goldâ€Ē7mo ago
unfortunately nope ok got the reply that serveStatic is default on for that preset. might want to check if this is changed somewhere (e.g. vinxi)?
deep-jade
deep-jadeOPâ€Ē7mo ago
i can check that yes 🙂 Is that the actual problem though because without any basepath set (so just hosting it a / ) works? i think it is more of a path usage issue (so that the basepath in createTanStackRouter is not used correctly)? Do you know why i have to set app.config.ts server.baseURL as well as router.tsx createTanstackRouter basepath or what the difference is between the two?
deep-jade
deep-jadeOPâ€Ē7mo ago
so this is how my folder structure looks like when deployed but the index.mjs looks for the preload chunks in the frontend folder which is / for it
No description
deep-jade
deep-jadeOPâ€Ē7mo ago
is it possible that i can't have TSR configured to run on subpath /hub but then also have the static assets be served from /hub?
deep-jade
deep-jadeOPâ€Ē7mo ago
i am just thinking loud here now 😄 with server.baseUrl it attempts to get the chunks first from / and then from /hub. It does not do this with no server.baseURL is set. However on /hub (http://localhost:3001/hub/_build/assets/client-DkXsmdUL.js) TSR responds instead of actually doing a static serve of the asset. I could change server.baseURL but I think then i would have to serve the app twice, once for static assets and once normally 😄
No description
deep-jade
deep-jadeOPâ€Ē7mo ago
I am looking at react-start-config which uses the clientBase to construct the URL for preload but ignores the baseURL set from server.baseURL https://github.com/TanStack/router/blob/0cf5e3e84695878578c2ed17d7fd2fdec4a04433/packages/react-start-config/src/index.ts#L589
GitHub
router/packages/react-start-config/src/index.ts at 0cf5e3e846958785...
ðŸĪ– Fully typesafe Router for React (and friends) w/ built-in caching, 1st class search-param APIs, client-side cache integration and isomorphic rendering. - TanStack/router
deep-jade
deep-jadeOPâ€Ē7mo ago
I lack some overall understanding but do you think the publicBase, clientBase, apiBase, serverBase should use the server.baseURL as a prefix? Currently not even sure if this would resolve my issue 😄
like-gold
like-goldâ€Ē7mo ago
maybe? just try it out ðŸĪŠ
deep-jade
deep-jadeOPâ€Ē7mo ago
ok that caused the first requests (screenshot above) to also include the baseUrl but ofc it still results in a 404, as the last request already tries to do that anyway. https://github.com/nitrojs/nitro/blob/v2/src/presets/node/preset.ts "node" does not come with serveStatic:true. In addition, other deployments had bugfixes to respect baseURL (vercel, cloudflare) I need to dig deeper what serveStatic does and if useNitroApp or toNodeListener(from h3) respect the baseURl option
deep-jade
deep-jadeOPâ€Ē7mo ago
server: {
preset: "node",
$production: {
baseURL: "/hub",
},
serveStatic: true,
},
server: {
preset: "node",
$production: {
baseURL: "/hub",
},
serveStatic: true,
},
Ok i got it working by enabling serveStatic and setting baseURL only in production, otherwise local development gets messed up. So the solution was much simpler. I now serve the nitro app like this:
app.all(['/hub', '/hub/*'], (req, res, next) => {
commuteHubServer.l(req, res, (err) => {
if (err) {
log.error('Nitro handler error:', err);
return next(err);
}
if (!res.writableEnded) {
log.debug('Nitro did not end response, restoring URL and passing to next');
return next();
}
});
});
app.all(['/hub', '/hub/*'], (req, res, next) => {
commuteHubServer.l(req, res, (err) => {
if (err) {
log.error('Nitro handler error:', err);
return next(err);
}
if (!res.writableEnded) {
log.debug('Nitro did not end response, restoring URL and passing to next');
return next();
}
});
});
Unfortunately there are still 2 initially failed requests. Looks like TSR has some kind of fallback already and then tries again with the baseURL attached or sth like that.
No description
deep-jade
deep-jadeOPâ€Ē7mo ago
well
$production: {
baseURL: "/hub",
},
$production: {
baseURL: "/hub",
},
actually does not work, i have to set it directly on the server object, which makes the build .output work but breaks the local development server.. Only way i found to make this adaptable is baseURL: process.env.BASE_URL,. So overall the out-of-the-box support for "node" preset on a subpath could be improved, but ofc this is a edge case 😄 also the baseURL is not applied to head links, which it definitely should :/
like-gold
like-goldâ€Ē7mo ago
can you create a PR for this?
deep-jade
deep-jadeOPâ€Ē7mo ago
I can try 😄 In a test like this where can i add options like in the createTanstackRouter method? Also would you say the baseURL should be added based on the nitro server prop or the TSR basepath?
test('links w/baseURL', async () => {
const rootRoute = createRootRoute({
head: () => ({
links: [{ href: 'root.css' }, { href: 'root2.css' }],
}),
})
const indexRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/',
head: () => ({
links: [{ href: 'index.css' }],
}),
component: () => <div>Index</div>,
})
const routeTree = rootRoute.addChildren([indexRoute])
const router = createRouter({ routeTree, })
render(<RouterProvider router={router} />)
const indexElem = await screen.findByText('Index')
expect(indexElem).toBeInTheDocument()

const linksState = router.state.matches.map((m) => m.links)
expect(linksState).toEqual([
[{ href: 'base/root.css' }, { href: 'base/root2.css' }],
[{ href: 'base/index.css' }],
])
})
test('links w/baseURL', async () => {
const rootRoute = createRootRoute({
head: () => ({
links: [{ href: 'root.css' }, { href: 'root2.css' }],
}),
})
const indexRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/',
head: () => ({
links: [{ href: 'index.css' }],
}),
component: () => <div>Index</div>,
})
const routeTree = rootRoute.addChildren([indexRoute])
const router = createRouter({ routeTree, })
render(<RouterProvider router={router} />)
const indexElem = await screen.findByText('Index')
expect(indexElem).toBeInTheDocument()

const linksState = router.state.matches.map((m) => m.links)
expect(linksState).toEqual([
[{ href: 'base/root.css' }, { href: 'base/root2.css' }],
[{ href: 'base/index.css' }],
])
})

Did you find this page helpful?