Skip to content

fix(metro): skip the generated uniwind.css when tracking imported CSS - #720

Draft
dlebedynskyi wants to merge 1 commit into
uni-stack:mainfrom
alexa-endpoints:fix/hmr-skip-generated-artifact
Draft

dlebedynskyi wants to merge 1 commit into
uni-stack:mainfrom
alexa-endpoints:fix/hmr-skip-generated-artifact

Conversation

@dlebedynskyi

@dlebedynskyi dlebedynskyi commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

I ran into this in a monorepo where uniwind is a workspace package. After #676, two apps' Metro dev servers running at the same time kept rebuilding each other, because each entry now watched the generated uniwind.css that the other server kept rewriting. Clode did the most of the work bellow.

This follows up #676. When uniwind is linked instead of installed, the native development entry requires uniwind.css. That's the file the entry's own transform rewrites. Linked setups include a workspace package (like the examples in this repo), npm link, or a file: dependency. If two Metro servers with different theme lists run at once, each one's writes rebuild the other, and it never stops.

On main (9fc9b40), I added an hmr-tokens.css import to apps/expo-example/global.css and fetched the iOS dev bundle. These are the stylesheets the CSS entry module depends on:

global.css -> ["hmr-tokens.css", "../../packages/uniwind/uniwind.css"]

apps/bare behaves the same way: src/global.css -> ["src/hmr-tokens.css", "../../packages/uniwind/uniwind.css"].

Next I ran apps/expo-example (light, dark, premium) alongside a copy of it with one extra theme (extraThemes: ['premium', 'ocean']). Each ran expo start from its own directory, with an HMR client registered for its iOS bundle. I counted the HMR updates that carried modules and the writes to packages/uniwind/uniwind.css:

window uniwind.css writes rebuilds, expo-example rebuilds, copy
30 s after start, no edits 427 233 218
30 s after one token edit in expo-example 445 243 230
next 15 s, no edits 212 118 108

This keeps going until one of the servers stops. Each rebuild runs the Tailwind compile for the entry again.

A single server pays once. On a fresh install the shipped artifact doesn't match the app's themes (expo-example adds premium), so the first transform rewrites it. When the HMR client connects, Metro handles that change and transforms the entry a second time.

Cause

#676 requires every stylesheet Tailwind reports through onDependency that ends in .css and isn't under node_modules. Tailwind's resolver follows symlinks. So when node_modules/uniwind is a link, @import 'uniwind' is reported by its real path, packages/uniwind/uniwind.css, and that passes the filter. It's the same file generateArtifacts(cssArtifactPath) writes at the start of the transform.

Once the artifact is a module in the graph, Metro handles every write to it as a change, and the uncached entry gets transformed again, which regenerates the artifact. With one theme list, the second write has identical content and is skipped. With two servers, each writes its own themes, which the other sees as a change.

Fix

isImportedStylesheet keeps the existing rules and also drops the stylesheet whose real path matches the generated artifact's. Both sides are resolved, so the check still works when the artifact path itself goes through a link. Local stylesheets and those of linked workspace packages are still required, so token edits still hot reload. I also added a line about this to CONTEXT.md.

Testing

  • tests/native/bundler/transformer.test.ts sets up a linked layout in a temporary directory: node_modules/uniwind and node_modules/brand link into packages/, next to a regular node_modules/theme. It compiles an entry that imports all three plus a local tokens.css with Tailwind, then checks which reported stylesheets the entry keeps (tokens.css and brand.css). It covers the artifact path given as a real path and as a path through the link. With the old filter, both cases fail because they keep packages/uniwind/uniwind.css. A plain string comparison passes the real-path case but fails the linked one.
  • I repeated the live run with the fix. The entries now require only hmr-tokens.css.
    • With both Expo servers running, nothing rebuilt and the artifact wasn't written, both in the 30 s after start and in the final 15 s.
    • Each token edit rebuilt the edited server once, with the new token, and didn't rebuild the other one. The other server's HMR client only got empty updates.
    • A single server no longer transforms the entry a second time at startup.
  • I ran the CI steps locally: install, build, type checks, lint, format, circular check, and bun run test (native 205, web 48, e2e 9, types). I also exported apps/expo-example for iOS, Android and web, built apps/vite-example, and bundled apps/bare.

For the two-server run I used a second Expo app, not apps/bare. The bare app's React Native CLI runs its own Metro 0.84, while uniwind patches the root Metro 0.85, so Metro doesn't re-run the bare entry on file changes. With apps/expo-example and apps/bare together, the artifact was written three times at startup, each server rebuilt once, and then it stopped. The bare entry still requires the artifact.

Who's affected

This affects native development builds with Metro where uniwind resolves outside node_modules:

  • a workspace package in a monorepo (workspace:*, as in this repo)
  • npm link, or a file:/link: dependency that's installed as a symlink

Regular installs aren't affected because Tailwind reports a path under node_modules, and that includes pnpm's node_modules/.pnpm. Production builds and web don't add these requires, so they aren't affected either.

The endless rebuilds need two or more Metro servers that share one uniwind copy and use different theme lists, for example two apps in the same monorepo. A single server transforms its entry one extra time at startup whenever the installed artifact doesn't match its themes.

Since uni-stack#676, development native entries require every stylesheet Tailwind
reports that isn't under node_modules. When uniwind is linked (a workspace
package, `npm link`, a `file:` install), Tailwind resolves `@import 'uniwind'`
to the package's uniwind.css by its real path, outside node_modules. The
entry then requires the artifact that its own transform rewrites. Every
write changes a module in Metro's graph and re-runs the uncached entry
transform. A single server rebuilds the entry once more at startup, and two
Metro servers with different theme lists rewrite the artifact for each
other indefinitely.

Leave out the stylesheet whose real path is the generated artifact's. Local
stylesheets and linked workspace stylesheets are still required.
@coderabbitai

coderabbitai Bot commented Oct 9, 2026

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

This branch has not been deployed

No deployments
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.

1 participant