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

fix(css): ensure order of extracted CSS #16588

Merged
merged 2 commits into from
Jun 10, 2024

Conversation

susnux
Copy link
Contributor

@susnux susnux commented May 3, 2024

Description

Without this the order of the extracted CSS rules is defined by the order renderChunk of the css plugin is called. So with this the order of CSS rules is always in alphabetical order of the output chunks.

Previously on my project every time I ran the build the output style.css was different by rule order, which caused unnecessary conflicts.

Copy link

stackblitz bot commented May 3, 2024

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

packages/vite/src/node/plugins/css.ts Outdated Show resolved Hide resolved
@susnux susnux force-pushed the fix/deterministic-styles branch 2 times, most recently from 92f9c5a to 9f584f1 Compare May 15, 2024 08:34
@susnux susnux requested a review from bluwy May 15, 2024 08:34
bluwy
bluwy previously approved these changes May 28, 2024
Copy link
Member

@bluwy bluwy left a comment

Choose a reason for hiding this comment

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

Seems like the best fix for now. If the bundle order is non-deterministic, it would be a bug in Rollup, otherwise it should work consistently now.

For other reviewers, from what I understand chunkCSSMap (and the renderChunk hook that sets it) is non-deterministic because Rollup calls renderChunk for all chunks in parallel. If some hook before the CSS plugin does async work with various timing, the calls would be affected as well.

Comment on lines 907 to 909
chunk.dynamicImports.forEach((importName) =>
collect(bundle[importName]),
)
Copy link
Member

Choose a reason for hiding this comment

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

Isn't this change unrelated to ordering? Wouldn't this mean that we'll be adding CSS of lazy loaded chunks into the parent chunks? 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No this is only about cssCodeSplit: false. For splitted css the CSS assets are emitted in the renderChunk hook.
So this needs to include all styles.

Copy link
Member

Choose a reason for hiding this comment

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

Hmm even for cssCodeSplit: false, I think it may make sense to remove this too. When iterating the bundle chunks, we don't want to eagerly crawl the dynamic imports and concatenate its CSS. Dynamic chunks styles should be ordered last so it has a higher priority.

Removing this should also be safe because we iterate chunks with isDynamicEntry: true, which will cover all these dynamic imported chunks.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Tested it right now, removing makes it non deterministic again, because the order of dynamic entries / dynamic imports in the bundle is not always the same.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Dynamic chunks styles should be ordered last so it has a higher priority.

So a working (tests) alternative proposal:
Do not include .isDynamicEntry in the main loop, but move the chunk.dynamicImports.forEach to the end.

Because the import order is deterministic, but the output order of dynamic entry chunks is not deterministic in the bundle.

Meaning it will look like this:

        function collect(chunk: OutputChunk | OutputAsset) {
          if (!chunk || chunk.type !== 'chunk' || collected.has(chunk)) return
          collected.add(chunk)

          // First collect all styles from the synchronous imports (lowest priority)
          chunk.imports.forEach((importName) => collect(bundle[importName]))
          // Then collect the styles of the current chunk (might overwrite some styles from previous imports)
          css += chunkCSSMap.get(chunk.preliminaryFileName) ?? ''
          // Finally collect the styles of dynamic imports (they have the highest priority and might overwrite other styles)
          chunk.dynamicImports.forEach((importName) =>
            collect(bundle[importName]),
          )
        }

        // The bundle is guaranteed to be deterministic, if not then we have a bug in rollup.
        // So we use it to ensure a deterministic order of styles
        for (const chunk of Object.values(bundle)) {
          if (chunk.type === 'chunk' && chunk.isEntry) {
            collect(chunk)
          }
        }

I tested this and the styles are always deterministic and the dynamically imported chunks will overwrite previous styles.

Copy link
Member

Choose a reason for hiding this comment

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

I'm not sure if that's enough. If the bundle has multiple direct entries, wouldn't that mean on the iteration of the first direct entry, we would crawl its dynamicImports before iterating the second direct entry?

Maybe we should do two loops instead:

  1. Loop through direct entries and collect the CSS, and record dynamic entries to a Set.
  2. Loop through the Set (of dynamic entries), and collect the CSS.

That should still keep the dynamic entries deterministic?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@bluwy you are right, tested this and it works (see commit I pushed).

@patak-dev
Copy link
Member

/ecosystem-ci run

@vite-ecosystem-ci
Copy link

@susnux susnux force-pushed the fix/deterministic-styles branch from 9f584f1 to 42319ec Compare May 30, 2024 08:57
@susnux
Copy link
Contributor Author

susnux commented May 30, 2024

Rebased to resolve the conflict.

@susnux susnux requested a review from patak-dev May 30, 2024 08:59
susnux added 2 commits June 8, 2024 15:06
Without this the order of the extracted CSS rules is defined by the order `renderChunk` of the css plugin is called.
So with this the order of CSS rules is always in order of the output chunks of the bundle.

Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
…e the styles

Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
@susnux susnux force-pushed the fix/deterministic-styles branch from 42319ec to f34bc55 Compare June 8, 2024 13:18
@bluwy
Copy link
Member

bluwy commented Jun 10, 2024

/ecosystem-ci run

@bluwy bluwy added feat: css p3-minor-bug An edge case that only affects very specific usage (priority) labels Jun 10, 2024
@vite-ecosystem-ci
Copy link

@patak-dev patak-dev merged commit a52ed1d into vitejs:main Jun 10, 2024
11 checks passed
@susnux susnux deleted the fix/deterministic-styles branch June 10, 2024 09:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feat: css p3-minor-bug An edge case that only affects very specific usage (priority)
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants