Skip to content

fix: validate serialized style entries individually instead of all-or-nothing#448

Closed
mertcanekiz wants to merge 1 commit into
uni-stack:mainfrom
mertcanekiz:fix/serialize-per-entry-validation
Closed

fix: validate serialized style entries individually instead of all-or-nothing#448
mertcanekiz wants to merge 1 commit into
uni-stack:mainfrom
mertcanekiz:fix/serialize-per-entry-validation

Conversation

@mertcanekiz

@mertcanekiz mertcanekiz commented Mar 8, 2026

Copy link
Copy Markdown

Summary

  • serializeJSObject currently validates the entire serialized stylesheet with a single new Function() call. If any single entry produces invalid JS (e.g. from a false-positive class name like [tid:%@] picked up by the Oxide scanner), the entire stylesheet is discarded, resulting in stylesheet: {} and zero styles at runtime in production builds.
  • This changes validation to per-entry: invalid entries are skipped with a warning while all valid styles (500+ entries) are preserved.
  • Changed Logger.error to Logger.warn since the build continues successfully

Reproduction

  1. Any Expo project with uniwind in production mode
  2. Have any file in the scanned source directory containing a string like [tid:%@] (common in iOS/Objective-C logs, Sentry configs, etc.)
  3. Run npx expo export --platform ios
  4. The generated bundle has stylesheet: {}, all styles missing

Test plan

  • TypeScript type check passes (tsc --noEmit)
  • All 110 existing tests pass (37 native + 2 web suites)
  • Lint passes
  • Build succeeds

Summary by CodeRabbit

  • Bug Fixes

    • Improved serialization stability by validating entries and gracefully skipping invalid ones instead of causing failures.
  • Refactor

    • Streamlined serialization logic with enhanced error handling that returns empty results for invalid data rather than propagating errors.

…-nothing

When Tailwind's Oxide scanner picks up false-positive class names (e.g.
`[tid:%@]` from Objective-C build artifacts), `serializeJSObject` would
discard the entire 255KB+ stylesheet because its single `new Function()`
validation failed on the one bad entry.

This changes validation to per-entry: invalid entries are skipped with a
warning while all valid styles are preserved.
@coderabbitai

coderabbitai Bot commented Mar 8, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

The serialization logic in serialize.ts shifts from a pipeline-based approach to direct Object.entries computation with added validation. Invalid entries are filtered via Function instantiation attempts, logged with warnings, and skipped; the function returns an empty string if no valid entries remain instead of an error path.

Changes

Cohort / File(s) Summary
Serialization Validation
packages/uniwind/src/metro/utils/serialize.ts
Refactored serializeJSObject to validate object entries by attempting Function instantiation; invalid entries are filtered with warnings. Changed return behavior from error logging to empty string when no valid entries exist. Removed addMissingSpaces import.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Possibly related PRs

  • fix: treat invalid js expression as plain string #410: Also modifies serialize.ts to prevent invalid JavaScript expressions from being serialized; this PR filters invalid object entries via Function instantiation while the referenced PR treats invalid tokens as plain strings.

Poem

🐰 Hoppy serialization, checked with care,
Invalid entries? We validate there!
Function tests each one, a bunny's delight,
Warnings logged gently when things aren't quite right.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix: validate serialized style entries individually instead of all-or-nothing' directly describes the main change: shifting from all-or-nothing validation to per-entry validation of serialized style entries.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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 and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
packages/uniwind/src/metro/utils/serialize.ts (1)

102-114: Consider logging which entries failed for easier debugging.

The validation logic correctly implements per-entry filtering. However, when entries fail, developers only see a count without knowing which class names caused issues. Logging the failed entries (or at least the first few) would significantly help diagnose false positives like the [tid:%@] case mentioned in the PR.

💡 Optional: Log failed entries for debugging
-    const validEntries = entries.filter(entry => {
+    const invalidEntries: string[] = []
+    const validEntries = entries.filter(entry => {
         try {
             // eslint-disable-next-line `@typescript-eslint/no-implied-eval`, no-new-func
             new Function(`function v() { const o = ({ ${entry} }) }`)
             return true
         } catch {
+            invalidEntries.push(entry)
             return false
         }
     })
 
-    if (validEntries.length < entries.length) {
-        Logger.warn(`Skipped ${entries.length - validEntries.length} invalid style entries during serialization`)
+    if (invalidEntries.length > 0) {
+        Logger.warn(`Skipped ${invalidEntries.length} invalid style entries during serialization: ${invalidEntries.slice(0, 3).join(', ')}${invalidEntries.length > 3 ? '...' : ''}`)
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/uniwind/src/metro/utils/serialize.ts` around lines 102 - 114, When
filtering entries in serialize.ts (the entries -> validEntries block) add
capturing of the failed entries and include them in the warning: compute
failedEntries = entries.filter(e => !validEntries.includes(e)) and call
Logger.warn with both the count and a short sample (e.g., first 10) of
failedEntries to avoid huge logs; reference the existing symbols entries,
validEntries, and Logger.warn and ensure the message stays concise and safe for
production (truncate or limit length).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/uniwind/src/metro/utils/serialize.ts`:
- Around line 102-114: When filtering entries in serialize.ts (the entries ->
validEntries block) add capturing of the failed entries and include them in the
warning: compute failedEntries = entries.filter(e => !validEntries.includes(e))
and call Logger.warn with both the count and a short sample (e.g., first 10) of
failedEntries to avoid huge logs; reference the existing symbols entries,
validEntries, and Logger.warn and ensure the message stays concise and safe for
production (truncate or limit length).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 76977aee-285a-454d-8b47-9a324cd015e5

📥 Commits

Reviewing files that changed from the base of the PR and between a828e68 and 416b915.

📒 Files selected for processing (1)
  • packages/uniwind/src/metro/utils/serialize.ts

Comment on lines -100 to +110
const serializedObject = pipe(obj)(
Object.entries,
entries => entries.map(([key, value]) => serializer(key, serialize(value))),
entries => entries.join(','),
result => {
if (result === '') {
return ''
}
const entries = Object.entries(obj).map(([key, value]) => serializer(key, serialize(value)))

const validEntries = entries.filter(entry => {
try {
// eslint-disable-next-line @typescript-eslint/no-implied-eval, no-new-func
new Function(`function v() { const o = ({ ${entry} }) }`)
return true
} catch {
return false
}
})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Let's use the pipe function like it was done before so we don't need random variable names

},
)
if (validEntries.length < entries.length) {
Logger.warn(`Skipped ${entries.length - validEntries.length} invalid style entries during serialization`)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It should be Logger.error, warn is not logged without debug flag enabled, we want to resolve all serialization errors and this would limit number of reported errors

const validEntries = entries.filter(entry => {
try {
// eslint-disable-next-line @typescript-eslint/no-implied-eval, no-new-func
new Function(`function v() { const o = ({ ${entry} }) }`)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Let's keep the original naming

Suggested change
new Function(`function v() { const o = ({ ${entry} }) }`)
new Function(`function validateJS() { const obj = ({ ${entry} }) }`)

@Brentlok

Brentlok commented Mar 9, 2026

Copy link
Copy Markdown
Contributor

Hi, thanks for the PR. It's a cool idea to move validation per entry instead of validating a full object.
I left a few suggestions inline, let me know your thoughts.

@Brentlok

Copy link
Copy Markdown
Contributor

Implemented in #456

@Brentlok Brentlok closed this Mar 16, 2026
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.

2 participants