Repository navigation
fix(metro): patch every loaded Metro graph for uncached modules - #721
Draft
dlebedynskyi wants to merge 1 commit into
Draft
dlebedynskyi wants to merge 1 commit into
dlebedynskyi wants to merge 1 commit into
Conversation
The uncached-module and lazy CSS entry patches only reached the Metro copy uniwind resolves. The CLI that runs the server can bring its own: React Native CLI's community plugin runs a Metro 0.84.4 copy in this workspace, beside the root 0.85.0, and `@expo/metro` runs its pinned 0.84.5 whenever the install doesn't hoist that version. Those graphs were never patched. Under React Native CLI the CSS entry wasn't re-transformed after other modules changed: a class added to a component got no styles, and a token-only edit to a stylesheet imported by the entry (tracked since uni-stack#676) hot-reloaded an empty module. Expo's getDefaultConfig patches its own graph for uncached modules, so a separate `@expo/metro` copy only missed the lazy CSS entry patch. Patch every Metro graph module already loaded as well; both CLIs load it before they evaluate the config.
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: true
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. Comment |
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
I ran into this in a monorepo where uniwind is a workspace package and
apps/bareruns on the React Native CLI. After #676 I imported a token stylesheet fromglobal.cssand edited a token. In the Expo app the change hot reloaded, but inapps/bareit never reached the app. Metro sent an HMR update, but the update only carried the imported stylesheet's empty module, and the CSS entry was never transformed again.uniwind patches Metro's
Graphin two places.patchMetroGraphToSupportUncachedModulesmakes it transform the CSS entry again on every change, andpatchMetroGraphToIncludeCssInLazyGraphs(#646) adds the entry to lazy development graphs. Both patch theGraphthatrequire('metro/private/DeltaBundler/Graph')returns from uniwind's location. That isn't always the Metro running the server. A fresh install ofmain(9fc9b40) has these copies:node_modules/metropackages/uniwind's dev dependency, and what uniwind resolvesnode_modules/@react-native/community-cli-plugin/node_modules/metroreact-native startinapps/barenode_modules/@expo/metro/node_modules/metroexpo startinapps/expo-exampleI preloaded a script into each dev server. It lists the
metro/src/DeltaBundler/Graph.jsmodules that are loaded and shows whether each has uniwind's patches:Neither server builds its graphs from the root copy. uniwind loads that one to patch it. Expo's own
getDefaultConfigpatchestraverseDependencieson the@expo/metrocopy. uniwind doesn't.Then I checked HMR in
apps/bare. I addedsrc/hmr-tokens.css, which defines one@themetoken (--color-hmr-token: #c1c1c1) and has@source inline("bg-hmr-token"). I imported it fromsrc/global.css, startedreact-native start, fetched the iOS dev bundle, and registered an HMR client for it. As #676 intends, the entry requires the token file:src/global.css -> ["src/hmr-tokens.css", "../../packages/uniwind/uniwind.css"](#720 removes the second one.) Then I changed the token to
#d2d2d2:apps/bareon mainapps/barewith this PRapps/expo-example, main and this PRsrc/hmr-tokens.csssrc/hmr-tokens.css,src/global.csshmr-tokens.css,global.cssOn main the update's only module is
src/hmr-tokens.css. Plain Metro builds it as an empty module (function (global, ...) {}), so nothing in the app changes. The problem isn't limited to imported stylesheets. I also added a class used nowhere else (bg-[#e3e3e3]) tosrc/App.tsx. On main the update contains onlysrc/App.tsx, so the new class gets no styles. With this PR it containssrc/App.tsxandsrc/global.css, and carries the new color.Cause
Metro only transforms the files that changed. The native CSS entry is marked
skipCache, andpatchMetroGraphToSupportUncachedModulesmakesGraph.prototype.traverseDependenciesadd such modules to every traversal. That's how Tailwind recompiles the entry whenever any file changes. But this patch and the lazy entry patch are both applied to theGraphclass of the Metro that resolves from uniwind, which is the root 0.85.0 here.react-native startruns@react-native/community-cli-plugin, which depends onmetro ^0.84.3. The root has 0.85.0,packages/uniwind's dev dependency (seeCONTEXT.md), so the install nests 0.84.4 for the plugin. That copy'sGraphis a separate class, and nothing patches it. So inapps/barea change re-transforms only the changed file, never the entry.Expo is different because
getDefaultConfigfrom@expo/metro-configapplies the sametraverseDependenciespatch to@expo/metro's graph.@expo/metropinsmetro0.84.5 exactly, so it runs its own copy here too. That copy misses only uniwind's lazy CSS entry patch from #646.Fix
A new
getMetroGraphsreturns theGraphfrom the Metro uniwind resolves, as before. It also returns theGraphexport of everymetro/src/DeltaBundler/Graph.jsmodule already inrequire.cache, and both patches loop over that set. The existing markers (__patched,__uniwindLazyCssEntryPatched) still stop a class from being wrapped twice. Expo marks its owntraverseDependenciespatch with__patchedas well, so uniwind leaves that one alone, as it already does when Expo and uniwind share a Metro copy.This relies on the CLI loading its graph before it evaluates
metro.config.js. The React Native CLI does: in the probe, itsGraphmodule shows up unpatched first and gets patched once the config runs. Expo'sgetDefaultConfigrequires its graph inside the config, beforewithUniwindConfigis called. I also added a sentence about this to the Metro section ofCONTEXT.md.Testing
tests/native/bundler/metro-patches.test.tswrites a stand-innode_modules/metro/src/DeltaBundler/Graph.jsto a temporary directory. It requires it before applying the patches, the way a CLI loads its own copy. Then it checks three things on that class:traverseDependenciesadds the uncached CSS entry to the paths and bumps itsunstable_transformResultKey, but leaves cached modules alone.On main the first two fail, because the paths and entry points come back without the CSS entry. With the fix all three pass.
I repeated the probe with the fix. Every loaded graph has both patches, in both apps.
I repeated the live runs above with the fix:
apps/bare, the token edit and the class edit each gave exactly one HMR update. It included the entry and carried the new value.uniwind.csswasn't written.apps/expo-examplebehaves as before: one rebuild per edit, carrying the new value, then nothing.I ran the CI steps locally: install, build, type checks, lint, format, circular check, and
bun run test(native 206, web 48, e2e 9, types). I also exportedapps/expo-examplefor iOS, Android and web, builtapps/vite-example, and bundledapps/bare.Relation to #720
For its two-server run, #720 used a second Expo app instead of
apps/bare, because Metro doesn't re-run the bare entry on file changes. That's this bug. Once the bare graph is patched,apps/barebehaves like the Expo apps in that scenario, so without #720 running both examples loops.I ran
apps/expo-exampleandapps/baretogether with only this PR. Both entries still requiredpackages/uniwind/uniwind.css. In the 30 s after start the artifact was written 451 times, and the servers rebuilt 244 and 293 times. Expo's transform also failed now and then withCannot use @variant with unknown variant: premium, when it read the light/dark artifact bare had just written. With #720's transformer change applied on top, the same run was quiet. The artifact was written twice at startup, nothing rebuilt in the 30 s after start or in the final 15 s, and each token edit rebuilt only the edited server, once, with the new value. The two changes don't touch the same code, but in this repo the examples only run together cleanly with both.Who's affected
This affects native development with Metro when the CLI runs a different Metro copy than the
metrothat resolves from where uniwind is installed. uniwind takesmetroas a peer dependency, so it uses whichever copy it can see, normally the hoisted one. When the copies differ, the CSS entry isn't transformed again after other files change. New classes in components and edits to stylesheets imported by the entry (#676) don't reach the app.A second copy appears when the package manager can't dedupe the CLI's
metrowith the hoisted one:@react-native/community-cli-plugindepends onmetro ^0.84.3(React Native 0.86). Any othermetroversion in the install can take the hoisted spot, and the CLI's copy gets nested. That includes a directmetrodependency (likepackages/uniwind's 0.85.0 here) and another workspace in a monorepo on a different React Native or Metro version. This is the case where hot reload breaks.@expo/metropins an exactmetro(0.84.5 in@expo/metro56.0.2, which Expo SDK 57 uses). If the hoistedmetrois any other version, as here with 0.85.0, Expo runs a nested copy. Native hot reload still works because of Expo's own patch. Only the lazy CSS entry patch from fix: web lazy components not hot reloading css #646 (web lazy components) was missing. I didn't run a web lazy-component check for that case. The unit test covers the patch itself.Installs with a single Metro copy aren't affected. That's the usual single-app project, where the CLI and uniwind resolve the same hoisted
metro. Production bundles (react-native bundle,expo export) and Vite aren't affected either.