Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Vite: build errors when non-server-only exports contain server-only code #7924

Closed
1 task done
pcattori opened this issue Nov 6, 2023 · 7 comments
Closed
1 task done

Comments

@pcattori
Copy link
Contributor

pcattori commented Nov 6, 2023

What version of Remix are you using?

2.2.0

Are all your remix dependencies & dev-dependencies using the same version?

  • Yes

Steps to Reproduce

Create a route with a handle that includes a function that relies on server-only code:

import { doServerStuff } from "~/do-server-stuff.server.ts"

export const handle = () => {
  value: 1,
  onlyCallThisFromServer: () => doServerStuff("wow")
}

In the client build, do-server-stuff.server.ts will be an empty module, but handle won't get treeshaken nor DCE'd. And it shouldn't be! value may still be used/needed for the client build.

Expected Behavior

There's some way to include server-only functions within handle without client build failing.

Actual Behavior

In the client build, do-server-stuff.server.ts will be an empty module, but handle won't get treeshaken nor DCE'd. And it shouldn't be! value may still be used/needed for the client build.

But this causes client build to fail with an error that do-server-stuff.server doesn't export doServerStuff.

@pcattori
Copy link
Contributor Author

pcattori commented Nov 6, 2023

A workaround might be to use Vite's import.meta.env.SSR:

import { doServerStuff } from "~/do-server-stuff.server.ts"

export const handle = () => {
  value: 1,
  onlyCallThisFromServer: import.meta.env.SSR ? () => doServerStuff("wow") : null,
}

But haven't tested if the SSR part of the ternary gets DCE'd by Vite or not at compile time.
If not, you could use dynamic imports:

export const handle = () => {
  value: 1,
  onlyCallThisFromServer: () => {
    if (!import.meta.env.SSR) throw Error("tried calling server-only function from client")
    let {doServerStuff} = await import(/* vite-ignore */ "~/do-server-stuff.server.ts")
    return doServerStuff("wow")
  }
}

@kentcdodds
Copy link
Member

I tested both of these and they did not work for me. I'm still getting:

[vite]: Rollup failed to resolve import "~/utils/mdx.server.tsx" from "/Users/kentcdodds/code/kentcdodds.com/app/routes/blog_+/$slug.tsx".
This is most likely unintended because it can break your application at runtime.
If you do want to externalize this module explicitly add it to
`build.rollupOptions.external`
error during build:
Error: [vite]: Rollup failed to resolve import "~/utils/mdx.server.tsx" from "/Users/kentcdodds/code/kentcdodds.com/app/routes/blog_+/$slug.tsx".
This is most likely unintended because it can break your application at runtime.
If you do want to externalize this module explicitly add it to
`build.rollupOptions.external`
    at viteWarn (file:///Users/kentcdodds/code/kentcdodds.com/node_modules/vite/dist/node/chunks/dep-bb8a8339.js:48216:27)
    at onRollupWarning (file:///Users/kentcdodds/code/kentcdodds.com/node_modules/vite/dist/node/chunks/dep-bb8a8339.js:48248:9)

@hi-ogawa
Copy link
Contributor

hi-ogawa commented Nov 7, 2023

I commented something similar previously in #7864 (comment).
Currently server only files are transformed to export default {}, so during dev, the import statement is "statically" rejected by browsers:

import {doServerStuff} from "/app/server-only.server.ts"
//  Uncaught SyntaxError: The requested module '/app/server-only.server.ts' does not provide an export named 'doServerStuff' (at test1.tsx:2:10)

So, in order to "cheat" this static error, I suggested this as a workaround:

import * as serverLib from '../server-only.server';

export const handle = {
  value: 1,
   // runtime error if it called on client since `serverLib = { default: {} }`
  onlyCallThisFromServer: () => serverLib.doServerStuff('wow'),
};

// during `vite build`, it shows warning, but application seems to be functional
//     app/routes/test2.tsx (6:42) "doServerStuff" is not exported by "app/server-only.server.ts", imported by "app/routes/test2.tsx".

I made this reproduction on stackblitz:
https://stackblitz.com/edit/remix-run-remix-dd56g3?file=app%2Froutes%2Ftest1.tsx
(Btw, I recently learned that stackblitz can run vite app from github, for example, by https://stackblitz.com/fork/github/remix-run/remix/tree/main/templates/unstable-vite which is so cool!)

I think import.meta.env.SSR based tree-shaking should work but only for vite build, so during dev, some workaround is probably required to support this use case.

I'm not sure if it's possible to expand remix's "remix-remove-server-exports" plugin to achieve this somehow, but I think another direction of the fix would be to improve "remix-empty-server-modules" plugin to do be aware of named exports.
For example, instead of emptifying to export default {}, it could analyze exported names and transform it to export const doServerStuff = {} etc...

I had this idea before and I made a simple vite plugin with es-module-lexer here, but I haven't tested with remix plugin.
https://github.com/hi-ogawa/vite-plugins/blob/main/packages/vite-null-export

It might not work with remix plugin directly since it has some intricate pre/post phases.
Also another concern was that it might expose the export name doServerStuff on client bundle, but this may be mitigated by switching back to export default {} for vite build.

@alicanerdogan
Copy link

I think this is the same issue: #7931

@pcattori pcattori changed the title Vite: build errors when handle contains server-only code Vite: build errors when non-server-only exports contain server-only code Nov 7, 2023
@na2hiro
Copy link
Contributor

na2hiro commented Nov 10, 2023

Similar tree-shaking related issue happens for client-only code as well: #7972

@pcattori
Copy link
Contributor Author

pcattori commented Nov 30, 2023

@kentcdodds I think the issue you're having is that ~/utils/mdx.server.tsx indeed does not exist. Should be ~/utils/mdx.server.ts (ts not tsx). If I make that change in your code, that specific error goes away and I can proceed to migrate the rest of the app.

@pcattori
Copy link
Contributor Author

pcattori commented Dec 2, 2023

Addressed by #8198


For onlookers, there are two separate cases that cause server-only code issues like this:

  1. User-defined route exports that reference server-only code (Vite: User-define route export referencing server-only code not working #7864)
  2. Referencing server-only code from client-safe route exports like handle (this issue)

Vite forced our hand to be stricter about what exports can be allowed for Remix routes, so user-defined exports from Remix routes are now considered invalid at build time, addressing (1). See #7864 for more details on that.

For (2), you can use dynamic exports as an escape hatch. For example, with handle, you can create an entry that is a function you'll only call on the server that dynamically imports server-only dependencies (test the demonstrates this #8198). This isn't something the vast majority of users will need, but its there if you need it. Just be careful, its a sharp knife.

@pcattori pcattori closed this as completed Dec 2, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants