feat: add Unistyles v3 support#59
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
|
Important Review skippedAuto incremental reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
WalkthroughAdds Unistyles v3 compatibility to the react-native-boost Babel plugin. The plugin now classifies each ChangesUnistyles v3 Support
Sequence Diagram(s)sequenceDiagram
participant BabelPlugin as Babel Plugin (index.ts)
participant isUnistylesInstalled
participant textOptimizer
participant createStyleOriginResolver
participant classifyStyleOrigin
participant replaceWithNativeComponent
BabelPlugin->>isUnistylesInstalled: isUnistylesInstalled(dirname)
isUnistylesInstalled-->>BabelPlugin: true/false (unistylesEnabled)
BabelPlugin->>textOptimizer: visit JSXOpeningElement(path, ..., unistylesEnabled)
textOptimizer->>createStyleOriginResolver: createStyleOriginResolver(path, unistylesEnabled)
createStyleOriginResolver->>classifyStyleOrigin: classify style expression
classifyStyleOrigin-->>createStyleOriginResolver: 'unistyles' | 'plain' | 'unknown'
createStyleOriginResolver-->>textOptimizer: getStyleOrigin()
alt origin = 'unknown'
textOptimizer-->>BabelPlugin: bail (no replacement)
else origin = 'unistyles'
textOptimizer->>replaceWithNativeComponent: (path, 'NativeText', UNISTYLES_TEXT_HOST)
replaceWithNativeComponent-->>textOptimizer: JSX rewritten to Unistyles NativeText
else origin = 'plain'
textOptimizer->>replaceWithNativeComponent: (path, 'NativeText', undefined)
replaceWithNativeComponent-->>textOptimizer: JSX rewritten to Boost NativeText
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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 |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/react-native-boost/src/plugin/index.ts`:
- Around line 28-30: The auto-detect hint in the Babel plugin is only tracked
per plugin instance, so it still logs once per JSX file instead of once per
build; move the latch from the local `unistylesHintLogged` variable in
`index.ts` to module scope and key it by the project root or equivalent shared
identifier used by the plugin. Also reorder the flow so `isIgnoredFile(...)` is
checked before any auto-detect warning logic, ensuring ignored files return
early and never trigger the hint. Use the existing `isUnistylesInstalled(...)`
and `unistylesEnabled` path as the entry point for the change.
In `@packages/react-native-boost/src/plugin/utils/unistyles.ts`:
- Around line 32-39: The package.json probe in unistyles.ts is swallowing all
read/parse failures and incorrectly falling back to parent manifests. Update the
loop in the package.json lookup logic to only continue when the current
package.json is missing, and rethrow malformed JSON or permission/read errors
instead of silently skipping them. Use the existing directory-walking code
around fs.readFileSync, JSON.parse, and nodePath.dirname to distinguish
file-not-found cases from other failures.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 2de5eadf-1df7-4ddf-8e3c-32a7551f0f7d
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (68)
README.mdapps/docs/content/docs/compatibility/unistyles.mdxapps/docs/content/docs/configuration/configure.mdxapps/docs/content/docs/index.mdxapps/docs/content/docs/meta.jsonapps/example/babel.config.jsapps/example/index.tsapps/example/package.jsonapps/example/src/app.tsxapps/example/src/navigation.tsapps/example/src/screens/launcher.tsxapps/example/src/screens/unistyles-demo/index.tsxapps/example/src/unistyles.tspackages/react-native-boost/src/plugin/__tests__/fixtures-nesting-unistyles/cascades-view-text/code.jspackages/react-native-boost/src/plugin/__tests__/fixtures-nesting-unistyles/cascades-view-text/output.jspackages/react-native-boost/src/plugin/__tests__/fixtures-nesting-unistyles/mixed-origins-under-lean-view/code.jspackages/react-native-boost/src/plugin/__tests__/fixtures-nesting-unistyles/mixed-origins-under-lean-view/output.jspackages/react-native-boost/src/plugin/__tests__/fixtures-nesting/cascades-view-text/code.jspackages/react-native-boost/src/plugin/__tests__/fixtures-nesting/cascades-view-text/output.jspackages/react-native-boost/src/plugin/__tests__/fixtures-nesting/text-in-text-stays-bailed/code.jspackages/react-native-boost/src/plugin/__tests__/fixtures-nesting/text-in-text-stays-bailed/output.jspackages/react-native-boost/src/plugin/__tests__/nesting.test.tspackages/react-native-boost/src/plugin/index.tspackages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures-unistyles-ts/unistyles-cast-style/code.tsxpackages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures-unistyles-ts/unistyles-cast-style/output.tsxpackages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures-unistyles/unistyles-aliased-style/code.jspackages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures-unistyles/unistyles-aliased-style/output.jspackages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures-unistyles/unistyles-imported-style-bails/code.jspackages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures-unistyles/unistyles-imported-style-bails/output.jspackages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures-unistyles/unistyles-literal-style/code.jspackages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures-unistyles/unistyles-literal-style/output.jspackages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures-unistyles/unistyles-no-style/code.jspackages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures-unistyles/unistyles-no-style/output.jspackages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures-unistyles/unistyles-optional-member-style/code.jspackages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures-unistyles/unistyles-optional-member-style/output.jspackages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures-unistyles/unistyles-plain-rn-style/code.jspackages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures-unistyles/unistyles-plain-rn-style/output.jspackages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures-unistyles/unistyles-style-array/code.jspackages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures-unistyles/unistyles-style-array/output.jspackages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures-unistyles/unistyles-style/code.jspackages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures-unistyles/unistyles-style/output.jspackages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures-unistyles/unistyles-unknown-style-bails/code.jspackages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures-unistyles/unistyles-unknown-style-bails/output.jspackages/react-native-boost/src/plugin/optimizers/text/__tests__/index.test.tspackages/react-native-boost/src/plugin/optimizers/text/index.tspackages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures-unistyles/unistyles-imported-style-bails/code.jspackages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures-unistyles/unistyles-imported-style-bails/output.jspackages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures-unistyles/unistyles-no-style/code.jspackages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures-unistyles/unistyles-no-style/output.jspackages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures-unistyles/unistyles-plain-rn-style/code.jspackages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures-unistyles/unistyles-plain-rn-style/output.jspackages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures-unistyles/unistyles-style-in-resolvable-spread-bails/code.jspackages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures-unistyles/unistyles-style-in-resolvable-spread-bails/output.jspackages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures-unistyles/unistyles-style/code.jspackages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures-unistyles/unistyles-style/output.jspackages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures-unistyles/unistyles-unknown-style-bails/code.jspackages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures-unistyles/unistyles-unknown-style-bails/output.jspackages/react-native-boost/src/plugin/optimizers/view/__tests__/index.test.tspackages/react-native-boost/src/plugin/optimizers/view/index.tspackages/react-native-boost/src/plugin/types/index.tspackages/react-native-boost/src/plugin/utils/common/base.tspackages/react-native-boost/src/plugin/utils/common/index.tspackages/react-native-boost/src/plugin/utils/common/optimized-host.tspackages/react-native-boost/src/plugin/utils/common/validation.tspackages/react-native-boost/src/plugin/utils/constants.tspackages/react-native-boost/src/plugin/utils/generate-test-plugin.tspackages/react-native-boost/src/plugin/utils/unistyles.tspackages/react-native-boost/tsconfig.json
| const autoDetectedUnistyles = options.unistyles === undefined && isUnistylesInstalled(dirname); | ||
| const unistylesEnabled = options.unistyles === true || autoDetectedUnistyles; | ||
| let unistylesHintLogged = false; |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major
🧩 Analysis chain
🌐 Web query:
With @babel/helper-plugin-utils's declare(...), is the plugin factory invoked once per transformed file or once per overall build session?
💡 Result:
The function you pass to @babel/helper-plugin-utils’s declare(...) (the “plugin factory” that returns the visitor object) is invoked once per Babel instance / transform run (i.e., once per file being transformed), not once per overall build session. Babel’s documentation describes declare(...) as wrapping a plugin definition so Babel can instantiate it; Babel then “instantiates” plugins during compilation rather than treating them as a single shared global for the whole process [1]. In practice, Babel calls the exported plugin function for each file being compiled (using the file’s options context like filename/dirname), then uses the returned visitor for that specific traversal [1][2]. So: expect declare(...) to run per transformed file (per transform call), not only once for the entire build/watch session. If you need true “run once per build” behavior, you should implement it outside the plugin (e.g., in the loader/build script) or use an external cache keyed by build/session rather than relying on declare(...) invocation frequency.
Citations:
🏁 Script executed:
sed -n '1,120p' packages/react-native-boost/src/plugin/index.tsRepository: kuatsu/react-native-boost
Length of output: 2981
🏁 Script executed:
python3 - <<'PY'
from pathlib import Path
p = Path('packages/react-native-boost/src/plugin/index.ts')
for i, line in enumerate(p.read_text().splitlines(), 1):
if 1 <= i <= 120:
print(f"{i:4}: {line}")
PYRepository: kuatsu/react-native-boost
Length of output: 3389
Make the auto-detect warning process-scoped. unistylesHintLogged resets for every Babel plugin instance, so the hint will still fire once per JSX file instead of once per build. Move the latch to module scope, keyed by the project root or equivalent. Also place the isIgnoredFile(...) check before this warning so ignored files short-circuit cleanly.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/react-native-boost/src/plugin/index.ts` around lines 28 - 30, The
auto-detect hint in the Babel plugin is only tracked per plugin instance, so it
still logs once per JSX file instead of once per build; move the latch from the
local `unistylesHintLogged` variable in `index.ts` to module scope and key it by
the project root or equivalent shared identifier used by the plugin. Also
reorder the flow so `isIgnoredFile(...)` is checked before any auto-detect
warning logic, ensuring ignored files return early and never trigger the hint.
Use the existing `isUnistylesInstalled(...)` and `unistylesEnabled` path as the
entry point for the change.
| // Walk up until the filesystem root, stopping at the first readable `package.json`. | ||
| for (;;) { | ||
| try { | ||
| return JSON.parse(fs.readFileSync(nodePath.join(directory, 'package.json'), 'utf8')); | ||
| } catch { | ||
| const parent = nodePath.dirname(directory); | ||
| if (parent === directory) return undefined; | ||
| directory = parent; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Don't swallow malformed local package.json files.
Lines 34-36 catch every failure and keep walking upward. If the nearest manifest exists but is invalid JSON or unreadable, the probe silently falls back to a parent workspace package.json, which can enable or disable Unistyles mode for the wrong project. Only treat missing-file cases as “keep walking”; rethrow parse and permission errors.
Suggested fix
function readNearestPackageJson(fromDirectory: string): Record<string, unknown> | undefined {
let directory = nodePath.resolve(fromDirectory);
// Walk up until the filesystem root, stopping at the first readable `package.json`.
for (;;) {
try {
return JSON.parse(fs.readFileSync(nodePath.join(directory, 'package.json'), 'utf8'));
- } catch {
+ } catch (error) {
+ const code =
+ typeof error === 'object' && error !== null && 'code' in error
+ ? (error as { code?: string }).code
+ : undefined;
+ if (code !== 'ENOENT' && code !== 'ENOTDIR') {
+ throw error;
+ }
const parent = nodePath.dirname(directory);
if (parent === directory) return undefined;
directory = parent;
}
}
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| // Walk up until the filesystem root, stopping at the first readable `package.json`. | |
| for (;;) { | |
| try { | |
| return JSON.parse(fs.readFileSync(nodePath.join(directory, 'package.json'), 'utf8')); | |
| } catch { | |
| const parent = nodePath.dirname(directory); | |
| if (parent === directory) return undefined; | |
| directory = parent; | |
| // Walk up until the filesystem root, stopping at the first readable `package.json`. | |
| for (;;) { | |
| try { | |
| return JSON.parse(fs.readFileSync(nodePath.join(directory, 'package.json'), 'utf8')); | |
| } catch (error) { | |
| const code = | |
| typeof error === 'object' && error !== null && 'code' in error | |
| ? (error as { code?: string }).code | |
| : undefined; | |
| if (code !== 'ENOENT' && code !== 'ENOTDIR') { | |
| throw error; | |
| } | |
| const parent = nodePath.dirname(directory); | |
| if (parent === directory) return undefined; | |
| directory = parent; | |
| } | |
| } |
🧰 Tools
🪛 ast-grep (0.44.0)
[warning] 34-34: Filesystem path is not a string literal; a request-/variable-derived path can enable path traversal. Validate and normalize the path before use.
Context: fs.readFileSync(nodePath.join(directory, 'package.json'), 'utf8')
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').
(detect-non-literal-fs-filename-typescript)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/react-native-boost/src/plugin/utils/unistyles.ts` around lines 32 -
39, The package.json probe in unistyles.ts is swallowing all read/parse failures
and incorrectly falling back to parent manifests. Update the loop in the
package.json lookup logic to only continue when the current package.json is
missing, and rethrow malformed JSON or permission/read errors instead of
silently skipping them. Use the existing directory-walking code around
fs.readFileSync, JSON.parse, and nodePath.dirname to distinguish file-not-found
cases from other failures.
Currently, React Native Boost is incompatible with Unistyles, see issue #58.
This PR adds a
unistylesmode that routes elements that are provably styled by Unistyles stylesheet to Unistyles' own lean host, keeping reactivity while still skipping React Native's JS-based wrapper. Previously, Boost would optimize elements with unknown sources ofstyle. Now, whenunistylesmode is enabled, React Native Boost bails on components with unresolvable styles (e.g.props.styleor styles imported from another file), because we can't reliably tell what to rewrite the component to. Ifunistylesmode is not enabled, the previous behavior stays unchanged.It also fixes a pre-existing bug where descendants of an already-optimized element bailed (a rewritten host was treated as an unknown ancestor), so only the outermost element ever optimized. Nested optimization now cascades correctly.
Closes #58
Summary by CodeRabbit
New Features
Bug Fixes
Documentation