Skip to content

Conversation

@maiieul
Copy link
Contributor

@maiieul maiieul commented Sep 13, 2025

This PR contains:

  • bugfix
  • feature
  • refactor
  • documentation
  • other

Are tests included?

  • yes (bugfixes and features will not be merged without tests)
  • no

Breaking Changes?

  • yes (breaking changes will not be merged unless absolutely necessary)
  • no

List any relevant issue numbers:

Description

  • Unifies manualChunks object and function form APIs into the function form implementation. As such, removes the addManualChunks logic of the object form and related error messages and tests. I only removed the obvious code, maybe more can be cleaned up.
  • Allows passing regular expressions in object form.

The idea is that when the developer passes

    manualChunks: {
      vendor: ['lodash-es', 'clsx'],
    },

Rollup then converts it to

    manualChunks: (id) => {
      if (id.includes('lodash-es')) return 'vendor';
      if (id.includes('clsx')) return 'vendor';
    },

and then always uses the assignManualChunks logic, without the addStaticDependenciesToManualChunk merge logic.

This will also reduce the changes needed for #6087, and simplify the implementation of onlyExplicitManualChunks.

⚠️ Edit: This can lead to non-deterministic behavior beucase of the .includes. See closing comment below.

@vercel
Copy link

vercel bot commented Sep 13, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
rollup Ready Ready Preview Comment Sep 13, 2025 6:28pm

private hasLoggedEffect = false;

hasCachedEffects(): boolean {
if (!this.included) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This was re-added in https://github.com/rollup/rollup/pull/4921/files to solve #4908. This solved the error message but also introduced a test that I think is incorrect. Discussing below on the test itself.

module.exports = defineTest({
description: 'handles manual chunks where the root is not part of the module graph',
description:
'does not output a bundle for the manual chunks that are not part of the module graph',
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 don't think this counts as a breaking change.

With the behavior after #4921, Rollup will artificially output a bundle for the manualChunks pattern provided if it was not in the module graph, but it won't be used by any bundle, unless Rollup merges some dependencies into it, which was the case in this test.

@@ -1 +1,3 @@
import './manual';
Copy link
Contributor Author

Choose a reason for hiding this comment

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

makes sure that manual.js is part of the module graph

@@ -1 +1,3 @@
import './manual';
Copy link
Contributor Author

Choose a reason for hiding this comment

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

makes sure that manual.js is part of the module graph

},
generateError: {
code: 'INVALID_CHUNK',
message: 'Cannot assign "dep.js" to the "dep2" chunk as it is already in the "dep1" chunk.'
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This won't happen anymore. I removed the related log error message code.

},
generateError: {
code: 'EXTERNAL_MODULES_CANNOT_BE_INCLUDED_IN_MANUAL_CHUNKS',
message: `"${externalModule}" cannot be included in manualChunks because it is resolved as an external module by the "external" option or plugins.`
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This won't happen anymore. I removed the related log error message code.

},
generateError: {
code: 'EXTERNAL_MODULES_CANNOT_BE_TRANSFORMED_TO_MODULES',
message: `${externalModule} is resolved as a module now, but it was an external module before. Please check whether there are conflicts in your Rollup options "external" and "manualChunks", manualChunks cannot include external modules.`
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This won't happen anymore. I removed the related log error message code.

@maiieul maiieul force-pushed the unify-manualChunks-impl-plus-support-obj-regexps branch from ffe5ca7 to e8682a5 Compare September 13, 2025 18:25
@maiieul maiieul force-pushed the unify-manualChunks-impl-plus-support-obj-regexps branch from e8682a5 to 9c83296 Compare September 13, 2025 18:26
```

will put all lodash modules into a manual chunk even if you are only using imports of the form `import get from 'lodash/get'`.
will put all lodash modules into a `lodash` chunk.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed the note about modules not needing to be part of the module graph since this is no longer true. See the manual-chunk-not-included test below.

The PR doesn't affect the behavior outlined in the docs though. I tested on a repro with lodash using import get from 'lodash/get' and the modules were properly grouped together into the lodash chunk. So I guess the statement wasn't really accurate to begin with.

@maiieul maiieul marked this pull request as draft September 14, 2025 09:42
@maiieul
Copy link
Contributor Author

maiieul commented Sep 16, 2025

Closing, because this approach can lead to non-deterministic behavior.

It turns out that

    manualChunks: {
      vendor: ['lodash-es', 'clsx'],
    },

and

    manualChunks: (id) => {
      if (id.includes('lodash-es')) return 'vendor';
      if (id.includes('clsx')) return 'vendor';
    },

are not the same.

In basic use cases such as in this example it is the same and unlikely to pose a problem, but imagine that the developer passes vendor: [..., 'utils']; then that would be converted to if (id.includes('utils')) return 'vendor';, which would put all modules that include 'utils' in their path into the same chunk. The object form on the other hand would create an entry for the utils file if it can resolve it, and add it to the manualChunk alias.

We cannot use endsWith or similar because the id might not end with the pattern exactly (e.g. with lodash-es it is /.../node_modules/lodash-es/lodash.js, etc.)

I tried to play with some ways to solve the problem, but it seems the best way to be deterministic with the object form is to be through the current logic of addManualChunks -> getChunkAssignments -> getChunkDefinitionsFromManualChunks -> addStaticDependenciesToManualChunk.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

'manualChunks' gives inconsistent results for object and function

1 participant