Skip to content

[Pro 1.7.0] Bundling rewrites installed CSS and changes Expo fingerprints, preventing OTA delivery #672

Description

@invivek26

What happened?

With runtimeVersion: { policy: "fingerprint" }, bundling our Expo app changes the fingerprint of node_modules/uniwind without any native code or dependency changes. Generated CSS is written back into the installed package, which Expo fingerprints because Uniwind Pro autolinks native code.

In our EAS workflow, updates fingerprint after expo export, while the build fingerprints a clean install. This caused OTA updates to be published for a runtime version our installed builds did not carry, so users never received them. We also hit a build-time "Runtime version mismatch" when starting a build from a local tree where Metro had already run.

Related: #487 reported fingerprint drift and was attributed to RC artifacts changing on the CDN. This report identifies a separate trigger on stable Pro 1.7.0: writes to the installed package during bundling, without downloading or changing the dependency version.

Evidence from our investigation:

  • Comparing the update and build fingerprints isolated the differing dependency source to {"type":"dir","filePath":"../../node_modules/uniwind","reasons":["rncoreAutolinkingIos"]}.
  • Comparing the installed package with the pristine Pro 1.7.0 tarball isolated the generated CSS content change to uniwind.css, apart from our known postinstall patch/installer files. animations.css is also rewritten, but was byte-identical in our comparison.
  • Appending a comment to the installed uniwind.css changes the iOS fingerprint. Ignoring the two generated CSS files prevents that change.

Write sites in the shipped bundler:

  • src/bundler/adapters/metro/transformer.ts resolves ../../uniwind.css relative to its bundled location and calls generateArtifacts(cssArtifactPath) when transforming the CSS entry file. The repro uses this real Metro path without patching it.

  • dist/module/bundler/artifacts/css/index.js writes compiled theme CSS to the supplied output path.

  • dist/module/bundler/artifacts/css/build-animations-css.js resolves animations.css from require.resolve("uniwind/package.json") and writes it unconditionally. Its identical content is not itself the hash drift we observed.

  • The artifact-generation CLI also derives its CSS output path from its own package location (dist/module/bundler/cli/index.js). The same package-relative path exists in the public packages/uniwind/src/bundler/cli/index.ts.

Suggested fix: keep generated CSS outside the installed package, in an app-owned cache directory, and update CSS resolution accordingly. If animation CSS is constant, shipping it without rewriting it may be sufficient for that file. Generated theme CSS should not modify a directory included in native autolinking fingerprints.

Happy to open a PR for this, including a regression test that artifact generation leaves the installed package unchanged, if this approach works for you.

Current workaround: our app's .fingerprintignore includes the following (paths reflect our monorepo layout):

../../node_modules/uniwind/uniwind.css
../../node_modules/uniwind/animations.css

This keeps native Uniwind files fingerprinted. We can remove these exclusions once an upstream fix ships.

Steps to Reproduce

Minimal repository: https://github.com/invivek26/uniwind-fingerprint-drift-repro

Requires your normal Uniwind Pro installation setup. No EAS account, simulator, native build, or running dev server is needed.

git clone https://github.com/invivek26/uniwind-fingerprint-drift-repro
cd uniwind-fingerprint-drift-repro
bun install
node repro.mjs

Run before starting Metro or exporting manually. The script uses a real Expo export with the Metro cache cleared, fingerprints before/after, asserts that only the autolinked Uniwind directory changes, and checks that excluding the two generated CSS files prevents the drift. It restores the original uniwind.css afterward so the repro can be repeated.

Verified twice using the pristine cached Pro 1.7.0 package and existing local dependencies (no fresh dependency install performed):

Before export: 4d9c79754f5cd40c8370f9ca0001cf07dadc5870
After export:  0a3f4ea28d5788a45a6341b100ccf9edc5ff96db
Only changed source: node_modules/uniwind (rncoreAutolinkingIos)
Ignoring generated CSS:
  016c70bf3d1539333c186566f96c219b72fe56f3
  016c70bf3d1539333c186566f96c219b72fe56f3
REPRODUCED: exporting changes only Uniwind's native fingerprint; the CSS exclusions prevent it.

Expected: an export alone leaves the native runtime fingerprint unchanged.

Actual: the fingerprints differ, with node_modules/uniwind as the only changed source. Absolute hashes vary by installation; the script asserts the single-source diff and unchanged control fingerprint.

Snack or Repository Link (Optional)

https://github.com/invivek26/uniwind-fingerprint-drift-repro

Uniwind version

npm:uniwind-pro@1.7.0

React Native Version

0.86.3

Platforms

iOS (verified). Android may be affected through native autolinking as well, but we have not independently verified its fingerprint diff.

Expo

Yes — SDK 57 (57.0.20 in the repro). Original investigation: @expo/fingerprint 0.19.4; standalone repro also confirms it on 0.20.12.

Additional information 〰

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions