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

feat: enable dependencies discovery and pre-bundling in ssr environments #18358

Merged
merged 4 commits into from
Oct 23, 2024

Conversation

dario-piotrowicz
Copy link
Contributor

@dario-piotrowicz dario-piotrowicz commented Oct 15, 2024

Description

This PR adds pre-bundling support for server environments

Copy link

stackblitz bot commented Oct 15, 2024

Review PR in StackBlitz Codeflow Run & review this pull request in StackBlitz Codeflow.

@dario-piotrowicz dario-piotrowicz force-pushed the dario/ssr-noDiscovery-false branch from 5e9488e to 23d6d9d Compare October 15, 2024 21:16
@dario-piotrowicz

This comment was marked as off-topic.

@dario-piotrowicz

This comment was marked as outdated.

@dario-piotrowicz dario-piotrowicz force-pushed the dario/ssr-noDiscovery-false branch from 431d438 to ec7f400 Compare October 17, 2024 09:43
Comment on lines 119 to 126
if (
!environment.config.dev.optimizeDeps.noDiscovery &&
!environment.config.dev.optimizeDeps.exclude?.includes(id)
) {
// If there is server side pre-bundling and the module is not
// in the `exclude` config then it is not external
return false
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one is interesting. We were trying to avoid conflating optimizeDeps.include/exclude with external/noExternal in the past but we will need this check for noDiscovery to make sense. This condition should be move to line 140 though.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, I understand trying not to conflate them, although it does feel like these concepts start to overlap here 🤔
I hope this is not a huge concern but yeah it does feel like this check is needed since we do not want prebundled modules to be run through runExternalModule

In any case I've moved the check 🙂👍

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If a dependency is marked as external, I guess we should not optimized it even if it is not excluded from optimizeDeps. I mean dropping this whole if block so that only no-externalized files would be pre-bundled.

Otherwise, for the following dependency graph:

  • app -> pkg-a (external: true, not excluded) -> pkg-b
  • app -> pkg-c (external: true, manually excluded) -> pkg-b

pkg-a will be prebundled with pkg-b, and pkg-a will use the bundled pkg-b. But pkg-c will use the non-bundled pkg-b because pkg-c uses a normal import and Vite cannot intercept that. This means there will be a problem similar to dual package hazard.
I think this is difficult to understand by users.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pkg-a will be prebundled with pkg-b, and pkg-a will use the bundled pkg-b. But pkg-c will use the non-bundled pkg-b because pkg-c uses a normal import and Vite cannot intercept that. This means there will be a problem similar to dual package hazard.

Yes I understand this issue, although I imagine that you can always end up in similar situations even today (without environments) if you have multiple packages with the same dependency and exclude one of them, no?
I think this boils down to trusting users to provide correct/sensible configs, if someone does try to use more advanced features such as optimizeDeps they should be able to understand the potential risks and implications I think? 🤔

Anyways I don't fully understand your suggestion, dropping this if block means that all the external modules will not be pre-bundled, doesn't it? but that's the whole point of this PR; Allowing environments to get opt-in externals pre-bundling without having to specify all the various dependencies to pre-bundle (since that's something we can do already today).
So I'm not sure what your suggestion is, unless I am missing something?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes I understand this issue, although I imagine that you can always end up in similar situations even today (without environments) if you have multiple packages with the same dependency and exclude one of them, no?

Oh, yes, it will happen. But the difference is that we can fix it in future for non-externalized deps since Vite can intercept them and that's not possible for externalized deps. If we add support for prebundling externalized deps, we cannot fix that issue for them. (That said, I'm not so much against keeping this block because I don't think it will be necessarily fixed.)

dropping this if block means that all the external modules will not be pre-bundled, doesn't it?

Yes.

but that's the whole point of this PR; Allowing environments to get opt-in externals pre-bundling without having to specify all the various dependencies to pre-bundle (since that's something we can do already today).

I thought the point was to make noDiscovery: false work in SSR and allow users / environments to enable it. That would make no-externalized CJS deps work in dev (It should be working in build already). Given that all files has to be bundled for a output for workerd, I thought the ideal way is to set noExternal: true (also set external: ['buffer' /* list supported node and cloudflare builtins */] if supported) so that all files will be processed by Vite in both dev and build. Do you have any other reason to avoid ssr.noExternal: true other than this CJS dep problem?

Copy link
Contributor Author

@dario-piotrowicz dario-piotrowicz Oct 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sapphi-red sorry I simply didn't consider noExternal: true + external: [...], it does seems like a valid solution to me, hoping that the maintenance of the external list doesn't become too cumbersome 🤔

Yes I agree that this is probably the most correct solution 👍

But, do you think that this could be confusing/easy to get wrong from a user's perspective?
That just setting noDiscovery: false is not enough but you also need to set noExternal: true for it to take effect (especially considering that this is not required for client environments)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(PS: I've removed the if block 👍)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hoping that the maintenance of the external list doesn't become too cumbersome 🤔

Looking at it again, it seems to work without setting external.

if (currentEnvironmentOptions.consumer === 'server') {
if (
options.webCompatible &&
options.noExternal === true &&
// if both noExternal and external are true, noExternal will take the higher priority and bundle it.
// only if the id is explicitly listed in external, we will externalize it and skip this error.
(options.external === true || !options.external.includes(id))
) {
let message = `Cannot bundle Node.js built-in "${id}"`
if (importer) {
message += ` imported from "${path.relative(
process.cwd(),
importer,
)}"`
}
message += `. Consider disabling environments.${environmentName}.noExternal or remove the built-in dependency.`
this.error(message)
}
return options.idOnly
? id
: { id, external: true, moduleSideEffects: false }

Maybe It just works only with noExternal: true.

But, do you think that this could be confusing/easy to get wrong from a user's perspective?
That just setting noDiscovery: false is not enough but you also need to set noExternal: true for it to take effect

If the runtime does not support running that module natively, then the module should be no-externalized. In other words, if that module can run natively, then it should work without pre-bundle. So I think the user would add the module to noExternal.

In future, I guess we'd want to set noDiscovery: false by default.

@dario-piotrowicz dario-piotrowicz marked this pull request as ready for review October 18, 2024 09:56
@patak-dev
Copy link
Member

/ecosystem-ci run

@vite-ecosystem-ci
Copy link

patak-dev
patak-dev previously approved these changes Oct 18, 2024
Copy link
Member

@patak-dev patak-dev left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are less reds because the PR isn't based on main, and there were some changes lately.

Thanks for working this out @dario-piotrowicz! Let's wait for another approval before we merge this. I think it would be useful to move forward to let environments experiment with this

Comment on lines 119 to 126
if (
!environment.config.dev.optimizeDeps.noDiscovery &&
!environment.config.dev.optimizeDeps.exclude?.includes(id)
) {
// If there is server side pre-bundling and the module is not
// in the `exclude` config then it is not external
return false
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If a dependency is marked as external, I guess we should not optimized it even if it is not excluded from optimizeDeps. I mean dropping this whole if block so that only no-externalized files would be pre-bundled.

Otherwise, for the following dependency graph:

  • app -> pkg-a (external: true, not excluded) -> pkg-b
  • app -> pkg-c (external: true, manually excluded) -> pkg-b

pkg-a will be prebundled with pkg-b, and pkg-a will use the bundled pkg-b. But pkg-c will use the non-bundled pkg-b because pkg-c uses a normal import and Vite cannot intercept that. This means there will be a problem similar to dual package hazard.
I think this is difficult to understand by users.

@sapphi-red sapphi-red added feat: environment API Vite Environment API p3-significant High priority enhancement (priority) labels Oct 21, 2024
@hi-ogawa
Copy link
Collaborator

Awesome work!
I was worrying about what happens when discovery new dep during request handling due to unscanned dynamic import, but it looks like this is not so likely if optimizeDeps.entries are configured well. Even if this happens, it doesn't look like a huge problem either since current module can keep handling requests though it can potentially cause dual modules due to old and new pre-bundled dep. I made a bit contrived example using virtual module to see it in action hi-ogawa@c655057, but I don't think this is a blocker for merging this.

@patak-dev
Copy link
Member

Awesome work! I was worrying about what happens when discovery new dep during request handling due to unscanned dynamic import, but it looks like this is not so likely if optimizeDeps.entries are configured well. Even if this happens, it doesn't look like a huge problem either since current module can keep handling requests though it can potentially cause dual modules due to old and new pre-bundled dep. I made a bit contrived example using virtual module to see it in action hi-ogawa@c655057, but I don't think this is a blocker for merging this.

Wouldn't this be equivalent to an edit event where a user added a new import in a file that caused a full-reload? This should work or we have a bug, no?

@hi-ogawa
Copy link
Collaborator

hi-ogawa commented Oct 21, 2024

Wouldn't this be equivalent to an edit event where a user added a new import in a file that caused a full-reload? This should work or we have a bug, no?

I think the manual edit case is different and it should work fine since request handling starts from importing a fresh entry module. My demo is to show what happens when there is re-optimization in the middle of request handling.

@patak-dev
Copy link
Member

Ah, you're right. In the browser, it doesn't matter because the full-reload would end up doing the request again so the modules being transformed can be discarded (we throw an outdated request exception in this case in the vite:optimized-deps plugin). We catch this in the transform middleware and send a 504. I don't know what would be the equivalent when using the server. A framework could catch the exception and issue a full-reload in the client if there isn't one in fly? (cc @sheremet-va, maybe you have some thoughts about this)

@dario-piotrowicz dario-piotrowicz force-pushed the dario/ssr-noDiscovery-false branch 2 times, most recently from 92f90b5 to 3d8d634 Compare October 21, 2024 12:24
@dario-piotrowicz dario-piotrowicz force-pushed the dario/ssr-noDiscovery-false branch from 3d8d634 to 2c7afed Compare October 21, 2024 12:27
@dario-piotrowicz
Copy link
Contributor Author

(PS: I had to rebase because of a merge conflict, so since I was already at it I squashed all my commits, sorry if this causes any inconvenience in reviewing the PR 🙇)

dario-piotrowicz and others added 2 commits October 22, 2024 09:33
set `scan: true` instead of adding relative prefix in `resolvePath`

Co-authored-by: 翠 / green <[email protected]>
Copy link
Member

@sapphi-red sapphi-red left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

@sheremet-va
Copy link
Member

sheremet-va commented Oct 22, 2024

Ah, you're right. In the browser, it doesn't matter because the full-reload would end up doing the request again so the modules being transformed can be discarded (we throw an outdated request exception in this case in the vite:optimized-deps plugin). We catch this in the transform middleware and send a 504. I don't know what would be the equivalent when using the server. A framework could catch the exception and issue a full-reload in the client if there isn't one in fly? (cc @sheremet-va, maybe you have some thoughts about this)

I wasn't able to make this work in Vitest because there is no mechanism for retrying to load a file again in tinypool. Maybe this is something that could be added to make a reloading work in environments like Vitest, @AriPerkkio? (Like restarting a worker)

For regular environments, I guess they would have to handle this themselves? I think loading modules in the same global scope has the same side effects as it does in the browser. So, if you use an external process, you can just reload it, but if you are running in the same process (like with the default ssr), what do you do? We can reset the cache of inlined modules, but we can't reset the cache of external modules. We also cannot clean up their side effects.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feat: environment API Vite Environment API p3-significant High priority enhancement (priority)
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants