diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml index fff51832..bec95aee 100644 --- a/.github/actions/setup/action.yml +++ b/.github/actions/setup/action.yml @@ -13,10 +13,6 @@ runs: cache: pnpm cache-dependency-path: pnpm-lock.yaml - - name: Enable Corepack - run: corepack enable - shell: bash - - name: Install dependencies run: pnpm install --frozen-lockfile shell: bash diff --git a/.github/workflows/test.yml b/.github/workflows/ci.yml similarity index 65% rename from .github/workflows/test.yml rename to .github/workflows/ci.yml index 852b03c4..a5b75736 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,4 @@ -name: Test & build +name: CI on: push: branches: @@ -9,6 +9,7 @@ on: jobs: lint: + name: Lint runs-on: ubuntu-latest steps: - name: Checkout @@ -30,6 +31,7 @@ jobs: run: pnpm typecheck test: + name: Test runs-on: ubuntu-latest steps: - name: Checkout @@ -42,6 +44,7 @@ jobs: run: pnpm test build: + name: Build runs-on: ubuntu-latest steps: - name: Checkout @@ -54,33 +57,44 @@ jobs: run: pnpm package build parity-matrix: - name: React Native ${{ matrix.react-native }} parity + name: React Native ${{ matrix.react-native }} / Metro ${{ matrix.metro }} parity runs-on: ubuntu-latest strategy: fail-fast: false matrix: - react-native: - - '0.83' - - '0.84' - - '0.85' - - '0.86' - - '0.87' + include: + - react-native: '0.83' + metro: '0.83.8' + - react-native: '0.84' + metro: '0.83.8' + - react-native: '0.85' + metro: '0.84.5' + - react-native: '0.86' + metro: '0.84.6' + - react-native: '0.87' + metro: '0.87.0' + - react-native: '0.88.0-rc.0' + metro: '0.87.0' steps: - name: Checkout uses: actions/checkout@v7 - - name: Select React Native version + - name: Select React Native and Metro versions env: + METRO_VERSION: ${{ matrix.metro }} REACT_NATIVE_VERSION: ${{ matrix.react-native }} run: | node <<'NODE' const fs = require('node:fs'); const path = 'packages/react-native-boost/package.json'; const packageJson = JSON.parse(fs.readFileSync(path, 'utf8')); - const version = process.env.REACT_NATIVE_VERSION; + const metroVersion = process.env.METRO_VERSION; + const reactNativeVersion = process.env.REACT_NATIVE_VERSION; - packageJson.devDependencies['react-native'] = version; - packageJson.devDependencies['@react-native/babel-preset'] = version; + packageJson.devDependencies['metro'] = metroVersion; + packageJson.devDependencies['metro-config'] = metroVersion; + packageJson.devDependencies['react-native'] = reactNativeVersion; + packageJson.devDependencies['@react-native/babel-preset'] = reactNativeVersion; fs.writeFileSync(path, `${JSON.stringify(packageJson, null, 2)}\n`); NODE @@ -95,9 +109,6 @@ jobs: pnpm-lock.yaml packages/react-native-boost/package.json - - name: Enable Corepack - run: corepack enable - - name: Install dependencies run: pnpm install --no-frozen-lockfile @@ -107,5 +118,8 @@ jobs: - name: Build package run: pnpm package build + - name: Test Metro integration + run: pnpm --filter react-native-boost exec vitest run --project unit src/metro/__tests__ + - name: Test runtime parity run: pnpm package test --run --project parity diff --git a/.gitignore b/.gitignore index c2ddf466..e1c36ccc 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,3 @@ yarn-error.log # pnpm .pnpm-store/ - -# Turborepo -.turbo/ diff --git a/.nvmrc b/.nvmrc index 54c65116..4aa0e0a7 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v24 +v26 diff --git a/.oxfmtrc.json b/.oxfmtrc.json index d6026497..dd3cca68 100644 --- a/.oxfmtrc.json +++ b/.oxfmtrc.json @@ -13,11 +13,5 @@ "arrowParens": "always", "endOfLine": "lf", "sortPackageJson": false, - "ignorePatterns": [ - "CHANGELOG.md", - "apps/docs/**/*.md", - "apps/docs/**/*.mdx", - "apps/docs/.docusaurus/**", - "benchmarks/**" - ] + "ignorePatterns": ["CHANGELOG.md", "apps/docs/**/*.md", "apps/docs/**/*.mdx", "benchmarks/**"] } diff --git a/.oxlintrc.json b/.oxlintrc.json index fe510454..06acbd05 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -12,8 +12,7 @@ "**/*.config.{js,mjs,cjs}", "**/scripts", "**/plugins", - "**/__tests__", - "apps/docs/.docusaurus/**" + "**/__tests__/**/fixtures-*/**/{code,output}.{js,jsx,ts,tsx}" ], "rules": { "no-unused-vars": [ @@ -42,7 +41,7 @@ "unicorn/error-message": "error", "unicorn/escape-case": "error", "unicorn/explicit-length-check": "error", - "unicorn/filename-case": "error", + "unicorn/filename-case": ["error", { "case": "kebabCase" }], "unicorn/new-for-builtins": "error", "unicorn/no-accessor-recursion": "error", "unicorn/no-anonymous-default-export": "error", @@ -75,7 +74,6 @@ "unicorn/no-nested-ternary": "off", "unicorn/no-abusive-eslint-disable": "off", "unicorn/no-zero-fractions": "error", - "unicorn/number-literal-case": "error", "unicorn/numeric-separators-style": "error", "unicorn/prefer-add-event-listener": "error", "unicorn/prefer-array-find": "error", @@ -131,7 +129,10 @@ }, "overrides": [ { - "files": ["packages/react-native-time-to-render/src/*.ts"], + "files": [ + "packages/react-native-boost/src/plugin/__tests__/parity/mocks/*", + "packages/react-native-time-to-render/src/*Native*.ts" + ], "rules": { "unicorn/filename-case": "off" } diff --git a/.zed/settings.json b/.zed/settings.json deleted file mode 100644 index f9aab007..00000000 --- a/.zed/settings.json +++ /dev/null @@ -1,237 +0,0 @@ -{ - "lsp": { - "oxlint": { - "initialization_options": { - "settings": { - "configPath": null, - "run": "onType", - "disableNestedConfig": false, - "fixKind": "safe_fix", - "typeAware": true, - "unusedDisableDirectives": "deny" - } - } - }, - "oxfmt": { - "initialization_options": { - "settings": { - "fmt.configPath": null, - "run": "onSave" - } - } - } - }, - "languages": { - "CSS": { - "format_on_save": "on", - "prettier": { - "allowed": false - }, - "formatter": [ - { - "language_server": { - "name": "oxfmt" - } - } - ] - }, - "GraphQL": { - "format_on_save": "on", - "prettier": { - "allowed": false - }, - "formatter": [ - { - "language_server": { - "name": "oxfmt" - } - } - ] - }, - "Handlebars": { - "format_on_save": "on", - "prettier": { - "allowed": false - }, - "formatter": [ - { - "language_server": { - "name": "oxfmt" - } - } - ] - }, - "HTML": { - "format_on_save": "on", - "prettier": { - "allowed": false - }, - "formatter": [ - { - "language_server": { - "name": "oxfmt" - } - } - ] - }, - "JavaScript": { - "format_on_save": "on", - "prettier": { - "allowed": false - }, - "formatter": [ - { - "language_server": { - "name": "oxfmt" - } - }, - { - "code_action": "source.fixAll.oxc" - } - ] - }, - "JSON": { - "format_on_save": "on", - "prettier": { - "allowed": false - }, - "formatter": [ - { - "language_server": { - "name": "oxfmt" - } - } - ] - }, - "JSON5": { - "format_on_save": "on", - "prettier": { - "allowed": false - }, - "formatter": [ - { - "language_server": { - "name": "oxfmt" - } - } - ] - }, - "JSONC": { - "format_on_save": "on", - "prettier": { - "allowed": false - }, - "formatter": [ - { - "language_server": { - "name": "oxfmt" - } - } - ] - }, - "Less": { - "format_on_save": "on", - "prettier": { - "allowed": false - }, - "formatter": [ - { - "language_server": { - "name": "oxfmt" - } - } - ] - }, - "Markdown": { - "format_on_save": "on", - "prettier": { - "allowed": false - }, - "formatter": [ - { - "language_server": { - "name": "oxfmt" - } - } - ] - }, - "MDX": { - "format_on_save": "on", - "prettier": { - "allowed": false - }, - "formatter": [ - { - "language_server": { - "name": "oxfmt" - } - } - ] - }, - "SCSS": { - "format_on_save": "on", - "prettier": { - "allowed": false - }, - "formatter": [ - { - "language_server": { - "name": "oxfmt" - } - } - ] - }, - "TypeScript": { - "format_on_save": "on", - "prettier": { - "allowed": false - }, - "formatter": [ - { - "language_server": { - "name": "oxfmt" - } - } - ] - }, - "TSX": { - "format_on_save": "on", - "prettier": { - "allowed": false - }, - "formatter": [ - { - "language_server": { - "name": "oxfmt" - } - } - ] - }, - "Vue.js": { - "format_on_save": "on", - "prettier": { - "allowed": false - }, - "formatter": [ - { - "language_server": { - "name": "oxfmt" - } - } - ] - }, - "YAML": { - "format_on_save": "on", - "prettier": { - "allowed": false - }, - "formatter": [ - { - "language_server": { - "name": "oxfmt" - } - } - ] - } - } -} diff --git a/AGENTS.md b/AGENTS.md index c2d84d5b..6c84c479 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -56,7 +56,7 @@ Native host components are loaded from a runtime library to gracefully fall back ## Testing -Optimizer tests use `babel-plugin-tester` with fixture directories, each containing `code.js` (input) and `output.js` (expected transform). Some cases have `dangerous-output.js` variants for the optional dangerous-ancestor option. +Use `babel-plugin-tester` fixtures with `code.js` and `output.js` for optimizer input-to-output contracts. Keep same-configuration cases flat in `fixtures`. Use another fixture root only when parser, platform, version, or plugin options differ. Some fixtures have `dangerous-output.js` variants for the optional unknown-ancestor assumption. ## Tooling notes diff --git a/README.md b/README.md index 2187eca0..8fc8f42c 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,35 @@ -# πŸš€ react-native-boost + + + react-native-boost: The React Native performance compiler + + + +# react-native-boost + +[![npm version](https://img.shields.io/npm/v/react-native-boost.svg)](https://www.npmjs.com/package/react-native-boost) +[![CI](https://github.com/kuatsu/react-native-boost/actions/workflows/ci.yml/badge.svg)](https://github.com/kuatsu/react-native-boost/actions/workflows/ci.yml) +[![License: MIT](https://img.shields.io/badge/license-MIT-black.svg)](LICENSE) -![npm bundle size](https://img.shields.io/bundlephobia/min/react-native-boost?style=flat-square) ![GitHub](https://img.shields.io/github/license/kuatsu/react-native-boost?style=flat-square) ![GitHub last commit](https://img.shields.io/github/last-commit/kuatsu/react-native-boost?style=flat-square) +**The React Native performance compiler.** -A powerful Babel plugin that automatically optimizes React Native apps through static source code analysis. It replaces standard React Native components with their native counterparts where possible, leading to significant performance improvements. +React Native Boost analyzes your app at build time and automatically applies safe performance optimizations across your React Native code. -- ⚑ Automatic performance optimization through source code analysis -- πŸ”’ Safe optimizations that don't break your app -- 🎯 Virtually zero runtime overhead -- πŸ“± Cross-platform compatible -- πŸ§ͺ Works seamlessly with Expo -- 🎨 Configurable optimization strategies +It eliminates unnecessary runtime work, replaces JavaScript abstractions with more efficient native equivalents, and fixes common performance pitfalls without requiring you to rewrite your application code. + +- ⚑ **Automatic build-time optimization** β€” improve performance without manually tuning every component +- 🧠 **Static source analysis** β€” detect optimization opportunities before your app even runs +- πŸ—οΈ **Move runtime work to build time** β€” precompute and simplify work whenever it can be determined at build time +- πŸ› οΈ **Automatic performance fixes** β€” correct common performance mistakes when they can be safely transformed +- πŸ”’ **Safe by default** β€” optimizations are only applied when behavior can be preserved +- πŸͺΆ **Minimal runtime overhead** β€” optimizations happen entirely during compilation +- πŸ§ͺ **Use with your favorite tools** β€” fully compatible with most other ecosystem tools including Expo ## Documentation -The documentation is available at [react-native-boost.oss.kuatsu.de](https://react-native-boost.oss.kuatsu.de). +The documentation is available at [boost.kuatsu.de](https://boost.kuatsu.de). ## Benchmark -The app in the `apps/example` directory serves as a benchmark for the performance of the plugin. -
@@ -26,20 +37,23 @@ The app in the `apps/example` directory serves as a benchmark for the performanc
-See the [benchmarks page](https://react-native-boost.oss.kuatsu.de/docs/information/benchmarks) for Android results and the full methodology. +See the [benchmarks page](https://boost.kuatsu.de/docs/information/benchmarks) for Android results and the full methodology. ## Compatibility -| `react-native-boost` | React Native | -| -------------------- | ---------------- | -| `0.x` | All versions[^1] | -| `1.x` | `>=0.83` | +| `react-native-boost` | React Native | +| -------------------- | ------------ | +| `0.x` | All versions | +| `1.x` | `>=0.83` | +| `2.x` | `>=0.83` | -[^1]: Starting from React Native `0.80`, `react-native-boost@0` prints import deprecation warnings. +Current versions of React Native Boost are compatible with **all React Native versions since 0.83**. We test runtime behavior against all of these versions. + +For older React Native versions, you can install `react-native-boost@^0`. Please note that we do not support this version anymore and, starting from React Native `0.80`, it prints import deprecation warnings from React Native. ## Installation -Install the package using your favorite package manager. Please **do not** install the package as a dev dependency. While the Babel plugin itself would work as a dev dependency, it relies on importing the runtime library (`react-native-boost/runtime`) into your code, which requires the package to be installed as a regular dependency. Read more [here](https://react-native-boost.oss.kuatsu.de/docs/runtime-library/). +Install the package using your favorite package manager. Please **do not** install the package as a development dependency. While the Babel plugin itself would work as a development dependency, it relies on importing the runtime library (`react-native-boost/runtime`) into your code, which requires the package to be installed as a regular dependency. Read more [here](https://boost.kuatsu.de/docs/runtime-library/). ```sh npm install react-native-boost @@ -47,21 +61,22 @@ npm install react-native-boost yarn add react-native-boost ``` -Then, add the plugin to your Babel configuration (`babel.config.js`): +Then, add Boost to your Metro configuration (`metro.config.js`): ```js -module.exports = { - plugins: ['react-native-boost/plugin'], -}; +const { getDefaultConfig } = require('expo/metro-config'); // use `@react-native/metro-config` for non-Expo apps +const { withBoostConfig } = require('react-native-boost/metro'); + +module.exports = withBoostConfig(getDefaultConfig(__dirname)); ``` -If you're using Expo and don't see the `babel.config.js` file, run the following command to create it: +If you don't see the `metro.config.js` file, run this command first: ```sh -npx expo customize babel.config.js +npx expo customize metro.config.js ``` -If you're using Unistyles or Nativewind, refer to additional required setup instructions in [the documentation](https://react-native-boost.oss.kuatsu.de/docs). +If you're using Unistyles, Nativewind, or a bundler other than Metro, see the [documentation](https://boost.kuatsu.de/docs) for setup instructions. Finally, restart your React Native development server and clear the bundler cache: @@ -73,16 +88,16 @@ yarn start --clear That's it! No imports in your code, rebuilding, or anything else is required. -Optionally, you can configure the Babel plugin with a few options described in the [documentation](https://react-native-boost.oss.kuatsu.de/docs/configuration/configure). +Optional configuration is described in the [documentation](https://boost.kuatsu.de/docs/configuration/configure). ## How it works -A [short overview of how the plugin works](https://react-native-boost.oss.kuatsu.de/docs/information/how-it-works) as well as [a technical deep dive](https://react-native-boost.oss.kuatsu.de/docs/information/deep-dive) can be found in the documentation. +A [short overview of how the plugin works](https://boost.kuatsu.de/docs/information/how-it-works) can be found in the documentation. ## Contributing See the [contributing guide](CONTRIBUTING.md) to learn how to contribute to the repository and the development workflow. -## License +## Built at Kuatsu -MIT +Kuatsu is a boutique React Native agency specialized on building highly performant React Native apps. Visit [https://kuatsu.de](kuatsu.de) to learn more about our work. diff --git a/apps/docs/app/(home)/page.tsx b/apps/docs/app/(home)/page.tsx index d5f126b6..5b0e242a 100644 --- a/apps/docs/app/(home)/page.tsx +++ b/apps/docs/app/(home)/page.tsx @@ -9,11 +9,9 @@ export default function HomePage() { React Native Boost

-

- Improve your app's performance with one line of code. -

+

The React Native performance compiler.

- A Babel plugin that replaces analyzes your code and performs safe optimizations to reduce unnecessary runtime + A Metro plugin that replaces analyzes your code and performs safe optimizations to reduce unnecessary runtime overhead in React Native apps.

@@ -33,7 +31,7 @@ export default function HomePage() { Faster renders

- Removes runtime overhead from wrapper components to improve UI-heavy screens. + Removes runtime overhead by moving expensive work to build time.

@@ -42,7 +40,7 @@ export default function HomePage() { Safety first

- Conservative analysis skips uncertain optimizations to reduce behavioral risk. + Conservative by default, optimizing only what can safely be optimized.

@@ -51,7 +49,7 @@ export default function HomePage() { Minimal setup

- Install, add the Babel plugin, get instant improvements. No code changes required. + Install, add the Metro plugin, get instant improvements. No code changes required.

diff --git a/apps/docs/app/docs/[[...slug]]/page.tsx b/apps/docs/app/docs/[[...slug]]/page.tsx index f9d8b9a9..80607f7f 100644 --- a/apps/docs/app/docs/[[...slug]]/page.tsx +++ b/apps/docs/app/docs/[[...slug]]/page.tsx @@ -29,7 +29,7 @@ async function getTocInsertions(slug: string): Promise { } if (slug === 'configuration/configure') { - const [pluginOptionsToc, pluginOptimizationOptionsToc] = await Promise.all([ + const [pluginOptionsToc, optimizationOptionsToc, assumptionsToc, integrationOptionsToc] = await Promise.all([ getAutoOptionSectionsToc({ path: pluginTypesPath, name: 'PluginOptions', @@ -42,6 +42,18 @@ async function getTocInsertions(slug: string): Promise { idPrefix: 'plugin-optimization-options', depth: 3, }), + getAutoOptionSectionsToc({ + path: pluginTypesPath, + name: 'PluginAssumptions', + idPrefix: 'plugin-assumptions', + depth: 3, + }), + getAutoOptionSectionsToc({ + path: pluginTypesPath, + name: 'PluginIntegrationOptions', + idPrefix: 'plugin-integration-options', + depth: 3, + }), ]); return [ @@ -50,8 +62,16 @@ async function getTocInsertions(slug: string): Promise { items: pluginOptionsToc, }, { - afterUrl: '#plugin-optimization-options', - items: pluginOptimizationOptionsToc, + afterUrl: '#optimization-options', + items: optimizationOptionsToc, + }, + { + afterUrl: '#assumptions', + items: assumptionsToc, + }, + { + afterUrl: '#integration-options', + items: integrationOptionsToc, }, ]; } diff --git a/apps/docs/app/layout.tsx b/apps/docs/app/layout.tsx index 67dee669..73962940 100644 --- a/apps/docs/app/layout.tsx +++ b/apps/docs/app/layout.tsx @@ -8,7 +8,7 @@ const inter = Inter({ }); export const metadata: Metadata = { - metadataBase: new URL('https://react-native-boost.oss.kuatsu.de'), + metadataBase: new URL('https://boost.kuatsu.de'), title: { default: 'React Native Boost', template: '%s | React Native Boost', diff --git a/apps/docs/app/repo-banner.jpg b/apps/docs/app/repo-banner.jpg new file mode 100644 index 00000000..fbeea834 Binary files /dev/null and b/apps/docs/app/repo-banner.jpg differ diff --git a/apps/docs/content/docs/compatibility/unistyles.mdx b/apps/docs/content/docs/compatibility/unistyles.mdx index 60ffa238..eabfabf2 100644 --- a/apps/docs/content/docs/compatibility/unistyles.mdx +++ b/apps/docs/content/docs/compatibility/unistyles.mdx @@ -3,23 +3,19 @@ title: Unistyles Support description: Keep Unistyles styling working on Boost-optimized components. --- -React Native Boost is compatible with react-native-unistyles v3. Previous versions of Unistyles are untested and not officially supported. +React Native Boost is compatible with Unistyles v3. Previous versions of Unistyles are untested and not officially supported. ## Setup -Enable Boost's Unistyles support layer through the Babel plugin's config options: +Enable Boost's Unistyles support layer in `metro.config.js`: ```js -// babel.config.js -module.exports = { - plugins: [ - ['react-native-boost/plugin', { unistyles: true }], - ['react-native-unistyles/plugin', { root: 'src' }], - ], -}; +module.exports = withBoostConfig(config, { + integrations: { unistyles: 'on' }, +}); ``` -Set `unistyles: false` to turn the mode off, e.g. when Unistyles is installed in your project's dependencies, but not actually used in code. The order of the two plugins does not matter. +Set `integrations.unistyles` to `off` when Unistyles is installed in your project's dependencies, but not actually used in code. React Native Boost can auto-detect Unistyles and enable this mode automatically if you haven't explicitly disabled it. However, this auto-detection is fragile and Boost will therefore log a warning to the console. Explicitly set the config flag as shown above to silence the warning. diff --git a/apps/docs/content/docs/compatibility/uniwind.mdx b/apps/docs/content/docs/compatibility/uniwind.mdx index 081dd549..33f2a69f 100644 --- a/apps/docs/content/docs/compatibility/uniwind.mdx +++ b/apps/docs/content/docs/compatibility/uniwind.mdx @@ -1,24 +1,40 @@ --- title: Uniwind Support -description: Why React Native Boost and Uniwind are currently incompatible. +description: Keep Uniwind styling working on Boost-optimized components. --- - - React Native Boost and [Uniwind](https://uniwind.dev) are currently incompatible. Enabling Boost in a Uniwind project drops `className` styling from optimized components entirely. - +React Native Boost supports Uniwind version 1.6.2 and up through its Metro integration. Uniwind Pro has not been tested and is not supported. -## What happens +Optimized components retain `className` styles and Uniwind subscriptions. -Uniwind resolves `className` to styles inside its own `Text`/`View` wrappers, which it swaps in via a -Metro resolver. Boost rewrites `Text`/`View` to their native counterparts **before** that swap, so `className` is left as an unknown prop on the raw native component. +## Setup -## What breaks +Keep your existing Uniwind CSS setup. Apply `withBoostConfig` **after** `withUniwindConfig` and set the Uniwind integration flag to `on`: -Any `Text`/`View` with a `className` (or a `*ClassName` prop such as `selectionColorClassName`): +```js +// metro.config.js -- All Tailwind styling is dropped, the element renders with no styles -- Uniwind extras are lost too (press state, `selectionColorClassName`, line-clamp) +const { getDefaultConfig } = require('expo/metro-config'); +const { withUniwindConfig } = require('uniwind/metro'); +const { withBoostConfig } = require('react-native-boost/metro'); -## Roadmap +module.exports = withBoostConfig( + withUniwindConfig(getDefaultConfig(__dirname), { + cssEntryFile: './global.css', + }), + { + integrations: { uniwind: 'on' }, + } +); +``` -Feasibility for Uniwind support is still being investigated. +The default `auto` setting detects an active Uniwind Metro configuration. It does not enable the integration just because the package is installed. Set `on` to require that configuration. Set `off` only when transformed code does not use Uniwind. + +Wrapper order matters. Boost must keep Uniwind's worker and resolver inside its own integration. This also keeps Metro's cross-file ancestor analysis available on supported Metro versions. + +## Limits + +- The integration requires Metro. A Babel-only setup does not install required runtime components. +- Uniwind 1.6.1 and lower are not supported. Boost uses version-specific Uniwind hooks for component state and accent colors. The Metro config rejects other package versions and will disable the integration. +- Uniwind Pro has not been tested and is not supported. +- Elements with Unistyles styles keep their original wrappers when both integrations are active at the same time. diff --git a/apps/docs/content/docs/configuration/boost-decorator.mdx b/apps/docs/content/docs/configuration/boost-decorator.mdx index 969eb5a2..8315a6b5 100644 --- a/apps/docs/content/docs/configuration/boost-decorator.mdx +++ b/apps/docs/content/docs/configuration/boost-decorator.mdx @@ -1,13 +1,13 @@ --- title: "Decorators" -description: Control optimization on individual JSX elements with @boost-ignore and @boost-force. +description: Control individual optimizations with @boost-ignore and @boost-force. --- ## @boost-ignore -Use `@boost-ignore` to disable optimization on a specific element. +Use `@boost-ignore` to disable a specific optimization. -If a line containing `@boost-ignore` appears immediately before a JSX opening tag, that component is skipped. +Place it immediately before a supported component or operation. ```jsx This will be optimized. @@ -17,20 +17,24 @@ If a line containing `@boost-ignore` appears immediately before a JSX opening ta ## @boost-force -Use `@boost-force` to force optimization on a specific element, even if it would normally be skipped by a bailout rule (e.g. blacklisted props, unresolvable spreads, or ancestor checks). +Use `@boost-force` to override bailout rules on a specific component or operation, such as an unknown ancestor, unresolved spread, or runtime style value. -The only check that `@boost-force` does **not** override is the `react-native` import check β€” the component must still be imported from `react-native`. +It cannot enable a disabled optimizer, include a file excluded by `ignores`, or optimize a component that is not imported from `react-native`. It also cannot run a platform-specific optimizer when Boost cannot tell whether the build targets iOS or Android. ```jsx const Component = ({ props }) => { return ( - {/* @boost-force */} - This will be optimized despite having unresolvable spread props. - ) -} + <> + {/* @boost-force */} + This will be optimized despite having unresolvable spread props. + + ); +}; + +/* @boost-force */ +const style = StyleSheet.flatten(styles); ``` - `@boost-force` bypasses safety checks that exist to prevent behavioral changes. - Only use it when you are confident that the optimization is safe for your specific use case. + `@boost-force` can bypass safety checks that prevent behavioral changes. Only use it when you have verified that the optimization is safe. diff --git a/apps/docs/content/docs/configuration/configure.mdx b/apps/docs/content/docs/configuration/configure.mdx index 0399073a..16df0334 100644 --- a/apps/docs/content/docs/configuration/configure.mdx +++ b/apps/docs/content/docs/configuration/configure.mdx @@ -1,41 +1,48 @@ --- -title: Configure the Babel Plugin -description: Control logging, ignores, and optimization behavior. +title: Configure Boost +description: Control logging, ignores, integrations, and optimization behavior. --- -The Babel plugin (`react-native-boost/plugin`) is the core of React Native Boost. +## Example Configuration -Defaults are safe and usable out of the box, but you can tune behavior for your app. +Pass options to `withBoostConfig` in your Metro config. -## Example Configuration +```js +// metro.config.js +const { getDefaultConfig } = require('expo/metro-config'); +const { withBoostConfig } = require('react-native-boost/metro'); + +module.exports = withBoostConfig(getDefaultConfig(__dirname), { + logLevel: 'debug', + ignores: ['node_modules/**'], + integrations: { + unistyles: 'on', + }, + optimizations: { + 'native-activity-indicator': 'off', + }, +}); +``` + +If you're using a custom `resolver.resolveRequest` that redirects react-native to a different installation, you may need to set the React Native package path explicitly: ```js -// babel.config.js -module.exports = { - plugins: [ - [ - 'react-native-boost/plugin', - { - verbose: false, - silent: false, - unistyles: false, - ignores: ['node_modules/**'], - optimizations: { - text: true, - view: true, - image: true, - }, - }, - ], - ], -}; +withBoostConfig(config, { + target: { reactNative: { packageJson: require.resolve('react-native/package.json') } }, +}); ``` + + Boost 2 does not need Babel configuration when you use Metro. For other bundlers, see the [non-Metro setup guide](./non-metro). + + ## Plugin Options -## Plugin Optimization Options +## Optimization Options + +Set an optimization to `on` or `off`. Omitted entries use their defaults. -## Environment-Specific Enablement +## Assumptions -You can enable React Native Boost by environment with Babel `env` config: +Assumptions let Boost optimize code that static analysis cannot prove safe. -```js -module.exports = { - env: { - development: { - plugins: ['react-native-boost/plugin'], - }, - }, -}; -``` + + A false assumption can change runtime behavior. Make sure you know what you're doing. + + + + +## Integration Options -See Babel docs: https://babeljs.io/docs/options#env + diff --git a/apps/docs/content/docs/configuration/non-metro.mdx b/apps/docs/content/docs/configuration/non-metro.mdx new file mode 100644 index 00000000..8458218a --- /dev/null +++ b/apps/docs/content/docs/configuration/non-metro.mdx @@ -0,0 +1,40 @@ +--- +title: Non-Metro environments +description: Configure Boost in apps that do not use Metro. +--- + +Since React Native Boost 2.x, we're using a Metro config adapter. In previous versions, a Babel config plugin was used instead. + +The Babel plugin is still provided as a fallback option for setups where Metro is unavailable. These are rare and it's likely that your app does not fall under this category. If you're unsure if you're using Metro, but simply don't see the `metro.config.js` file in your project, try running this command first to create it: + +```sh +npx expo customize metro.config.js +``` + +Then refer back to the [Metro configuration guide](./configure). + +If Metro is indeed unavailable in your setup, you can use the Babel plugin: + +```js +// babel.config.js +module.exports = { + plugins: [ + [ + 'react-native-boost/plugin', + { + target: { + reactNative: { + packageJson: require.resolve('react-native/package.json'), + }, + }, + }, + ], + ], +}; +``` + +Boost reads this package to select version-specific behavior and matching build-time processors. + +The target is optional. Without it, the Babel plugin tries to find the installed React Native package automatically and logs a warning. If detection fails, Boost skips version-dependent work and uses runtime fallbacks where available. + +Cross-file ancestor resolution needs Metro's resolved module graph. Non-Metro builds can therefore only safely use same-file ancestor analysis, which leads to lower optimization coverage. diff --git a/apps/docs/content/docs/index.mdx b/apps/docs/content/docs/index.mdx index dda0e607..cc898aa0 100644 --- a/apps/docs/content/docs/index.mdx +++ b/apps/docs/content/docs/index.mdx @@ -7,21 +7,23 @@ import { Tab, Tabs } from 'fumadocs-ui/components/tabs' ## Introduction -React Native Boost consists of two pieces: +React Native Boost is a React Native performance compiler. -- A Babel plugin that statically analyzes your source code and replaces safe `Text`, `View`, and `Image` components with their direct native counterparts, leading to significant performance improvements compared to the JS-based wrapper components. -- A runtime package used internally by the plugin for cross-platform-safe imports and helper utilities. +It analyzes your app at build time and automatically applies safe performance optimizations across your React Native code. -The analyzer is intentionally strict and skips any optimizations that may lead to user-facing bugs and behavioral changes. +It eliminates unnecessary runtime work, replaces JavaScript abstractions with more efficient native equivalents, and fixes common performance pitfalls without requiring you to rewrite your application code. ## Compatibility | `react-native-boost` | React Native | | --- | --- | -| `0.x` | All versions[^1] | +| `0.x` | All versions | | `1.x` | `>=0.83` | +| `2.x` | `>=0.83` | -[^1]: Starting from React Native `0.80`, `react-native-boost@0` prints import deprecation warnings. [See react-native-community/discussions-and-proposals #893.](https://github.com/react-native-community/discussions-and-proposals/discussions/893) +Boost 0.x and 1.x are not supported anymore and may break with future React Native updates. For Boost 2.x, we validate runtime behavior against all stable versions of React Native since 0.83. + +Starting from React Native 0.80, Boost 0.x causes React Native to print import deprecation warnings. [See react-native-community/discussions-and-proposals #893.](https://github.com/react-native-community/discussions-and-proposals/discussions/893) ## Getting Started @@ -58,20 +60,18 @@ The analyzer is intentionally strict and skips any optimizations that may lead t -2. If you use Expo and do not have a `babel.config.js` yet: +2. Add Boost to `metro.config.js`: + +```js +const { getDefaultConfig } = require('expo/metro-config'); +const { withBoostConfig } = require('react-native-boost/metro'); -```bash -npx expo customize babel.config.js +module.exports = withBoostConfig(getDefaultConfig(__dirname)); ``` -3. Add the plugin: +Bare React Native apps should import `getDefaultConfig` from `@react-native/metro-config` instead. -```js -// babel.config.js -module.exports = { - plugins: ['react-native-boost/plugin'], -}; -``` +If you do not use Metro in your project, refer to the [Babel plugin setup instructions](./configuration/non-metro.mdx) instead. If you're using Unistyles or Nativewind in your project, refer to these additional setup instructions: @@ -80,7 +80,7 @@ If you're using Unistyles or Nativewind in your project, refer to these addition -4. Restart the development server and clear cache: +3. Restart the development server and clear cache: @@ -124,8 +124,7 @@ React Native Boost supports all platforms. Optimizations are performed on iOS an - + - diff --git a/apps/docs/content/docs/information/benchmarks.mdx b/apps/docs/content/docs/information/benchmarks.mdx index 31ca4972..da8a00ff 100644 --- a/apps/docs/content/docs/information/benchmarks.mdx +++ b/apps/docs/content/docs/information/benchmarks.mdx @@ -10,42 +10,31 @@ import fpsAndroidDark from './img/fps-android.svg'; import fibersLight from './img/fibers-light.svg'; import fibersDark from './img/fibers.svg'; -To show what that React Native Boost buys you, -the example app in the repository serves as a benchmark by rendering a heavy, constantly-updating screen and -measuring the **frame rate**. We compare three configurations on the exact same device: the -**baseline** (stock React Native), a [**core-optimized** configuration](#reducedefaultpropsintext-feature-flag-in-react-native) that turns on React Native's own -overhead-reduction feature flag (detailed below), and **React Native Boost**. +To show what React Native Boost buys you, the example app in the repository hosts a benchmark that renders a heavy, constantly-updating screen and measures the **frame rate**. We compare three configurations on the exact same device: the **baseline** (stock React Native), a [**core-optimized** configuration](#reducedefaultpropsintext-feature-flag-in-react-native) that turns on any experimental feature flags in React Native that aim to do similar runtime work reductions as Boost (detailed below), and **React Native Boost**. Higher frames per second (FPS) means a smoother UI. The ideal target is 60 FPS, which means you need to commit one new frame every ~16 ms. Once the app can't keep up, FPS drops and the interface starts to stutter. ## Results -**iOS**: The baseline starts dropping frames early and falls to ~32 FPS under the heaviest load, while React Native Boost -holds a solid 60 FPS far longer and stays roughly **69% faster** at the top end. The core-optimized build (the middle -line) recovers part of the gap β€” around **8–15%** under heavy load β€” but Boost stays roughly **50% ahead of even -that**. +**iOS**: The baseline starts dropping frames early and falls to ~32 FPS under the heaviest load, while React Native Boost holds a solid 60 FPS far longer and stays roughly **69% faster** at the top end. The core-optimized build (the middle line) recovers part of the gap, but Boost stays roughly **50% ahead of even that**. -**Android**: Same shape, with Boost about **55% faster** at the heaviest load (and higher at intermediate heavy loads). -The core-optimized build helps here too β€” roughly **10–22%** at heavy loads. This device is noisy enough that a handful -of loads couldn't be measured reliably for the core comparison and are left out of that middle line (you'll see small -gaps), but Boost's lead is unambiguous throughout. +**Android**: Similar, with Boost about **55% faster** at the heaviest load (and higher at intermediate heavy loads). The core-optimized build helps here too, but can't recover the whole gap. The load on the test device used is quite noisy, but Boost's lead is unambiguous. ## Methodology -The test screen is a live crypto-style order book: two stacked columns of rows, each +The test screen is a live crypto-style order book. It displays two stacked columns of rows, each row a few pieces of `Text`, all updating many times per second from a simulated price feed. -Each run is a clean comparison on identical inputs: +Each run compares these identical inputs: -1. **One build, three configurations.** From a single release build we run the *baseline* (stock React Native), a *core-optimized* configuration with React Native's `reduceDefaultPropsInText` flag enabled, and *React Native Boost*. Nothing else changes. -2. **Sweep the load.** Each configuration steps through a range of row counts, from light to heavy. -3. **Hold the temperature constant.** Phones throttle as they heat up, which would corrupt the comparison. So before every capture the app idles until the device cools back to a fixed thermal floor, and every sample records the device's thermal state. All three series are measured at the same temperature. -4. **Measure FPS, repeatedly.** The app runs a continuous animation loop and records how long every frame takes over a fixed window, reporting the average FPS (plus 95th-percentile frame time and the share of dropped frames). Each point is captured several times and reported as the **median**, with the replicate spread drawn as error bars on the graphs. -5. **Validate the core comparison.** Boost's frame rate doesn't depend on the `reduceDefaultPropsInText` flag, so the Boost curve should land identically in the baseline and core-optimized runs. This is used as a built-in sanity check / validator. At any load where the two disagree (or the device is simply too noisy to trust), the point is dropped as unreliable. +1. **One build, three configurations.** From a single release build we run the *baseline* (stock React Native), a *core-optimized* configuration with any experimental performance optimization React Native feature flags enabled, and *React Native Boost*. +2. **Change the load.** Each configuration steps through a wide range of row counts, from light to heavy. +3. **Not run into thermal throttling.** Phones throttle as they heat up, which would corrupt the comparison. So before every capture the app idles until the device cools back to a fixed thermal floor. +4. **Measure FPS.** The app records how long every frame takes over a fixed window, reporting the average FPS (plus 95th-percentile frame time and the share of dropped frames). Each point is captured several times and reported as the **median**, with the replicate spread drawn as error bars on the graphs above. Everything runs as a **release/production build on the New Architecture**. @@ -53,8 +42,7 @@ Everything runs as a **release/production build on the New Architecture**. ### Number of React tree nodes -We're also making a second, device-independent measurement. For a single row, we count the **React tree nodes** -(fibers). One of the ways React Native Boost impacts an app's performance is by deleting a layer of these fibers per element it optimizes. +We're also making a second, device-independent measurement. For a single row, we count the **React tree nodes** (fibers). One of the ways React Native Boost impacts an app's performance is by deleting a layer of these fibers per element it optimizes. @@ -68,7 +56,7 @@ Thanks to various improvements to the reconciler and other internals in React Na ## `reduceDefaultPropsInText` feature flag in React Native -In React Native 0.82, a `reduceDefaultPropsInText` feature flag was introduced. It trimmed the `Text` wrapper's output so that unset accessibility/`aria-*` props no longer crossed the JSβ†’native boundary on every render. It shipped disabled by default and has since **graduated**: in newer React Native the flag is gone and the optimized behavior is simply the default. It's one step in React Native's long-term goal of reducing the `Text` and `View` wrapper overhead directly in core. The [Deep Dive](/docs/information/deep-dive#react-natives-own-roadmap) traces its full lifecycle. +In React Native 0.82, a `reduceDefaultPropsInText` feature flag was introduced that reduced some of the default props being set in the JS wrapper component, similar to what Boost is doing. It has since **graduated**, meaning the optimized behavior is the default in newer React Native versions. It's one step in React Native's long-term goal of reducing the `Text` and `View` wrapper overhead directly in core. wrappers so that we no longer have any reason to export these APIs.`} /> -In our benchmark, the **core-optimized** series in the graphs enables `reduceDefaultPropsInText` (on the React Native version measured here, where the flag still exists). Under heavy render load it recovers roughly **8–15%** of the baseline's lost frame rate on iOS (a bit more on Android), but closes only part of the gap. `NativeText`, the component React Native Boost uses under the hood, stays well ahead β€” around a **50%** margin over the core-optimized build at the heaviest loads on iOS. As React Native lands more of these optimizations, we expect the core-optimized line to keep rising toward React Native Boost. +In our benchmark, the **core-optimized** series in the graphs enables `reduceDefaultPropsInText` (on the React Native version measured in this benchmark run, where the flag still exists and isn't enabled by default). Under heavy render load it recovers roughly **8–15%** of the baseline's lost frame rate on iOS (a bit more on Android), but closes only part of the gap. `NativeText`, the component React Native Boost uses under the hood, stays well ahead. As React Native lands more of these optimizations, we expect the core-optimized line (and then the core line) to keep rising toward React Native Boost. diff --git a/apps/docs/content/docs/information/deep-dive.mdx b/apps/docs/content/docs/information/deep-dive.mdx deleted file mode 100644 index a5051067..00000000 --- a/apps/docs/content/docs/information/deep-dive.mdx +++ /dev/null @@ -1,418 +0,0 @@ ---- -title: Technical Deep Dive -description: A technical tour of the Text and View wrappers, the render-time tax they impose, and how React Native Boost removes it at build time. ---- - -This page is the long version of [How It Works](/docs/information/how-it-works). It walks through what the -`Text` and `View` wrappers actually do on every render, traces a single element from JSX all the way to -a native shadow node, and then shows exactly how the Babel plugin and its runtime helpers collapse that -work safely. - - - This has last been updated for React Native 0.85 and React Native Boost 1.3.0. - - -## `Text` and `View` are not host components - -It's tempting to think of `Text` and `View` as native platform primitives. Surprisingly (even to many senior RN developers), they aren't. They are ordinary JavaScript **function components** that each render a -lower-level component underneath: - -- `Text` renders `NativeText` (the host component `RCTText`), or `NativeVirtualText` (`RCTVirtualText`) when nested inside another `Text`. -- `View` renders `ViewNativeComponent` (the host component `RCTView`). - -Those underlying components are the real primitives. At runtime they resolve to the plain string `'RCTText'` and `'RCTView'` respectively, and React reconciles that to a **host component**. It builds the shadow node directly, with no JavaScript component fiber in between. - - = - NativeComponentRegistry.get('RCTView', () => ({ - uiViewClassName: 'RCTView', - }));`} -/> - -`NativeComponentRegistry.get(...)` registers the view config and **returns the name string**. `ViewNativeComponent` is literally `'RCTView'`, the same is true of `NativeText`: - - = - createReactNativeComponentClass('RCTText', () => - createViewConfig(textViewConfig), - ) as any;`} -/> - -The wrapper exists to translate ergonomic, cross-platform props into the props the host component actually -understands, and to manage some JS-side state (specifically, `TextAncestorContext` and pressability). The crucial -detail is that **all of that translation runs in JavaScript, on every render**, even when your element uses none -of it. They are rare edge cases producing real overhead for _every_ element. - -## Itemizing this tax - -### `Text` - -The `Text` wrapper is a single 550+ line file. Each time React renders a ``, its function body runs top to bottom. -Even a bare `Hello` pays for: - -- A **function-component invocation** and the React fiber that backs it. -- **Destructuring ~35 named props** out of `props`; every one a property read, most resolving to `undefined`. -- **`aria-*` β†’ `accessibility*` translation**: an `aria-label` coalesce, plus a five-field `accessibilityState` merge that allocates a fresh object whenever any `aria-*` state prop is set. -- **`disabled` ↔ `accessibilityState.disabled` reconciliation**. -- A **`Platform.select`** to resolve the default `accessible` value. -- An **`isPressable`** computation and `accessibilityRole`/`role` link defaulting. -- **`processColor(selectionColor)`** when a selection color is set. -- A **`numberOfLines` clamp** (with a dev-only `console.error` for negatives). -- A **`flattenStyle` walk** of the `style` prop, followed by `fontWeight` numberβ†’string conversion, `userSelect` lookup, and `verticalAlign` β†’ `textAlignVertical` mapping, each potentially allocating an `overrides` object and a new `[style, overrides]` array: - -(_style); - if (processedStyle != null) { - let overrides: ?{...TextStyleInternal} = null; - if (typeof processedStyle.fontWeight === 'number') { - overrides = overrides || ({} as {...TextStyleInternal}); - overrides.fontWeight = - // $FlowFixMe[incompatible-type] - String(processedStyle.fontWeight) as TextStyleInternal['fontWeight']; - } - - if (processedStyle.userSelect != null) { - _selectable = userSelectToSelectableMap[processedStyle.userSelect]; - overrides = overrides || ({} as {...TextStyleInternal}); - overrides.userSelect = undefined; - } - - if (processedStyle.verticalAlign != null) { - overrides = overrides || ({} as {...TextStyleInternal}); - overrides.textAlignVertical = - verticalAlignToTextAlignVerticalMap[processedStyle.verticalAlign]; - overrides.verticalAlign = undefined; - } - - if (overrides != null) { - // $FlowFixMe[incompatible-type] - _style = [_style, overrides]; - } - }`} -/> - -- A **`useContext(TextAncestorContext)`** subscription, used to decide whether to render as `RCTText` or `RCTVirtualText`. Once a fiber subscribes to a context, React must re-render it whenever that context value changes. -- A decision about whether to wrap the output in a **`` provider** which, when installed, is itself an extra fiber with its own context push/pop in the commit phase: - -{nativeText};`} -/> - -Importantly, none of this is expensive individually. The problem is the multiplier. A list row might contain a dozen -`Text` elements, a screen a few hundred. And the tax is paid by -every single one, on every single UI commit. - -### `View` - -The `View` wrapper is a lot leaner. Still, on each render it subscribes to `TextAncestorContext`, destructures 17 named props, runs a series of -`aria-*` translations, and decides whether to flip the context back to `false` for its descendants: - -, ...props: ViewProps) { - const hasTextAncestor = use(TextAncestorContext); - - const { - accessibilityState, - accessibilityValue, - 'aria-busy': ariaBusy, - 'aria-checked': ariaChecked, - 'aria-disabled': ariaDisabled, - 'aria-expanded': ariaExpanded, - 'aria-hidden': ariaHidden, - 'aria-label': ariaLabel, - 'aria-labelledby': ariaLabelledBy, - 'aria-live': ariaLive, - 'aria-selected': ariaSelected, - 'aria-valuemax': ariaValueMax, - 'aria-valuemin': ariaValueMin, - 'aria-valuenow': ariaValueNow, - 'aria-valuetext': ariaValueText, - id, - tabIndex, - ...otherProps - } = props;`} -/> - -## From JSX to pixels: the call graph - -Consider `Hello`. Here is the path it travels, with and without Boost. - - q{"What type does React see?"} - q -- "react-native Text (a function)" --> w["React renders a wrapper fiber"] - w --> work["Wrapper re-runs its whole body"] - work --> ret["Wrapper returns the RCTText host element"] - ret --> host["React renders an RCTText host fiber"] - q -- "Boost: NativeText (the string RCTText)" --> host - host --> node["completeWork builds a ParagraphShadowNode"] - node --> raw["A child RawTextShadowNode holds the string Hello"] - raw --> paint["Yoga calls TextLayoutManager to measure, then layout and paint"]`} -/> - -Without Boost, React sees a **function** type and renders a component fiber. It has to run everything in the -section above before the wrapper can even return the `RCTText` element. With Boost, React skips straight to the host fiber. Everything between "wrapper fiber" and "host fiber" disappears. - -The host side is identical either way. A single `Hello` always produces **two** shadow -nodes: a `ParagraphShadowNode` and a `RawTextShadowNode` child for the string (mounted as the `RCTText` and `RCTRawText` views). -Boost doesn't touch any of that. It only removes the JavaScript that ran *before* the host node was ever -created. - -The same is true of the React tree. Boost deletes the wrapper fiber (and, where the wrapper would have -added one, the `TextAncestorContext` provider fiber): - - w2["TextAncestorContext.Provider (sometimes)"] - w2 --> w3["RCTText (host fiber)"] - w3 --> w4["RCTRawText (host fiber)"] - end - subgraph withBoost["With Boost"] - direction TB - b3["RCTText (host fiber)"] --> b4["RCTRawText (host fiber)"] - end`} -/> - -Fewer fibers means less for React to build, diff, and commit on every frame (something our -[React tree node benchmark](/docs/information/benchmarks#number-of-react-tree-nodes) measures). - -## What Boost emits - -Boost's core move is to swap the JSX element's type from `Text`/`View`/`Image` to -`NativeText`/`NativeView`/`NativeImage` (imported from `react-native-boost/runtime`), after reproducing -(at *build time*) whatever inescapable work the wrapper would have done for the specific element. - -### Text - -The simplest case bakes in the defaults the wrapper would have applied, too: - -```tsx -// in -Hello - -// out -import { getDefaultTextAccessible as _getDefaultTextAccessible, NativeText as _NativeText } from 'react-native-boost/runtime'; -import { Text } from 'react-native'; - -<_NativeText allowFontScaling={true} ellipsizeMode={'tail'} accessible={_getDefaultTextAccessible()}>Hello; -``` - -`accessible` is -platform-specific (`true` on iOS, `false` on Android, omitted on web). When Metro tells Boost which -platform it's bundling for at compile time, the literal is inlined directly (`accessible={true}`); *only when it doesn't* (though it usually does!) it falls -back to the tiny [`getDefaultTextAccessible()`](/docs/runtime-library) runtime helper shown above. `allowFontScaling` and `ellipsizeMode` are simply the wrapper's defaults, inlined. - -A **fully static** style is normalized at build time and emitted as a plain object with no runtime call at -all: - -```tsx -// in - - -// out (style normalized at build time) -<_NativeText style={{ color: 'red' }} allowFontScaling={true} ellipsizeMode={'tail'} accessible={_getDefaultTextAccessible()} />; -``` - -A **dynamic** style is routed through the `processTextStyle` runtime helper instead, where a reference -cache and a single `StyleSheet.flatten` are the win (more on that below): - -```tsx -// in - - -// out -<_NativeText {..._processTextStyle(dynamicStyle)} allowFontScaling={true} ellipsizeMode={'tail'} accessible={_getDefaultTextAccessible()} />; -``` - -Accessibility and `aria-*` props (and `disabled`) are collected into a single `processAccessibilityProps` -call; a negative `numberOfLines` literal is rewritten to `0`; an `id` is renamed to `nativeID`; a static -`userSelect` is lifted out of the style into a top-level `selectable` prop. - -### View - -`View` optimizes even with a `style` prop, since the wrapper passes style through unchanged: - -```tsx - β†’ <_NativeView /> - β†’ <_NativeView style={{ width: 1 }} /> - β†’ <_NativeView nativeID="x" /> - β†’ <_NativeView focusable={true} /> - β†’ <_NativeView accessibilityLiveRegion="none" /> -``` - -Static `aria-*`/`tabIndex`/`id` props are translated into their native counterparts **at build time**. -Dynamic values, or `aria-*` state/value groups that the wrapper merges, are routed through the -`processViewAccessibilityProps` helper: - -```tsx -// in - - -// out -<_NativeView {..._processViewAccessibilityProps(Object.assign({}, { 'aria-label': label }))} />; -``` - -### Image - -`Image` source, style, and accessibility props are normalized before Boost swaps the wrapper for -`NativeImage`. Static values are processed at build time. Dynamic values use small runtime helpers. -Compared to `Text` and `View`, the host component for `Image` is not imported directly. React Native does not export it through a supported interface, and only exports it through deprecated deep imports which log warnings when used. The plugin therefore registers the `RCTImageView` host directly. - -For the complete matrix of what's optimized, translated, and skipped, see -[Coverage & Bailouts](/docs/information/optimization-coverage). - -## Inside the plugin - -The plugin is a single Babel visitor on `JSXOpeningElement`. For each element it runs the `Text`, -`View`, and `Image` optimizers; each follows the same shape. - - g{"Resolves to the react-native component?"} - g -- no --> skip["Leave unchanged"] - g -- yes --> b{"Does any bailout check fail?"} - b -- "yes, and no @boost-force" --> skip - b -- "no, or @boost-force" --> rw["Rewrite props, swap type to its native host"] - rw --> imp["Inject the runtime import (cached once per file)"]`} -/> - -### Proving the element is really `react-native`'s - -Two gates run first, and neither can be overridden: - -- **`isReactNativeComponent`** checks the JSX binding, imported name, and `'react-native'` source. Aliases such as `import { Text as RNText } from 'react-native'` work, while local components and deep imports do not qualify. - -### The bailout checks - -Each optimizer defines a list of **bailout checks**. If any fires (and the line isn't marked -`@boost-force`), the element is left untouched and logged as `skipped`. The checks encode "for this prop -or shape, the wrapper does something the host can't, or that Boost can't prove equivalent". This could be blacklisted -props, an unresolvable spread, non-primitive `Text` children, an unsafe ancestor, and so on. - -A subtle one worth highlighting: `Text` children must be **provably primitive** (a string or number). A -non-string child could smuggle in a nested element, including another ``, which would break the -`TextAncestorContext` invariant. So `{name}` is optimized only when `name` provably resolves -to a string/number; `{maybeJSX()}` is not. - -### Ancestor classification - -The most intricate check is shared by all three optimizers. Components under a `Text` can need different -host semantics. A nested `Text` uses `RCTVirtualText`, and Android uses a separate inline Image host. -Before optimizing, the plugin therefore walks **up** the tree and classifies the ancestor chain as one of: - -- `safe` β€” no `Text` ancestor anywhere up the chain β†’ optimize. -- `text` β€” a `react-native` `Text` is an ancestor β†’ skip. -- `unknown` β€” an ancestor is a component the plugin can't resolve β†’ skip, unless you opt in. - -The walk is more than a parent scan. It resolves JSX member expressions (`RN.View`), follows aliased -identifiers, recurses into **local function components** (including `memo()`/`forwardRef()` wrappers and -`props.children` render paths), and uses `WeakSet`s to break cycles in mutually-recursive components. When -it can't prove safety, it returns `unknown` and bails. False-positives (a missed optimization) are, as everywhere in the plugin, preferred over -false-negatives (a regression). - -The `unknown` case is *often* safe in practice (third-party components rarely wrap children in `Text`), but there are still cases where optimizing components with an `unknown` ancestor could genuinely cause regressions. Therefore, Boost provides -explicit opt-in escape hatches: `dangerouslyOptimizeViewWithUnknownAncestors`, -`dangerouslyOptimizeTextWithUnknownAncestors` and `dangerouslyOptimizeImageWithUnknownAncestors` (see [Configuration](/docs/configuration/configure)). - -### Rewriting and import injection - -Once an element passes, the optimizer rewrites its props (the build-time translations above), then swaps -the JSX type and injects the needed import. Imports are **cached per file** on the Babel file object, so -even though the visitor fires thousands of times, each runtime symbol is imported exactly once. - -## The runtime helpers - -`react-native-boost/runtime` is the small library the generated code calls into. For reference, its full -API lives on the [Runtime Library](/docs/runtime-library) page. The most load-bearing pieces are: - -- **`NativeText` / `NativeView`** resolve `unstable_NativeText` / `unstable_NativeView` from `react-native` at module load, and **gracefully fall back** to the standard `Text`/`View` on web or any runtime where these exports are missing. -- **`NativeImage`** loads React Native's public `Image` module to register its host, then renders the registered `RCTImageView` name directly. Web uses the standard `Image` component. -- **`processTextStyle(style)`** does the same flatten-and-normalize work as the wrapper, with one small difference: it **caches by reference in a `WeakMap`**. When you pass a `StyleSheet.create` reference, the first call flattens it and every later call returns the cached result. The wrapper re-flattens on every render. (Only stable references hit the cache; an inline `style={{…}}` is a fresh reference each render, so it re-flattens either way.) -- **`processAccessibilityProps(props)`** mirrors `Text`'s `aria-*` translation, `accessibilityState` merge, `disabled` reconciliation, and platform `accessible` default. It runs only when the element actually has accessibility props. -- **`processViewAccessibilityProps(props)`** does the same for `View`'s ARIA cluster (`aria-labelledby` split, live-region mapping, state/value aggregation, `tabIndex` β†’ `focusable`). - -## Why it's safe - -Boost is built on one strict contract: **never change rendered output, layout, or the accessibility tree.** Skipping a `Text` that could have been optimized just leaves -performance on the table; optimizing one that needed the wrapper would be a correctness bug with potential UI or behavior regressions. - -Two escape hatches let you override the analysis when *you* know more than the plugin can prove: -[`@boost-force`](/docs/configuration/boost-decorator#boost-force) on a single element, or the -`dangerouslyOptimize*WithUnknownAncestors` options for whole-project ancestor resolution. Both are named -to signal that you've taken ownership of the correctness argument. - -## React Native's own roadmap - -The wrappers are not meant to live forever. React Native's core team has clearly stated the goal is to reduce the JS overhead as much as possible, making direct use of the native host components unnecessary: - - -// and wrappers so that we no longer have any reason to export these APIs.`} -/> - -The first example for this is the -`reduceDefaultPropsInText` feature flag: introduced in RN 0.82, it made the -`Text` wrapper assign derived accessibility/`aria-*` props **only when defined** (and spread the rest) -instead of always passing a fixed set of named props, so that prop keys whose value was `undefined` no longer -crossed the JSβ†’native boundary. The feature flag **graduated** to default behavior in RN 0.85. - -It's a real improvement. However, the wrapper continues to destructure every -prop, flatten styles, subscribe to context, and run as its own fiber on every render. - -So, **React Native core is optimizing the runtime, while Boost patches the call site at build-time.** At some point in the future, in an ideal scenario, the JS wrapper components could become performant enough to make Boost genuinely unnecessary. However, today, it still offers a genuine performance improvement for a lot of apps. - - - - - - - diff --git a/apps/docs/content/docs/information/how-it-works.mdx b/apps/docs/content/docs/information/how-it-works.mdx index 5ef257cd..a51841be 100644 --- a/apps/docs/content/docs/information/how-it-works.mdx +++ b/apps/docs/content/docs/information/how-it-works.mdx @@ -1,52 +1,47 @@ --- title: How It Works -description: Why React Native Boost makes your app faster (the short version). +description: Why React Native Boost makes your app faster. --- -In React Native, `Text`, `View`, and `Image` aren't quite what they appear to be. They look like basic building blocks, but each one is a small JavaScript component that runs **every time it renders**, wrapping a lower-level native component underneath. +In React Native, many of the standard components, like `Text` and `View`, aren't quite what they appear to be. They look like basic building blocks, but each one runs JavaScript code **every time it renders**, wrapping lower-level native components underneath. -That wrapper is genuinely useful. It handles edge cases and powers conveniences such as accessibility props, style normalization, and Image source processing. But many elements do not need that runtime work, so the wrapper becomes pure overhead. On a busy screen with hundreds of these components, that overhead adds up and starts costing you frames. +These wrappers are genuinely useful. They handle edge cases and power many conveniences such as style normalization. But the majority of elements do not need all that runtime work, making these wrappers pure overhead in most cases. On a busy screen with hundreds of these components, that overhead adds up and starts costing you frames. -React Native Boost removes the wrapper when it isn't needed. +React Native Boost removes such wrappers when they're not needed. Additionally, it also makes a few smaller micro-optimizations that move small runtime work to the build time, like resolving `StyleSheet` function calls, folding `Platform.OS` branches, and more, which all stack up additionally across an app. -## The one-sentence version - -At build time, Boost rewrites `Text`, `View`, and `Image` elements into the native components they were going to render anyway. The work the wrapper used to repeat on every render is either gone completely, or moved from the user's device to build-time. +React Native Boost can therefore be described as a performance optimization compiler for React Native. ## A quick before and after ```tsx // You write: -Hello +Hello -// Boost compiles it to (simplified): -Hello +// On iOS, Boost compiles it to (simplified): +Hello ``` -`NativeText` is exactly what the `Text` wrapper would have rendered after doing the wrapper work outlines above. Boost just skips the middleman. The defaults the wrapper would have applied are baked in at build time, so what ends up on screen is identical. - -These optimized components are imported from `react-native-boost/runtime` rather than `react-native` directly. That indirection lets them fall back to the standard components on web and other platforms where the native versions aren't available. +This is exactly what React Native would have rendered using its `Text` wrapper component. The difference is that Boost does this work at build time, while pure React Native relies on a middleman on the runtime doing this work on your user's device instead. ## Only when it's safe -Boost never changes how your app looks or behaves. It rewrites a component only when it can **prove** that doing so is safe. For each supported component it checks a lot of things. For example: +By default, Boost rewrites a component only when it can **prove** that doing so is safe. For each optimization candidate it checks a lot of things. For example: -- Is this really the `react-native` component, or some other `Text` from another library? +- Is this really a `react-native` component, or some other `Text` from another library? - Are its props fully compatible with the underlying native component? - Is it in a safe spot in the tree (for example, not nested inside another `Text`)? -If any check fails, Boost leaves that component exactly as it was. The bias is strongly toward safety: it would rather miss an optimization than risk a bug. +If any check fails and Boost can't safely transform it at build time, it leaves that component or function exactly as it was. It has a strong bias toward safety and would rather miss an optimization than risk a bug. + +## Compatibility boundary + +Boost guarantees parity with React Native's public API and default internal configuration. It does not guarantee behavioral parity if your app uses private deep imports, custom internal feature flag values, internal component injection, or similar private or internal APIs. - - Files skipped via `ignores` are filtered before optimizer checks, so you will not see per-component skip logs for - those files. - - -## Overriding Bailouts - -Use `@boost-force` to force optimization on a component that would otherwise be skipped. This bypasses all bailout checks except the `react-native` import check. See the [Decorators](/docs/configuration/boost-decorator#boost-force) page for details. - -## Text Coverage - -`Text` is optimized when all checks pass. - -### Text blacklisted props - -If any of these are present, the `Text` node is skipped: - -- Interaction/responder props (`onPress`, `onLongPress`, `onResponder*`, `pressRetentionOffset`, `suppressHighlighting`, etc.) -- `selectionColor` -- `aria-hidden` - -`id` is renamed to `nativeID` at build time (`id` wins when both are present). `Text` bails only if `id`/`nativeID` arrive via a spread, or a dynamic `id` appears alongside a `nativeID`. - -### Text structure checks - -- Children must be provably primitive (resolve to a `string` or `number`). Anything that could be a React element (nested elements, function calls, unresolved identifiers) bails as `contains non-primitive children`. - -### Text ancestor safety checks - -Like `View`, `Text` optimization depends on ancestor classification: - -- `safe`: optimize -- `text`: skip (`has Text ancestor`) β€” a `Text` nested in another `Text` renders as the inline `NativeVirtualText` host (`RCTVirtualText`), not `NativeText` (`RCTText`), so optimizing it would emit the wrong host. -- `unknown`: skip by default - -A root `Text` returned by a component or renderer also skips as `has unresolved runtime parent that may render Text`. Its caller can mount it inside another `Text`, which requires `NativeVirtualText` instead of `NativeText`. - -Set `dangerouslyOptimizeTextWithUnknownAncestors: true` to optimize unknown ancestors and runtime parents. - - - Enabling dangerous mode can introduce regressions if an unresolved ancestor or runtime parent renders a `Text` - wrapper. For example, `expo-router`'s `Link` wraps its children in a `` by default. A `Text` under a `Link` - must render as `NativeVirtualText`, and optimizing it would be wrong. - - -A `Text` used as a direct child of `expo-router` `Link` with `asChild` is **always** skipped -(`is a direct child of expo-router Link with asChild`), even under the dangerous flag, because `Link` makes that child -pressable. - -```tsx -import { Link } from 'expo-router'; -import { Text } from 'react-native'; - - - Open profile -; -``` - -## View Coverage - -`View` and `Text` share the same ancestor safety checks. The `View` wrapper translates a few ergonomic props into -native equivalents; Boost reproduces each translation at build time (or via a small runtime helper for dynamic values), -so these props no longer force a bailout. - -### View translated props - -- `aria-*` β†’ `accessibility*` (`aria-label`, `aria-labelledby`, `aria-live`, `aria-hidden`, and the state/value groups) -- `tabIndex` β†’ `focusable` -- `id` β†’ `nativeID` (`id` wins when both are present) - -Everything else β€” including `accessible`, `accessibilityLabel`, and a lone `accessibilityState`/`accessibilityValue` β€” -is passed through unchanged. - -### View prop bailouts - -- A spread that may carry one of the translated props, since Boost cannot reach inside it to translate. -- A dynamic `id` alongside a `nativeID` (their runtime precedence cannot be resolved statically). - -### Ancestor safety checks - -`View` optimization depends on ancestor classification: - -- `safe`: optimize -- `text`: skip (`has Text ancestor`) -- `unknown`: skip by default - -Set `dangerouslyOptimizeViewWithUnknownAncestors: true` to optimize `unknown` ancestors too. - - - Enabling dangerous mode can increase optimization coverage, but it can also introduce regressions if unresolved - ancestors render Text wrappers. - - -## Image Coverage - -The `Image` optimizer rewrites supported `Image` elements when the target platform is known (`ios` or `android`) and the -source/style/accessibility props can be reproduced safely. - -In Unistyles mode, an Image is skipped when its `style` is (or may be) a Unistyles style. - -## Spread Props: Resolvable vs Unresolvable - -Unresolvable spread props are treated as unsafe and cause bailouts. - -```tsx -// Usually optimizable (resolvable object literal) -Hello - -// Usually skipped (cannot be statically resolved) -Hello -``` - -Same rule applies to `View`. diff --git a/apps/docs/content/docs/information/troubleshooting.mdx b/apps/docs/content/docs/information/troubleshooting.mdx index 86d1d2b6..72e5ff20 100644 --- a/apps/docs/content/docs/information/troubleshooting.mdx +++ b/apps/docs/content/docs/information/troubleshooting.mdx @@ -5,10 +5,10 @@ description: Common setup and optimization issues, plus fast ways to diagnose th ## Quick Diagnostic Flow -1. Set `verbose: true` and `silent: false` in plugin config. +1. Set `logLevel: 'debug'` in the Metro configuration. 2. Restart Metro with cache clear. 3. Check skip reasons in logs. -4. Compare with the coverage rules in [Optimization Coverage](/docs/information/optimization-coverage). +4. Compare the message with the relevant [optimizer page](/docs/optimizers). ## Common Issues @@ -16,24 +16,16 @@ description: Common setup and optimization issues, plus fast ways to diagnose th Likely causes: -- Plugin not loaded in `babel.config.js` -- `silent: true` +- `withBoostConfig` is missing from `metro.config.js` +- `logLevel: 'silent'` - File matched by `ignores` -Quick checks: +Quick check: ```js -module.exports = { - plugins: [ - [ - 'react-native-boost/plugin', - { - verbose: true, - silent: false, - }, - ], - ], -}; +module.exports = withBoostConfig(config, { + logLevel: 'debug', +}); ``` ```bash @@ -42,9 +34,7 @@ npm start -- --clear ### Skip reason: `contains blacklisted props` -This is expected for unsupported prop sets, e.g. a `Text` with press/responder props, `selectionColor`, or -`aria-hidden`. (`View` translates accessibility props, `tabIndex`, and `id` instead of skipping; see -[Optimization Coverage](/docs/information/optimization-coverage#view-coverage).) +This is expected for a `Text` with press or responder props. The native text host does not provide the wrapper behavior these props need. `View` can translate its accessibility props, `tabIndex`, and `id`; see the [Native View optimizer](/docs/optimizers/native-view). Fix options: @@ -57,24 +47,22 @@ Fix options: A `View` with a spread (`{...props}`) Boost can't statically resolve, or that may contain a prop the `View` wrapper translates (`aria-*`, `tabIndex`, `id`). -### Skip reason: `has unresolved ancestor and dangerous optimization is disabled` +### Skip reason: `has unresolved ancestor that may render Text` -A `View` or `Text` is inside an ancestor React Native Boost cannot statically classify, so it cannot prove the -ancestor is not a `Text` (which would change the correct native host). A common case is a `Text` inside a -third-party wrapper such as `expo-router`'s `Link` (which itself wraps its children in a ``). +A component is inside an ancestor React Native Boost cannot statically classify. Boost follows local and imported wrappers through Metro, but unsupported component patterns remain unknown. Boost also treats React Native components other than parity-proven hosts such as `View` and `Text` as unknown. Options: -- Keep default behavior (safest) +- Keep the default behavior (safest) - Use `@boost-force` on a specific line you have verified is safe -- Refactor ancestor/component structure to be statically obvious -- Enable `dangerouslyOptimizeViewWithUnknownAncestors` / `dangerouslyOptimizeTextWithUnknownAncestors` to override this behavior +- Refactor the ancestor or component structure so Boost can classify it +- Set `assumptions.unknownAncestorsDoNotRenderText` to `true` only if that statement is true across the project ### Skip reason: `has unresolved runtime parent that may render Text` -A root `Text` returned by a component or renderer can be mounted inside another `Text`. Boost keeps the wrapper because only that wrapper can select `NativeText` or `NativeVirtualText` from the runtime context. +A root component can be mounted inside a `Text`. Boost keeps a root `Text` wrapper so it can select `NativeText` or `NativeVirtualText`. It also keeps a root `View` with children so the wrapper can reset inherited text context. Android 0.83–0.84 Image uses the same check to select its inline host. -Use `@boost-force` only when every use of that component has a non-text parent. The `dangerouslyOptimizeTextWithUnknownAncestors` option also overrides this check. +Use `@boost-force` only when every use of that component has a non-text parent. The project-wide unknown ancestor assumption also overrides this check. ### Ignores do not work as expected in monorepos diff --git a/apps/docs/content/docs/meta.json b/apps/docs/content/docs/meta.json index 3f51cb5e..207a656c 100644 --- a/apps/docs/content/docs/meta.json +++ b/apps/docs/content/docs/meta.json @@ -6,12 +6,21 @@ "index", "---Information---", "information/how-it-works", - "information/deep-dive", - "information/optimization-coverage", "information/troubleshooting", "information/benchmarks", + "---Optimizers---", + "optimizers/index", + "optimizers/native-text", + "optimizers/native-view", + "optimizers/native-image", + "optimizers/native-activity-indicator", + "optimizers/animated-value-initialization", + "optimizers/animated-wrapper-removal", + "optimizers/stylesheet-operations", + "optimizers/platform-folding", "---Configuration---", "configuration/configure", + "configuration/non-metro", "configuration/boost-decorator", "---Compatibility---", "compatibility/unistyles", diff --git a/apps/docs/content/docs/optimizers/animated-value-initialization.mdx b/apps/docs/content/docs/optimizers/animated-value-initialization.mdx new file mode 100644 index 00000000..108d2ad4 --- /dev/null +++ b/apps/docs/content/docs/optimizers/animated-value-initialization.mdx @@ -0,0 +1,50 @@ +--- +title: Animated Value Initialization +description: Avoids recreating unused Animated values during component renders. +--- + +JavaScript evaluates a hook argument before it calls the hook. This means the common pattern below creates a new `Animated.Value` on every render, even though `useRef` keeps only the first value. + +```tsx +// Before +const opacity = useRef(new Animated.Value(0)).current; + +// After, simplified +const opacity = useAnimatedValue(0); +``` + +Boost also uses `useAnimatedValueXY` and `useAnimatedColor`. For state, it changes an eager value to React's lazy initializer form: + +```tsx +// Before +const [opacity] = useState(new Animated.Value(0)); + +// After +const [opacity] = useState(() => new Animated.Value(0)); +``` + +This removes construction and garbage collection for values that React would discard after each render. + +## When it runs + +Boost requires: + +- `Animated` from `react-native`. +- `useRef` or `useState` from `react`. +- A static, valid constructor input. +- An iOS or Android build. +- Immediate `.current` access for `useRef`. + +`useAnimatedValue` is supported on React Native 0.83 and later. `useAnimatedValueXY` and `useAnimatedColor` are supported on React Native 0.85 and later. + +Boost skips dynamic constructor inputs, ref objects that escape, other Animated implementations, and web builds. + +## Configuration + +```js +module.exports = withBoostConfig(config, { + optimizations: { + 'animated-value-initialization': 'off', + }, +}); +``` diff --git a/apps/docs/content/docs/optimizers/animated-wrapper-removal.mdx b/apps/docs/content/docs/optimizers/animated-wrapper-removal.mdx new file mode 100644 index 00000000..51eb2edf --- /dev/null +++ b/apps/docs/content/docs/optimizers/animated-wrapper-removal.mdx @@ -0,0 +1,68 @@ +--- +title: Animated Wrapper Removal +description: How Boost removes Animated wrappers that do not contain an animation. +--- + +The Animated Wrapper Removal optimizer removes an `Animated` wrapper when its element contains no animated value or event. + +```tsx +// Before + + Hello + + +// After, simplified + + Hello + +``` + +The resulting `View`, `Text`, or `Image` may then additionally pass through its corresponding Boost optimizer. For example, `Animated.View` can become `View` and then [`NativeView`](./native-view) in the same build. + +Boost keeps the defaults that React Native's Animated wrapper would have added. It also flattens static style arrays at build time. + +## Supported components + +- `Animated.View` +- `Animated.Text` +- `Animated.Image` +- `Animated.ScrollView` + +## When it runs + +The optimizer can remove a wrapper when: + +- `Animated` is imported from `react-native`. +- The build targets iOS or Android. +- Its props and expression children are known not to contain animated values or events. +- Its style is absent, null, or a static object or array literal. +- Any callbacks are inline functions or local functions that Boost can identify. + +## When it skips + +Boost keeps the Animated wrapper when it finds: + +- An `Animated.Value`, interpolation, animated calculation, or `Animated.event`. +- A prop, style, or child value that cannot be resolved at build time. +- Spread props or duplicate props. +- A `ref` or Animated passthrough prop. +- A `refreshControl` on `Animated.ScrollView`. +- An unknown or unsupported target platform. + +## React Native version support + +This optimizer is enabled by default for React Native 0.83 through 0.86. + +It is disabled by default for React Native 0.87 and later. React Native 0.87 changed how Android flushes native Animated operations. An `Animated` wrapper that has no animations itself may still flush work queued elsewhere, so removing it is not always safe. + +You can set `animated-wrapper-removal` to `on` explicitly, but this may cause small behavioral changes on Android in React Native 0.87 and higher. + +## Configuration + +```js +module.exports = withBoostConfig(config, { + optimizations: { + 'animated-wrapper-removal': 'off', + }, +}); +``` diff --git a/apps/docs/content/docs/optimizers/index.mdx b/apps/docs/content/docs/optimizers/index.mdx new file mode 100644 index 00000000..a45f5634 --- /dev/null +++ b/apps/docs/content/docs/optimizers/index.mdx @@ -0,0 +1,48 @@ +--- +title: Overview +description: What React Native Boost can optimize and the cases it safely skips. +--- + +React Native Boost includes different optimizers that optimize different parts of your app. + + + + + + + + + + + + +## Shared component safety rules + +When optimizing a component, all optimizers first confirm that the component comes from `react-native`. A component from another package, or a local component with the same name, is not changed. + +Boost also skips a component when: + +- Its file matches an [`ignores`](/docs/configuration/configure#plugin-options-ignores) pattern. +- Its line has [`@boost-ignore`](/docs/configuration/boost-decorator#boost-ignore). +- Required information is only available at runtime. + +Set `logLevel: 'debug'` to see why a component was skipped. + +## Controlling optimizers + +Most optimizers are enabled by default. Exceptions are: + +- The [Animated Wrapper Removal optimizer](./animated-wrapper-removal) is enabled by default only for React Native 0.83 through 0.86. + +You can turn each optimizer on or off in `metro.config.js`: + +```js +module.exports = withBoostConfig(config, { + optimizations: { + 'native-image': 'off', + 'animated-wrapper-removal': 'on', + }, +}); +``` + +See [Configure Boost](/docs/configuration/configure#optimization-options) for all options. diff --git a/apps/docs/content/docs/optimizers/native-activity-indicator.mdx b/apps/docs/content/docs/optimizers/native-activity-indicator.mdx new file mode 100644 index 00000000..83fa433f --- /dev/null +++ b/apps/docs/content/docs/optimizers/native-activity-indicator.mdx @@ -0,0 +1,34 @@ +--- +title: Native Activity Indicator +description: How Boost prepares the native ActivityIndicator tree and when it keeps the wrapper. +--- + +React Native's `ActivityIndicator` component renders an outer native `View` and a platform spinner. Boost creates those same two native elements at build time, removing a fiber from the tree. + +It also applies the platform defaults for color, size, visibility, and layout. Dynamic direct values use small runtime helpers when needed. + +## When it runs + +The optimizer can usually replace an `ActivityIndicator` when: + +- It is imported from `react-native`. +- The build targets iOS or Android. +- Its props are written directly on the element. +- It has no children. +- Moving its props to the two generated native elements keeps their evaluation order safe. + +## When it skips + +Boost keeps the wrapper when it finds: + +- An unknown or unsupported target platform. +- Spread props. +- Children. +- A prop expression with side effects, such as a function call. +- A Unistyles style, or a style that might come from Unistyles. + +These checks prevent Boost from changing prop precedence, execution order, or style reactivity. + +## Configuration + +The optimizer is enabled by default. Set `native-activity-indicator` to `off` to disable it. diff --git a/apps/docs/content/docs/optimizers/native-image.mdx b/apps/docs/content/docs/optimizers/native-image.mdx new file mode 100644 index 00000000..6e1f2f60 --- /dev/null +++ b/apps/docs/content/docs/optimizers/native-image.mdx @@ -0,0 +1,37 @@ +--- +title: Native Image +description: How Boost moves React Native Image wrapper work to build time and when it skips an image. +--- + +The Native Image optimizer replaces React Native's `Image` component, which is a JavaScript-based wrapper component, with its native host component. + +The wrapper normally prepares sources, dimensions, styles, request headers, and accessibility props during rendering. Boost moves this work to build time when possible. Dynamic sources use a small runtime helper when they can still be handled safely. + +## When it runs + +The optimizer can usually replace an `Image` when: + +- It is imported from `react-native`. +- The build targets iOS or Android. +- It has a supported `source`, `src`, or static `srcSet`. +- Its wrapper-level props can be safely prepared or passed through. +- On Android 0.83–0.84, Boost can prove that it is not inside a `Text`. Boost also requires this proof when it cannot detect the React Native version. + +## When it skips + +Boost keeps the `Image` wrapper when it finds: + +- No image source. +- A dynamic or malformed `srcSet`. +- Unproved load callbacks, `onProgress`, `onPartialLoad`, a `ref`, children, or another prop that requires the wrapper. +- A spread that might contain Image wrapper props. +- A `srcSet` combination whose dimensions or styles cannot be resolved safely. +- On Android 0.83–0.84, or when the version is unknown, a parent, runtime parent, or unknown ancestor that might provide a text context. +- A Unistyles style, or a style that might come from Unistyles. Unistyles does not provide a lean Image host. +- An unknown or unsupported target platform. + +Text ancestry does not select the Image host on iOS or Android 0.85 and later. Unknown ancestors can still inspect or clone an Image and inject props. Boost keeps these sites on the wrapper, even with `unknownAncestorsDoNotRenderText` enabled. + +## Configuration + +The optimizer is enabled by default. Set `native-image` to `off` to disable it. diff --git a/apps/docs/content/docs/optimizers/native-text.mdx b/apps/docs/content/docs/optimizers/native-text.mdx new file mode 100644 index 00000000..f3730ff5 --- /dev/null +++ b/apps/docs/content/docs/optimizers/native-text.mdx @@ -0,0 +1,50 @@ +--- +title: Native Text +description: How Boost removes the React Native Text wrapper and when it keeps it. +--- + +The Native Text optimizer replaces React Native's `Text` component, which is a JavaScript-based wrapper component, with its native host component. + +```tsx +// Before +Hello + +// After, simplified +Hello +``` + +Boost adds the same defaults and converts the same accessibility and style props that the wrapper would handle at runtime. + +## When it runs + +The optimizer can usually replace a `Text` when: + +- It is imported from `react-native`. +- Its children are known to be text or numbers. +- It is not inside another `Text`. +- It has no press or responder behavior that needs the wrapper. +- Boost can determine its text context. + +Direct accessibility props, `id`, `numberOfLines`, `selectionColor`, and supported text styles are normalized before the wrapper is removed. + +Boost preserves authored style entries and React Native's override order so public style preprocessors receive the same inputs. Dynamic styles are inspected on each call, without an identity cache. + +## When it skips + +Boost keeps the `Text` wrapper when it finds: + +- Press or responder props such as `onPress`, `onLongPress`, or `onResponderGrant`. +- Children that might be React elements instead of text. +- A parent `Text`, which requires React Native's inline text host. +- An unknown parent or runtime caller that might place it inside a `Text`. +- A spread that might contain props Boost must convert. +- A dynamic `id` together with `nativeID`, where their precedence is unclear. +- Disabled-state reconciliation that could mutate a shared accessibility state object before React renders the `Text`. +- An `expo-router` `Link` with `asChild`, which makes its child pressable. +- An unknown style source while Unistyles support is active. + +If your project guarantees that unknown parents never add a text context, you can use [`unknownAncestorsDoNotRenderText`](/docs/configuration/configure#plugin-assumptions-unknownancestorsdonotrendertext) to increase optimization coverage. + +## Configuration + +The optimizer is enabled by default. Set `native-text` to `off` to disable it. diff --git a/apps/docs/content/docs/optimizers/native-view.mdx b/apps/docs/content/docs/optimizers/native-view.mdx new file mode 100644 index 00000000..0f5d6cf6 --- /dev/null +++ b/apps/docs/content/docs/optimizers/native-view.mdx @@ -0,0 +1,38 @@ +--- +title: Native View +description: How Boost removes the React Native View wrapper and when it keeps it. +--- + +The Native View optimizer replaces React Native's `View` wrapper with its native host component. It converts props such as accessibility attributes, `tabIndex`, and `id` to preserve their behavior. + +```tsx +// Before + + +// After, simplified + +``` + +Static values are converted at build time. Dynamic values use minimal runtime helpers where needed. + +## Nested components + +In order to optimize a `View` into a `NativeView`, Boost needs to classify its ancestors. If it has an ancestor that is a `Text` or `TextInput` (or Boost is not sure if it has), the native host component cannot be used directly. In these cases, Boost still tries to perform a partial optimization by using a small wrapper that preserves text context instead of the full View wrapper. + +Boost recognizes common parents such as `Pressable`, `TouchableOpacity`, and `ImageBackground` as safe ancestors for optimization. It also recognizes `Animated.View` from React Native and Reanimated without removing animation behavior. + + +## When it skips + +Boost keeps the original `View` when it cannot preserve behavior. Common reasons include: + +- A parent might inspect or change its children. This includes unknown custom parents and some scroll, list, and touchable components. +- A prop spread might contain values that need conversion. +- Prop conversion could change evaluation order or precedence. +- Unistyles styles or parent context cannot be resolved. + +If your project guarantees that unknown parents never add text context, [`unknownAncestorsDoNotRenderText`](/docs/configuration/configure#plugin-assumptions-unknownancestorsdonotrendertext) can increase coverage. Enable it only when that guarantee holds. + +## Configuration + +The optimizer is enabled by default. Set `native-view` to `off` to disable it. diff --git a/apps/docs/content/docs/optimizers/platform-folding.mdx b/apps/docs/content/docs/optimizers/platform-folding.mdx new file mode 100644 index 00000000..93059791 --- /dev/null +++ b/apps/docs/content/docs/optimizers/platform-folding.mdx @@ -0,0 +1,37 @@ +--- +title: Platform Folding +description: Resolves React Native Platform values during the build. +--- + +The Platform Folding optimizer resolves React Native `Platform.select` calls and `Platform.OS` uses before the other Boost optimizers run. While Metro and Expo already perform similar release transforms in a later stage of the pipeline, that is too late for Boost to optimize any JSX and styles that make use of the `Platform` module. This early pass can therefore remove these runtime helpers earlier and increase optimization coverage of other Boost optimizers. + +```jsx +import { Platform } from 'react-native'; + +const color = Platform.select({ ios: 'blue', android: 'green', default: 'red' }); +const cacheKey = `${Platform.OS}-cache`; +const screen = Platform.OS === 'ios' ? : ; +if (Platform.OS === 'android') require('./install-android'); +``` + +For an iOS build, this becomes the equivalent of: + +```jsx +const color = 'blue'; +const cacheKey = 'ios-cache'; +const screen = ; +``` + +`Platform.select` follows React Native's fallback order: the target platform, `native`, then `default`. The optimizer requires a plain object literal. It skips computed keys, spreads, getters, and additional arguments. Branch folding requires a strict (`===` or `!==`) comparison between `Platform.OS` and a string literal. + +## Configuration + +The optimizer is enabled by default. You can disable it in `metro.config.js`: + +```js +module.exports = withBoostConfig(config, { + optimizations: { + 'platform-folding': 'off', + }, +}); +``` diff --git a/apps/docs/content/docs/optimizers/stylesheet-operations.mdx b/apps/docs/content/docs/optimizers/stylesheet-operations.mdx new file mode 100644 index 00000000..cf0049a6 --- /dev/null +++ b/apps/docs/content/docs/optimizers/stylesheet-operations.mdx @@ -0,0 +1,68 @@ +--- +title: StyleSheet Operations +description: Evaluates simple StyleSheet operations during the build. +--- + +The StyleSheet Operations optimizer moves simple `StyleSheet.flatten` and `StyleSheet.compose` work from the runtime to build time. + +## Flattening static styles + +When every style value is known during the build, Boost can create the final object ahead of time: + +```jsx +const style = StyleSheet.flatten([ + { padding: 8, color: 'red' }, + null, + [{ color: 'blue' }, { opacity: 0.5 }], +]); +``` + +This becomes the equivalent of: + +```jsx +const style = { + padding: 8, + color: 'blue', + opacity: 0.5, +}; +``` + +Later style values still override earlier values. Boost does not process colors, transforms, or other style values. React Native continues to handle those values normally. + +## Composing static styles + +Boost can also simplify `StyleSheet.compose` when it knows whether either value is empty: + +```jsx +const style = StyleSheet.compose(null, styles.card); +``` + +This becomes the equivalent of: + +```jsx +const style = styles.card; +``` + +When both values are static, Boost replaces the call with the same two-item style array that React Native would create. + +## When Boost skips an operation + +Boost leaves the call unchanged when the result depends on runtime data: + +```jsx +const style = StyleSheet.flatten([styles.card, active && styles.active]); +``` + +It also skips calls from other style libraries. The optimizer only handles `StyleSheet` imported from `react-native`. + +## Configuration + +The optimizer is enabled by default. You can disable it in `metro.config.js`: + +```js +module.exports = withBoostConfig(config, { + optimizations: { + 'stylesheet-operations': 'off', + }, +}); +``` diff --git a/apps/docs/next.config.mjs b/apps/docs/next.config.mjs index 91c6ac17..41252d91 100644 --- a/apps/docs/next.config.mjs +++ b/apps/docs/next.config.mjs @@ -5,6 +5,15 @@ const withMDX = createMDX(); /** @type {import('next').NextConfig} */ const config = { reactStrictMode: true, + async redirects() { + return [ + { + source: '/docs/information/optimization-coverage', + destination: '/docs/optimizers', + permanent: true, + }, + ]; + }, async rewrites() { return [ { diff --git a/apps/docs/package.json b/apps/docs/package.json index 30ce7317..0c1e461a 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -11,24 +11,24 @@ "postinstall": "fumadocs-mdx" }, "dependencies": { - "fumadocs-core": "16.15.4", + "fumadocs-core": "16.15.5", "fumadocs-mdx": "15.4.0", - "fumadocs-typescript": "^5.2.7", - "fumadocs-ui": "16.15.4", - "lucide-react": "^1.38.0", + "fumadocs-typescript": "^5.3.0", + "fumadocs-ui": "16.15.5", + "lucide-react": "^1.39.0", "mermaid": "^11.17.2", - "next": "16.3.3", - "react": "19.2.3", - "react-dom": "19.2.3", - "ts-morph": "^27.0.2" + "next": "16.3.4", + "react": "19.2.8", + "react-dom": "19.2.8", + "ts-morph": "^28.0.0" }, "devDependencies": { "@tailwindcss/postcss": "^4.3.3", "@types/mdx": "^2.0.14", - "@types/node": "^24", - "@types/react": "^19.2.14", - "@types/react-dom": "^19.2.3", + "@types/node": "^26.4.1", + "@types/react": "^19.2.18", + "@types/react-dom": "^19.2.0", "tailwindcss": "^4.3.3", - "typescript": "^6.0.3" + "typescript": "^7.0.2" } } diff --git a/apps/docs/vercel.json b/apps/docs/vercel.json new file mode 100644 index 00000000..74079587 --- /dev/null +++ b/apps/docs/vercel.json @@ -0,0 +1,3 @@ +{ + "installCommand": "pnpm install --frozen-lockfile" +} diff --git a/apps/example/app.json b/apps/example/app.json index 7838c201..e90ac7f4 100644 --- a/apps/example/app.json +++ b/apps/example/app.json @@ -31,7 +31,8 @@ "resizeMode": "contain", "backgroundColor": "#ffffff" } - ] + ], + "expo-status-bar" ] } } diff --git a/apps/example/babel.config.js b/apps/example/babel.config.js index f8f8eca3..b210fbf9 100644 --- a/apps/example/babel.config.js +++ b/apps/example/babel.config.js @@ -1,21 +1,14 @@ module.exports = function (api) { api.cache(true); + if (process.env.EXPO_PUBLIC_UNIWIND_PARITY === '1') return { presets: ['babel-preset-expo'] }; return { presets: ['babel-preset-expo'], - plugins: [ - [ - 'react-native-boost/plugin', - { - unistyles: true, - ignores: ['node_modules/**', '../../node_modules/**', '**/*.unoptimized.tsx'], - }, - ], - [ - 'react-native-unistyles/plugin', - { - root: 'src', - }, - ], + overrides: [ + { + // The runtime and RN-only benchmark must not receive Unistyles wrappers. + exclude: (filename) => /react-native-boost[/\\]dist[/\\]|view-context-(benchmark|parity)/.test(filename ?? ''), + plugins: [['react-native-unistyles/plugin', { root: 'src' }]], + }, ], }; }; diff --git a/apps/example/index.ts b/apps/example/index.ts index 5cf48400..fb2fca46 100644 --- a/apps/example/index.ts +++ b/apps/example/index.ts @@ -1,11 +1,16 @@ +/* eslint-disable @typescript-eslint/no-require-imports,unicorn/prefer-module */ + // Must be the first import: bakes the benchmark `core` profile's RN feature-flag overrides in before // react-native's Text module evaluates (no-op outside the benchmark). See the module's @remarks. import './src/benchmark/feature-flags'; -import './src/unistyles'; +if (process.env.EXPO_PUBLIC_UNIWIND_PARITY !== '1') require('./src/unistyles'); import { registerRootComponent } from 'expo'; -import App from './src/app'; +const App = + process.env.EXPO_PUBLIC_UNIWIND_PARITY === '1' + ? require('./src/screens/uniwind-parity').default + : require('./src/app').default; // registerRootComponent calls AppRegistry.registerComponent('main', () => App); // It also ensures that whether you load the app in Expo Go or in a native build, diff --git a/apps/example/metro.config.js b/apps/example/metro.config.js new file mode 100644 index 00000000..51d8e9ab --- /dev/null +++ b/apps/example/metro.config.js @@ -0,0 +1,15 @@ +const { getDefaultConfig } = require('expo/metro-config'); +const { withBoostConfig } = require('react-native-boost/metro'); + +const uniwindParity = process.env.EXPO_PUBLIC_UNIWIND_PARITY === '1'; +const config = uniwindParity + ? require('uniwind/metro').withUniwindConfig(getDefaultConfig(__dirname), { + cssEntryFile: './src/screens/uniwind-parity/global.css', + dtsFile: './uniwind-types.d.ts', + }) + : getDefaultConfig(__dirname); + +module.exports = withBoostConfig(config, { + integrations: { unistyles: uniwindParity ? 'off' : 'on' }, + ignores: ['node_modules/**', '../../node_modules/**', '**/*.unoptimized.tsx'], +}); diff --git a/apps/example/package.json b/apps/example/package.json index 7bf3d42b..cc9f365b 100644 --- a/apps/example/package.json +++ b/apps/example/package.json @@ -9,7 +9,7 @@ "preandroid": "node scripts/gen-unoptimized.mjs", "preios": "node scripts/gen-unoptimized.mjs", "preweb": "node scripts/gen-unoptimized.mjs", - "pretypecheck": "node scripts/gen-unoptimized.mjs", + "pretypecheck": "pnpm --filter react-native-boost build && node scripts/gen-unoptimized.mjs", "typecheck": "tsc --noEmit", "start": "expo start", "dev": "expo start", @@ -18,28 +18,29 @@ "web": "expo start --web" }, "dependencies": { - "@expo/metro-runtime": "~57.0.3", - "@react-native-community/slider": "5.2.0", - "@react-navigation/native": "^7.3.3", - "@react-navigation/native-stack": "^7.17.5", - "expo": "^57.0.4", - "expo-splash-screen": "~57.0.2", - "expo-status-bar": "~57.0.0", + "@expo/metro-runtime": "~57.0.15", + "@react-native-community/slider": "5.2.1", + "@react-navigation/native": "^7.3.18", + "@react-navigation/native-stack": "^7.18.10", + "expo": "^57.0.19", + "expo-splash-screen": "~57.0.8", + "expo-status-bar": "~57.0.1", "react": "19.2.3", "react-dom": "19.2.3", - "react-native": "0.86.0", + "react-native": "0.86.3", "react-native-boost": "workspace:*", "react-native-nitro-modules": "0.37.1", "react-native-safe-area-context": "~5.7.0", - "react-native-screens": "4.25.2", + "react-native-screens": "4.26.2", "react-native-time-to-render": "workspace:*", "react-native-unistyles": "3.3.0", - "react-native-web": "~0.21.0" + "react-native-web": "~0.21.0", + "uniwind": "1.12.0" }, "devDependencies": { "@babel/core": "^7.29.7", - "@types/react": "~19.2.14", - "typescript": "~6.0.3" + "@types/react": "~19.2.18", + "typescript": "~7.0.2" }, "private": true } diff --git a/apps/example/scripts/gen-unoptimized.mjs b/apps/example/scripts/gen-unoptimized.mjs index a46c93ce..cb200230 100644 --- a/apps/example/scripts/gen-unoptimized.mjs +++ b/apps/example/scripts/gen-unoptimized.mjs @@ -9,7 +9,11 @@ import { fileURLToPath } from 'node:url'; const scriptDirectory = dirname(fileURLToPath(import.meta.url)); const projectRoot = join(scriptDirectory, '..'); -const canonicalSources = ['src/screens/trading-demo/components/rows.tsx']; +const canonicalSources = [ + 'src/screens/uniwind-parity/cases.tsx', + 'src/screens/trading-demo/components/rows.tsx', + 'src/screens/view-context-benchmark/box.tsx', +]; const header = (sourceRelativePath) => [ diff --git a/apps/example/scripts/view-context-benchmark.tsx b/apps/example/scripts/view-context-benchmark.tsx new file mode 100644 index 00000000..cdfb5708 --- /dev/null +++ b/apps/example/scripts/view-context-benchmark.tsx @@ -0,0 +1,142 @@ +import { AppRegistry, Platform, Text, unstable_TextAncestorContext as TextContext } from 'react-native'; +import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react'; +import type { ReactNode } from 'react'; +import { NativeView } from 'react-native-boost/runtime'; +import { startMarker, TimeToRenderView, getThermalState } from 'react-native-time-to-render'; +import { Box } from '../src/screens/view-context-benchmark/box'; +import { Box as StockBox } from '../src/screens/view-context-benchmark/box.unoptimized'; +import { ViewContextParity } from './view-context-parity'; + +function RawBox({ children, tick }: { children: ReactNode; tick: number }) { + return ( + + {children} + + ); +} + +const { enableNativeViewPropTransformations } = + require('react-native/src/private/featureflags/ReactNativeFeatureFlags') as { + enableNativeViewPropTransformations: () => boolean; + }; +const variants = { stock: StockBox, context: Box, raw: RawBox }; +type Mode = keyof typeof variants; +type Step = { mode: Mode; count: number; insideText: boolean; tick: number; marker: string }; +const server = process.env.EXPO_PUBLIC_BENCHMARK_SERVER ?? 'http://127.0.0.1:8099'; +const child = ; +const delay = (milliseconds: number) => new Promise((resolve) => setTimeout(resolve, milliseconds)); + +function ViewContextBenchmark() { + const [verified, setVerified] = useState(false); + const verificationComplete = useCallback(() => setVerified(true), []); + const [step, setStep] = useState(null); + const [status, setStatus] = useState('View context benchmark'); + const started = useRef(0); + const committed = useRef(0); + const complete = useRef<(paintMs: number) => void>(() => {}); + + useLayoutEffect(() => { + if (step) committed.current = performance.now() - started.current; + }, [step]); + + useEffect(() => { + if (!verified) return; + let cancelled = false; + void (async () => { + if (__DEV__) throw new Error('This benchmark requires a release bundle'); + const plan: { loads: number[]; replicates: number } = await fetch(`${server}/plan`).then((response) => + response.json() + ); + const samples = []; + let sequence = 0; + const measure = (mode: Mode, count: number, insideText: boolean, tick: number) => + new Promise<{ commitMs: number; paintMs: number }>((resolve) => { + const marker = `view-context-${sequence++}`; + requestAnimationFrame((time) => { + startMarker(marker, time); + complete.current = (paintMs) => resolve({ commitMs: committed.current, paintMs }); + started.current = performance.now(); + setStep({ mode, count, insideText, tick, marker }); + }); + }); + + for (let replicate = 0; replicate < plan.replicates; replicate++) { + const loads = replicate % 2 ? [...plan.loads].reverse() : plan.loads; + for (const count of loads) { + for (const insideText of replicate % 2 ? [true, false] : [false, true]) { + // Balance variant order across measured rounds. + const modes: Mode[] = ['stock', 'context', 'raw']; + const offset = replicate % modes.length; + const order = [...modes.slice(offset), ...modes.slice(0, offset)]; + if (Math.floor(replicate / modes.length) % 2) order.reverse(); + for (const mode of order) { + if (cancelled) return; + setStep(null); + await delay(250); + for (const tick of [0, 1]) { + const thermalStart = getThermalState(); + const timing = await measure(mode, count, insideText, tick); + const sample = { + mode, + count, + insideText, + replicate, + phase: tick === 0 ? 'mount' : 'update', + thermalStart, + thermalEnd: getThermalState(), + ...timing, + }; + samples.push(sample); + await fetch(`${server}/measure`, { method: 'POST', body: JSON.stringify(sample) }); + await delay(100); + } + } + } + } + } + await fetch(`${server}/done`, { + method: 'POST', + body: JSON.stringify({ + platform: Platform.OS, + reactNative: Platform.constants.reactNativeVersion, + engine: 'HermesInternal' in globalThis ? 'Hermes' : 'other', + development: __DEV__, + lifecycleParity: verified, + nativeViewPropTransformations: enableNativeViewPropTransformations(), + samples, + }), + }); + setStep(null); + setStatus('View context benchmark complete'); + })().catch((error: Error) => setStatus(error.message)); + return () => { + cancelled = true; + }; + }, [verified]); + + if (!verified) return ; + const Component = step ? variants[step.mode] : Box; + return ( + + {status} + {step && ( + + + {Array.from({ length: step.count }, (_, index) => ( + + {child} + + ))} + + complete.current(event.nativeEvent.paintTime)} + /> + + )} + + ); +} + +AppRegistry.registerComponent('main', () => ViewContextBenchmark); diff --git a/apps/example/scripts/view-context-parity.tsx b/apps/example/scripts/view-context-parity.tsx new file mode 100644 index 00000000..384c7932 --- /dev/null +++ b/apps/example/scripts/view-context-parity.tsx @@ -0,0 +1,73 @@ +import { cloneElement, use, useCallback, useLayoutEffect, useRef, useState } from 'react'; +import { View, unstable_TextAncestorContext as TextContext } from 'react-native'; +import { NativeViewWithContext } from 'react-native-boost/runtime'; + +const steps = [ + { insideText: false, key: 'a' }, + { insideText: false, key: 'a' }, + { insideText: true, key: 'a' }, + { insideText: true, key: 'a' }, + { insideText: false, key: 'a' }, + { insideText: false, key: 'b' }, +]; + +function Probe({ allocate, report }: { allocate: () => number; report: (event: string) => void }) { + const [instance] = useState(allocate); + const insideText = use(TextContext); + useLayoutEffect(() => { + if (insideText) throw new Error('View did not reset Text context'); + report(`render:${instance}`); + }); + useLayoutEffect(() => () => report(`unmount:${instance}`), [instance, report]); + return null; +} + +export function ViewContextParity({ onComplete }: { onComplete: () => void }) { + const [index, setIndex] = useState(0); + const baseline = useRef([]); + const events = useRef([]); + const identifier = useRef(0); + const allocate = useCallback(() => identifier.current++, []); + const report = useCallback((event: string) => events.current.push(event), []); + const captureRef = useCallback((instance: unknown) => { + events.current.push(instance ? 'ref:attach' : 'ref:detach'); + return () => { + events.current.push('ref:cleanup'); + }; + }, []); + const phase = index % (steps.length + 1); + const stock = index < steps.length + 1; + const Component = stock ? View : NativeViewWithContext; + const step = steps[phase]; + + useLayoutEffect(() => { + if (!step) { + if (stock) { + baseline.current = [...events.current]; + events.current = []; + identifier.current = 0; + } else { + if (JSON.stringify(events.current) !== JSON.stringify(baseline.current)) { + throw new Error( + `View lifecycle mismatch: ${JSON.stringify({ baseline: baseline.current, context: events.current })}` + ); + } + onComplete(); + return; + } + } + const frame = requestAnimationFrame(() => setIndex((value) => value + 1)); + return () => cancelAnimationFrame(frame); + }, [index, onComplete, step, stock]); + + return step ? ( + + {cloneElement( + + + , + { testID: `cloned-${index}` } + )} + + ) : null; +} diff --git a/apps/example/src/screens/benchmark.tsx b/apps/example/src/screens/benchmark.tsx index 8f732e64..8b201cfc 100644 --- a/apps/example/src/screens/benchmark.tsx +++ b/apps/example/src/screens/benchmark.tsx @@ -1,6 +1,6 @@ -import { Image, Pressable, StyleSheet, Text, View } from 'react-native'; +import { ActivityIndicator, Image, Pressable, StyleSheet, Text, View } from 'react-native'; import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; -import { useMemo, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { startMarker } from 'react-native-time-to-render'; import { Benchmark, BenchmarkStep } from '../types'; import MeasureComponent from '../components/measure-component'; @@ -21,6 +21,13 @@ const benchmarks = [ unoptimizedComponent: , optimizedComponent: , }, + { + title: 'ActivityIndicator', + count: 2000, + // @boost-ignore + unoptimizedComponent: , + optimizedComponent: , + }, { title: 'Image', count: 2000, @@ -48,37 +55,35 @@ const benchmarks = [ }, ] satisfies Benchmark[]; +type RunStatus = 'idle' | 'settling' | 'measuring' | 'complete'; +type ScheduledStep = { benchmarkIndex: number; step: BenchmarkStep }; + +const SETTLE_DELAY_MS = 500; + export default function BenchmarkScreen() { const insets = useSafeAreaInsets(); + const [selectedBenchmarks, setSelectedBenchmarks] = useState(() => benchmarks.map(() => true)); + const [schedule, setSchedule] = useState([]); const [currentStepIndex, setCurrentStepIndex] = useState(0); - const [runBenchmark, setRunBenchmark] = useState(false); + const [runStatus, setRunStatus] = useState('idle'); const [results, setResults] = useState>({}); - const totalSteps = benchmarks.length * 2; - const currentBenchmark = useMemo(() => Math.floor(currentStepIndex / 2), [currentStepIndex]); - const currentStep = useMemo( - () => (currentStepIndex % 2 === 0 ? BenchmarkStep.Unoptimized : BenchmarkStep.Optimized), - [currentStepIndex] - ); - - const progress = useMemo<[number, number]>(() => { - return [currentStepIndex, totalSteps]; - }, [currentStepIndex, totalSteps]); - - const buttonTitle = useMemo(() => { - if (currentStepIndex === 0) { - return 'Start Benchmark'; - } - if (currentStepIndex === totalSteps - 1) { - return 'Last Step'; - } - return 'Next Step'; - }, [currentStepIndex, totalSteps]); - - const markerName = useMemo( - () => getMarkerName(benchmarks[currentBenchmark].title, currentStep), - [currentBenchmark, currentStep] - ); + const activeStep = schedule[currentStepIndex]; + const activeBenchmark = activeStep ? benchmarks[activeStep.benchmarkIndex] : undefined; + const isRunning = runStatus === 'settling' || runStatus === 'measuring'; + const selectedCount = selectedBenchmarks.filter(Boolean).length; + const markerName = activeStep ? getMarkerName(benchmarks[activeStep.benchmarkIndex].title, activeStep.step) : ''; + const subtitle = + isRunning && activeStep + ? `Step ${currentStepIndex + 1} / ${schedule.length}: ${activeBenchmark?.title} (${activeStep.step})` + : runStatus === 'complete' + ? 'Benchmark complete' + : `${selectedCount} of ${benchmarks.length} benchmarks selected`; + const buttonTitle = isRunning + ? `Running ${currentStepIndex + 1} / ${schedule.length}` + : runStatus === 'complete' + ? 'Run Selected Again' + : 'Run Selected'; const resultRows = useMemo(() => { return benchmarks.map((benchmark, index) => { @@ -99,28 +104,74 @@ export default function BenchmarkScreen() { }); }, [results]); - const handleRun = (timestamp: number) => { - startMarker(markerName, timestamp); - setRunBenchmark(true); + // Let cleanup and result UI work finish before the next measurement starts. + useEffect(() => { + if (runStatus !== 'settling' || !activeStep) return; + + let frame: number | undefined; + const timeout = setTimeout(() => { + frame = requestAnimationFrame((timestamp) => { + startMarker(markerName, timestamp); + setRunStatus('measuring'); + }); + }, SETTLE_DELAY_MS); + + return () => { + clearTimeout(timeout); + if (frame !== undefined) cancelAnimationFrame(frame); + }; + }, [activeStep, markerName, runStatus]); + + const handleRun = () => { + const nextSchedule = selectedBenchmarks.flatMap((selected, benchmarkIndex) => + selected + ? [ + { benchmarkIndex, step: BenchmarkStep.Unoptimized }, + { benchmarkIndex, step: BenchmarkStep.Optimized }, + ] + : [] + ); + if (nextSchedule.length === 0) return; + + setSchedule(nextSchedule); + setCurrentStepIndex(0); + setResults({}); + setRunStatus('settling'); + }; + + const handleToggleBenchmark = (benchmarkIndex: number) => { + if (isRunning) return; + setSelectedBenchmarks((current) => + current.map((selected, index) => (index === benchmarkIndex ? !selected : selected)) + ); + if (runStatus === 'complete') setRunStatus('idle'); }; const handleRenderTimeChange = (renderTime: number) => { - setRunBenchmark(false); + if (!activeStep) return; setResults((previousResults) => { - const baseResults = currentStepIndex === 0 ? {} : previousResults; - const previousBenchmarkResult = baseResults[currentBenchmark] ?? { unoptimized: null, optimized: null }; + const previousBenchmarkResult = previousResults[activeStep.benchmarkIndex] ?? { + unoptimized: null, + optimized: null, + }; return { - ...baseResults, - [currentBenchmark]: - currentStep === BenchmarkStep.Unoptimized + ...previousResults, + [activeStep.benchmarkIndex]: + activeStep.step === BenchmarkStep.Unoptimized ? { unoptimized: renderTime, optimized: null } : { ...previousBenchmarkResult, optimized: renderTime }, }; }); - setCurrentStepIndex((previousStepIndex) => (previousStepIndex + 1) % totalSteps); + const nextStepIndex = currentStepIndex + 1; + if (nextStepIndex < schedule.length) { + setCurrentStepIndex(nextStepIndex); + setRunStatus('settling'); + } else { + setRunStatus('complete'); + } }; return ( @@ -128,10 +179,7 @@ export default function BenchmarkScreen() { React Native Boost Benchmark - {`Step ${progress[0] + 1} / ${progress[1]}: ${benchmarks[currentBenchmark].title} (${currentStep})`} + {subtitle} @@ -148,9 +196,22 @@ export default function BenchmarkScreen() { style={[ styles.tableRow, index % 2 === 0 ? styles.tableStripeLight : styles.tableStripeDark, - index === currentBenchmark && styles.tableActiveRow, + isRunning && index === activeStep?.benchmarkIndex && styles.tableActiveRow, + !selectedBenchmarks[index] && styles.tableRowDisabled, ]}> - {row.title} + handleToggleBenchmark(index)} + style={[styles.tableCell, styles.benchmarkColumn, styles.benchmarkSelector]}> + + {selectedBenchmarks[index] && } + + {row.title} + {row.unoptimizedText} {row.optimizedText} handleRun(event.nativeEvent.timestamp)} - style={({ pressed }) => [styles.runButton, pressed && styles.runButtonPressed]}> + accessibilityState={{ disabled: isRunning || selectedCount === 0 }} + disabled={isRunning || selectedCount === 0} + onPress={handleRun} + style={({ pressed }) => [ + styles.runButton, + (isRunning || selectedCount === 0) && styles.runButtonDisabled, + pressed && styles.runButtonPressed, + ]}> {buttonTitle} - {runBenchmark && ( + {runStatus === 'measuring' && activeStep && activeBenchmark && ( )} @@ -238,6 +305,9 @@ const styles = StyleSheet.create({ transform: [{ scale: 0.985 }], opacity: 0.95, }, + runButtonDisabled: { + opacity: 0.5, + }, runButtonText: { color: '#0b0e11', fontSize: 15, @@ -280,6 +350,9 @@ const styles = StyleSheet.create({ tableActiveRow: { backgroundColor: '#1f2a36', }, + tableRowDisabled: { + opacity: 0.45, + }, tableCell: { paddingHorizontal: 10, paddingVertical: 8, @@ -291,6 +364,30 @@ const styles = StyleSheet.create({ flex: 1, alignItems: 'flex-end', }, + benchmarkSelector: { + flexDirection: 'row', + alignItems: 'center', + gap: 8, + }, + checkbox: { + width: 18, + height: 18, + borderRadius: 4, + borderWidth: 1, + borderColor: '#6c7480', + alignItems: 'center', + justifyContent: 'center', + }, + checkboxSelected: { + backgroundColor: '#f0b90b', + borderColor: '#f0b90b', + }, + checkboxMark: { + width: 8, + height: 8, + borderRadius: 2, + backgroundColor: '#0b0e11', + }, benchmarkText: { fontSize: 14, color: '#eaecef', diff --git a/apps/example/src/screens/uniwind-parity/cases.tsx b/apps/example/src/screens/uniwind-parity/cases.tsx new file mode 100644 index 00000000..74ea3679 --- /dev/null +++ b/apps/example/src/screens/uniwind-parity/cases.tsx @@ -0,0 +1,61 @@ +/* eslint-disable @typescript-eslint/no-require-imports,unicorn/prefer-module */ + +import { useEffect, useRef } from 'react'; +import { ActivityIndicator, Animated, Image, Platform, StyleSheet, Text, View } from 'react-native'; + +export default function Cases({ selected }: { selected: boolean }) { + const position = useRef(new Animated.ValueXY({ x: 0, y: 0 })).current; + // @ts-expect-error RN 0.86 exports Animated.Color without a public type. + const color = useRef(new Animated.Color('#fed7aa')).current; + useEffect(() => { + color.setValue(selected ? '#bbf7d0' : '#fed7aa'); + }, [color, selected]); + return ( + + + View + Text + + + + Disabled / data + + {selected ? 'Selected' : 'Not selected'} + + + + This text must stop after two lines with the same size and color. + + + + + + + + Animated wrapper + + + Animated values + + + {Platform.OS}: style wins + + + ); +} diff --git a/apps/example/src/screens/uniwind-parity/global.css b/apps/example/src/screens/uniwind-parity/global.css new file mode 100644 index 00000000..ae108db0 --- /dev/null +++ b/apps/example/src/screens/uniwind-parity/global.css @@ -0,0 +1,2 @@ +@import 'tailwindcss'; +@import 'uniwind'; diff --git a/apps/example/src/screens/uniwind-parity/global.d.ts b/apps/example/src/screens/uniwind-parity/global.d.ts new file mode 100644 index 00000000..35306c6f --- /dev/null +++ b/apps/example/src/screens/uniwind-parity/global.d.ts @@ -0,0 +1 @@ +declare module '*.css'; diff --git a/apps/example/src/screens/uniwind-parity/index.tsx b/apps/example/src/screens/uniwind-parity/index.tsx new file mode 100644 index 00000000..078c6c65 --- /dev/null +++ b/apps/example/src/screens/uniwind-parity/index.tsx @@ -0,0 +1,46 @@ +import './global.css'; +import { useState } from 'react'; +import { Pressable, SafeAreaView, Text, View } from 'react-native'; +import { ScopedTheme } from 'uniwind'; +import Cases from './cases'; +import OriginalCases from './cases.unoptimized'; + +export default function UniwindParity() { + const [dark, setDark] = useState(false); + const [selected, setSelected] = useState(false); + return ( + + + Uniwind parity + + setDark(!dark)} + style={{ padding: 10, backgroundColor: '#e2e8f0', borderRadius: 8 }}> + Theme: {dark ? 'dark' : 'light'} + + setSelected(!selected)} + style={{ padding: 10, backgroundColor: '#e2e8f0', borderRadius: 8 }}> + Toggle selected + + + + + Original + + + + + + Boost + + + + + + + + ); +} diff --git a/apps/example/src/screens/view-context-benchmark/box.tsx b/apps/example/src/screens/view-context-benchmark/box.tsx new file mode 100644 index 00000000..18b0c6fd --- /dev/null +++ b/apps/example/src/screens/view-context-benchmark/box.tsx @@ -0,0 +1,10 @@ +import type { ReactNode } from 'react'; +import { View } from 'react-native'; + +export function Box({ children, tick }: { children: ReactNode; tick: number }) { + return ( + + {children} + + ); +} diff --git a/apps/example/uniwind-types.d.ts b/apps/example/uniwind-types.d.ts new file mode 100644 index 00000000..ef1d8f81 --- /dev/null +++ b/apps/example/uniwind-types.d.ts @@ -0,0 +1,10 @@ +// NOTE: This file is generated by uniwind and it should not be edited manually. +/// + +declare module 'uniwind' { + export interface UniwindConfig { + themes: readonly ['light', 'dark']; + } +} + +export {}; diff --git a/package.json b/package.json index 547809c6..a70cddb2 100644 --- a/package.json +++ b/package.json @@ -15,14 +15,14 @@ "format": "oxfmt .", "prepare": "husky" }, - "packageManager": "pnpm@10.28.2", + "packageManager": "pnpm@12.3.1", "devDependencies": { "@commitlint/cli": "^21.2.2", "@commitlint/config-conventional": "^21.2.2", "husky": "^9.1.7", "lint-staged": "^17.4.1", - "oxfmt": "^0.65.0", - "oxlint": "^1.80.0", - "typescript": "~6.0.3" + "oxfmt": "^0.66.0", + "oxlint": "^1.81.0", + "typescript": "~7.0.2" } } diff --git a/packages/react-native-boost/package.json b/packages/react-native-boost/package.json index a7acbda5..371d5f06 100644 --- a/packages/react-native-boost/package.json +++ b/packages/react-native-boost/package.json @@ -17,6 +17,14 @@ "default": "./dist/plugin/esm/index.mjs" }, "default": "./dist/plugin/index.js" + }, + "./metro": { + "types": "./dist/metro/index.d.ts", + "default": "./dist/metro/index.js" + }, + "./uniwind": { + "types": "./dist/runtime/uniwind.d.ts", + "default": "./dist/runtime/uniwind.js" } }, "typesVersions": { @@ -26,6 +34,9 @@ ], "plugin": [ "dist/plugin/index.d.ts" + ], + "metro": [ + "dist/metro/index.d.ts" ] } }, @@ -43,7 +54,8 @@ "build": "pnpm clean && rollup -c", "build:watch": "rollup -c -w", "dev": "pnpm build:watch", - "test": "vitest", + "pretest": "pnpm build", + "test": "vitest run", "typecheck": "tsc --noEmit", "release": "release-it", "prepack": "cp ../../README.md ./README.md", @@ -67,7 +79,7 @@ "url": "https://github.com/kuatsu/react-native-boost/issues" }, "homepage": "https://github.com/kuatsu/react-native-boost#readme", - "packageManager": "pnpm@10.28.2", + "packageManager": "pnpm@12.3.1", "publishConfig": { "registry": "https://registry.npmjs.org" }, @@ -81,23 +93,33 @@ "@babel/plugin-syntax-jsx": "^7.29.7", "@babel/plugin-syntax-typescript": "^7.29.7", "@babel/preset-react": "^7.29.7", - "@react-native/babel-preset": "0.86.0", + "@react-native/babel-preset": "0.87.1", "@release-it/conventional-changelog": "^12.0.0", - "@rollup/plugin-node-resolve": "^16.0.0", - "@types/babel__helper-module-imports": "^7.0.0", - "@types/babel__helper-plugin-utils": "^7.0.0", - "@types/node": "^24", + "@rollup/plugin-node-resolve": "^16.0.3", + "@types/babel__core": "^7.20.5", + "@types/babel__helper-module-imports": "^7.18.3", + "@types/babel__helper-plugin-utils": "^7.10.3", + "@types/node": "^26.4.1", + "@types/react": "^19.2.18", + "@types/react-dom": "^19.2.5", + "@types/react-test-renderer": "^19.1.0", + "@typescript/typescript6": "^6.0.2", + "babel-plugin-react-compiler": "1.0.0", "babel-plugin-tester": "^12.0.0", "fast-check": "^4.9.0", - "react": "19.2.3", - "react-dom": "19.2.3", - "react-native": "0.86.0", + "metro": "0.87.0", + "metro-config": "0.87.0", + "react": "19.2.8", + "react-dom": "19.2.8", + "react-native": "0.87.1", + "react-test-renderer": "19.2.8", "release-it": "^21.0.2", "rollup": "^4.63.1", "rollup-plugin-dts": "^6.5.1", - "rollup-plugin-esbuild": "^6.2.0", - "typescript": "~6.0.3", - "vitest": "^4.1.11" + "rollup-plugin-esbuild": "^6.2.1", + "typescript": "~7.0.2", + "uniwind": "1.12.0", + "vitest": "^5.0.0" }, "peerDependencies": { "react": "*", @@ -121,7 +143,21 @@ "plugins": { "@release-it/conventional-changelog": { "preset": { - "name": "angular" + "name": "conventionalcommits", + "types": [ + { + "type": "feat", + "section": "✨ Features" + }, + { + "type": "perf", + "section": "⚑ Performance Improvements" + }, + { + "type": "fix", + "section": "πŸ› Bug Fixes" + } + ] }, "infile": "CHANGELOG.md" } diff --git a/packages/react-native-boost/rollup.config.mjs b/packages/react-native-boost/rollup.config.mjs index 0f937a83..2dc058a0 100644 --- a/packages/react-native-boost/rollup.config.mjs +++ b/packages/react-native-boost/rollup.config.mjs @@ -12,8 +12,21 @@ const commonPlugins = [ tsconfig: 'tsconfig.json', }), ]; +const nodePlugins = [resolve({ extensions }), esbuild({ target: 'esnext', tsconfig: 'tsconfig.json' })]; export default [ + { + input: 'src/runtime/uniwind.ts', + external, + plugins: commonPlugins, + output: { file: 'dist/runtime/uniwind.js', format: 'cjs', sourcemap: true }, + }, + { + input: 'src/runtime/uniwind.ts', + external, + plugins: [dts()], + output: { file: 'dist/runtime/uniwind.d.ts', format: 'esm' }, + }, { input: 'src/runtime/index.ts', external, @@ -41,6 +54,24 @@ export default [ { file: 'dist/plugin/esm/index.mjs', format: 'esm', sourcemap: true }, ], }, + { + input: 'src/metro/index.ts', + external, + plugins: nodePlugins, + output: { file: 'dist/metro/index.js', format: 'cjs', sourcemap: true }, + }, + { + input: 'src/metro/transformer.ts', + external, + plugins: nodePlugins, + output: { file: 'dist/metro/transformer.js', format: 'cjs', sourcemap: true }, + }, + { + input: 'src/metro/worker.ts', + external, + plugins: nodePlugins, + output: { file: 'dist/metro/worker.js', format: 'cjs', sourcemap: true }, + }, { input: 'src/runtime/index.ts', plugins: [dts()], @@ -59,4 +90,10 @@ export default [ external, output: { file: 'dist/plugin/index.d.ts', format: 'esm' }, }, + { + input: 'src/metro/index.ts', + plugins: [dts()], + external, + output: { file: 'dist/metro/index.d.ts', format: 'esm' }, + }, ]; diff --git a/packages/react-native-boost/src/ancestor-types.ts b/packages/react-native-boost/src/ancestor-types.ts new file mode 100644 index 00000000..c1302ab0 --- /dev/null +++ b/packages/react-native-boost/src/ancestor-types.ts @@ -0,0 +1,29 @@ +/** `context` means child handling is known, but Text context varies between branches. */ +export type AncestorClassification = 'safe' | 'text' | 'context' | 'unknown'; +export type ComponentAncestorClassification = AncestorClassification | 'transparent'; + +export type AncestorSummary = + | ComponentAncestorClassification + | { kind: 'import'; source: string; imported: string } + | { kind: 'ancestors' | 'branches'; values: AncestorSummary[] }; + +export type SpreadKeys = string[] | null; +export type SpreadSummary = SpreadKeys | { kind: 'import'; source: string; imported: string }; + +export interface ModuleAncestorAnalysis { + exports: Record; + exportAll: string[]; + spreadExports?: Record; + spreadReferences?: Array<{ source: string; imported: string; keys: SpreadKeys }>; + /** Import paths after Babel's module resolver, keyed by their pre-compiler spelling. */ + sources?: Record; + /** The values used by Babel let Metro skip transforms whose ancestor inputs did not change. */ + references: Array<{ source: string; imported: string; classification: ComponentAncestorClassification }>; +} + +export interface AncestorSnapshot { + version: 1; + revision: number; + platforms: Record>>>; + spreadPlatforms?: Record>>>; +} diff --git a/packages/react-native-boost/src/metro/__tests__/graph.test.ts b/packages/react-native-boost/src/metro/__tests__/graph.test.ts new file mode 100644 index 00000000..6ea78474 --- /dev/null +++ b/packages/react-native-boost/src/metro/__tests__/graph.test.ts @@ -0,0 +1,468 @@ +import fs from 'node:fs'; +import { createRequire } from 'node:module'; +import os from 'node:os'; +import path from 'node:path'; +import { transformSync, types as t, type TransformCaller, type PluginItem, type PluginObj } from '@babel/core'; +import compiler from 'babel-plugin-react-compiler'; +import { afterEach, describe, expect, it } from 'vitest'; +import { installMetroGraphPatch } from '../graph'; +import boostPlugin from '../../plugin'; +import type { PluginOptions } from '../../plugin/types'; + +const requireFromTest = createRequire(import.meta.url); +const temporaryDirectories: string[] = []; + +afterEach(() => { + for (const directory of temporaryDirectories.splice(0)) fs.rmSync(directory, { recursive: true, force: true }); +}); + +describe('private Metro graph integration', () => { + it.each([false, true])('rechecks literal spread ancestry after graph edits (compiler: %s)', async (compile) => { + const safe = `export {View as Box} from 'react-native';`; + const sources = { + 'Screen.js': `import {Text} from 'react-native'; import {Box} from './Box'; export function Screen() { return Hello; }`, + 'Box.js': safe, + }; + const project = createGraph(sources, 'Screen.js', {}, compile ? [compiler] : []); + await project.graph.initialTraverseDependencies(project.options); + expect(project.code()).toContain('NativeText'); + sources['Box.js'] = `export {Text as Box} from 'react-native';`; + await project.graph.traverseDependencies([project.filename('Box.js')], project.options); + expect(project.code()).not.toContain('NativeText'); + expect(project.code()).toMatch(/ { + const safe = `export function props(enabled, index) { if (!enabled) return {}; const result = {role: 'cell'}; if (index !== undefined) result['aria-colindex'] = index; return result; }`; + const sources = { + 'Screen.js': `import {View} from 'react-native'; import {props} from './Barrel'; export function Screen({enabled, index}) { return ; }`, + 'Barrel.js': `export * from './Props';`, + 'Props.js': safe, + }; + const project = createGraph(sources, 'Screen.js', {}, compile ? [compiler] : []); + await project.graph.initialTraverseDependencies(project.options); + expect(project.code()).toContain('<_NativeView'); + expect(project.code()).not.toContain('NativeViewWithContext'); + + for (const body of [ + `return {'aria-label': 'changed'};`, + `const result = {}; escape(result); return result;`, + `return unknown();`, + ]) { + sources['Props.js'] = `export function props() { ${body} }`; + await project.graph.traverseDependencies([project.filename('Props.js')], project.options); + expect(project.code()).not.toContain('react-native-boost/runtime'); + sources['Props.js'] = safe; + await project.graph.traverseDependencies([project.filename('Props.js')], project.options); + expect(project.code()).toContain('<_NativeView'); + } + }); + + it('preserves compiler summaries and resolved import paths across incremental edits', async () => { + const resolveAliases: PluginObj = { + visitor: { + Program(path) { + path.traverse({ + StringLiteral(child) { + if (child.node.value.startsWith('@app/')) child.node.value = child.node.value.replace('@app/', './'); + }, + }); + }, + }, + }; + const sources = { + 'Screen.js': `import {View, Text} from 'react-native'; import {Box} from '@app/Barrel'; export function Screen() { return hello; }`, + 'Barrel.js': `export {Box} from '@app/Box';`, + 'Box.js': `import {View} from 'react-native'; export function Box({children}) { return {children}; }`, + }; + const project = createGraph(sources, 'Screen.js', {}, [compiler, resolveAliases]); + await project.graph.initialTraverseDependencies(project.options); + expect(project.code()).toContain('react/compiler-runtime'); + expect(project.code()).toContain('<_NativeView testID="target">'); + + sources['Box.js'] = + `import {Text} from 'react-native'; export function Box({children}) { return {children}; }`; + await project.graph.traverseDependencies([project.filename('Box.js')], project.options); + expect(project.code()).toContain('<_NativeViewWithContext testID="target">'); + + sources['Box.js'] = + `import React from 'react'; export function Box({children}) { return React.cloneElement(children, {testID: children.type.name}); }`; + await project.graph.traverseDependencies([project.filename('Box.js')], project.options); + expect(project.code()).toContain(''); + expect(project.code()).not.toContain('NativeViewWithContext'); + }); + + it.each([ + [`export { Pressable as Card } from 'react-native';`, `export { Text as Card } from 'react-native';`], + [ + `import Animated from 'react-native-reanimated'; export const Card = Animated.View;`, + `import Animated from 'react-native-reanimated'; export const Card = Animated.Text;`, + ], + ])('invalidates an intrinsic ancestor summary: %s', async (safe, text) => { + const sources = { + 'Screen.js': `import { Text } from 'react-native'; import { Card } from './Card'; export default () => Hello;`, + 'Card.js': safe, + }; + const project = createGraph(sources); + await project.graph.initialTraverseDependencies(project.options); + expect(project.code()).toContain('<_NativeText'); + + sources['Card.js'] = text; + const delta = await project.graph.traverseDependencies([project.filename('Card.js')], project.options); + expect(project.code()).toContain('Hello'); + expect(delta.modified.has(project.filename('Screen.js'))).toBe(true); + }); + + it('keeps unsafe branches distinct from variable Text context across files', async () => { + const sources = { + 'Screen.js': `import { Text, View } from 'react-native'; import { Wrapper } from './Wrapper'; export default () => Helloinline;`, + 'Wrapper.js': `import { Text } from 'react-native'; import { Inner } from './Inner'; export function Wrapper({children, inline}) { if (inline) return {children}; return {children}; }`, + 'Inner.js': `export const Inner = ({children}) => children;`, + }; + const project = createGraph(sources); + await project.graph.initialTraverseDependencies(project.options); + expect(project.code()).toContain('<_NativeViewWithContext testID="target">'); + expect(project.code()).toContain('inline'); + + sources['Inner.js'] = + `import React from 'react'; export const Inner = ({children}) => React.Children.map(children, child => React.cloneElement(child, {'aria-label': 'Name'}));`; + await project.graph.traverseDependencies([project.filename('Inner.js')], project.options); + expect(project.code()).toContain(''); + expect(project.code()).not.toContain('NativeViewWithContext'); + + sources['Inner.js'] = + `import { Text } from 'react-native'; export const Inner = ({children}) => {children};`; + await project.graph.traverseDependencies([project.filename('Inner.js')], project.options); + expect(project.code()).toContain('<_NativeViewWithContext testID="target">'); + }); + + it('continues resolving ancestors after a queued transform fails', async () => { + const project = createGraph({ + 'Screen.js': `import { Text } from 'react-native'; import { Card } from './Card'; export default () => Hello;`, + 'Card.js': `import { View } from 'react-native'; export const Card = ({ children }) => {children};`, + }); + const transform = project.options.transform; + let consumerTransforms = 0; + project.options.transform = async (filename) => { + if (filename === project.filename('Screen.js') && ++consumerTransforms === 2) { + throw new Error('Queued transform failed'); + } + return transform(filename); + }; + + await expect(project.graph.initialTraverseDependencies(project.options)).rejects.toThrow('Queued transform failed'); + await project.graph.traverseDependencies([project.filename('Screen.js')], project.options); + + expect(project.code()).toContain('<_NativeText'); + }); + + it.each([ + ['View', '{children}', false], + ['Text', '{children}', true], + ['View', 'children', true], + ['Text', 'children', false], + ['View', '{children}', false], + ])('preserves an imported %s ancestor around %s', async (host, inner, optimized) => { + const project = createGraph({ + 'Screen.js': `import { Text } from 'react-native'; import { Wrapper } from './Wrapper'; export default () => Hello;`, + 'Wrapper.js': `import { ${host} } from 'react-native'; import { Inner } from './Inner'; export const Wrapper = ({ children }) => <${host}>{children};`, + 'Inner.js': `import { Text, View } from 'react-native'; export const Inner = ({ children }) => ${inner};`, + }); + + await project.graph.initialTraverseDependencies(project.options); + + expect(project.code()).toContain(optimized ? '<_NativeText' : 'Hello'); + }); + + it('resolves newly discovered ancestors in initial and incremental graphs', async () => { + const sources = { + 'Screen.js': `import { Text } from 'react-native'; import { Shell } from './Shell'; import { Middle } from './Middle'; import { Pass } from './Pass'; export default () => Hello;`, + 'Shell.js': `import { View } from 'react-native'; export const Shell = ({ children }) => {children};`, + 'Middle.js': `export const Middle = ({ children }) => children;`, + 'Pass.js': `export const Pass = ({ children }) => children;`, + }; + const project = createGraph(sources); + await project.graph.initialTraverseDependencies(project.options); + + expect(project.code()).toContain('<_NativeText'); + expect(project.transforms.get('Screen.js')).toBe(4); + expect(project.transforms.get('Shell.js')).toBe(1); + + sources['Pass.js'] = + `import { Text } from 'react-native'; export const Pass = ({ children }) => {children};`; + await project.graph.traverseDependencies([project.filename('Pass.js')], project.options); + expect(project.code()).toContain('Hello'); + + sources['Pass.js'] = `export const Pass = ({ children }) => children;`; + const delta = await project.graph.traverseDependencies([project.filename('Pass.js')], project.options); + expect(project.code()).toContain('<_NativeText'); + expect(delta.modified.has(project.filename('Screen.js'))).toBe(true); + + const count = project.transforms.get('Screen.js'); + await project.graph.traverseDependencies([project.filename('Shell.js')], project.options); + expect(project.transforms.get('Screen.js')).toBe(count); + }); + + it('does not retransform unknown ancestors, but still tracks them for edits', async () => { + const sources = { + 'Screen.js': `import { Text } from 'react-native'; import { Card } from './Card'; export default () => Hello;`, + 'Card.js': `export const Card = unknownFactory();`, + }; + const project = createGraph(sources); + await project.graph.initialTraverseDependencies(project.options); + expect(project.transforms.get('Screen.js')).toBe(1); + expect(project.code()).toContain('Hello'); + + sources['Card.js'] = + `import { View } from 'react-native'; export const Card = ({ children }) => {children};`; + await project.graph.traverseDependencies([project.filename('Card.js')], project.options); + expect(project.transforms.get('Screen.js')).toBe(2); + expect(project.code()).toContain('<_NativeText'); + + sources['Card.js'] = `export const Card = unknownFactory();`; + await project.graph.traverseDependencies([project.filename('Card.js')], project.options); + expect(project.transforms.get('Screen.js')).toBe(3); + expect(project.code()).toContain('Hello'); + }); + + it('does not resolve ancestors for elements that already fail a prop safety check', async () => { + const sources = { + 'Screen.js': `import { View } from 'react-native'; import { Card } from './Card'; export default (props) => ;`, + 'Card.js': `import { View } from 'react-native'; export const Card = ({ children }) => {children};`, + }; + const project = createGraph(sources); + await project.graph.initialTraverseDependencies(project.options); + expect(project.transforms.get('Screen.js')).toBe(1); + expect(project.code()).toContain(' { + const project = createGraph( + { + 'Screen.js': `import { Text } from 'react-native'; import { Card } from './Card'; export default () => Hello;`, + 'Card.js': `import { View } from 'react-native'; export const Card = ({ children }) => {children};`, + }, + 'Screen.js', + { ignores: ['**/Card.js'] } + ); + await project.graph.initialTraverseDependencies(project.options); + expect(project.code()).toContain('<_NativeText'); + expect(project.graph.dependencies.get(project.filename('Card.js'))!.output[0].data.code).toContain(''); + }); + + it('does not repeat a consumer edit that already used the resolved snapshot', async () => { + const sources = { + 'Screen.js': `import { Text } from 'react-native'; import { Card } from './Card'; export default () => Hello;`, + 'Card.js': `import { View } from 'react-native'; export const Card = ({ children }) => {children};`, + }; + const project = createGraph(sources); + await project.graph.initialTraverseDependencies(project.options); + expect(project.transforms.get('Screen.js')).toBe(2); + + sources['Screen.js'] = sources['Screen.js'].replace('Hello', 'Edited'); + await project.graph.traverseDependencies([project.filename('Screen.js')], project.options); + expect(project.transforms.get('Screen.js')).toBe(3); + expect(project.code()).toContain('<_NativeText'); + expect(project.code()).toContain('Edited'); + + sources['Screen.js'] = sources['Screen.js'].replace('Edited', 'Together'); + sources['Card.js'] = + `import { Text } from 'react-native'; export const Card = ({ children }) => {children};`; + await project.graph.traverseDependencies( + [project.filename('Screen.js'), project.filename('Card.js')], + project.options + ); + expect(project.code()).toContain('Together'); + }); + + it('resolves consumers with an external entry', async () => { + const sources = { + '../entry.js': `import './app/Screen';`, + 'Screen.js': `import { Text } from 'react-native'; import { Card } from './Card'; export default () => Hello;`, + 'Card.js': `import { View } from 'react-native'; export const Card = ({ children }) => {children};`, + }; + const project = createGraph(sources, '../entry.js'); + await project.graph.initialTraverseDependencies(project.options); + expect(project.code()).toContain('<_NativeText'); + + sources['Card.js'] = + `import { Text } from 'react-native'; export const Card = ({ children }) => {children};`; + const delta = await project.graph.traverseDependencies([project.filename('Card.js')], project.options); + expect(project.code()).toContain('Hello'); + expect(delta.modified.has(project.filename('Screen.js'))).toBe(true); + }); + + it('updates an unchanged consumer in the same graph delta', async () => { + const graphPath = requireFromTest.resolve('metro/private/DeltaBundler/Graph'); + const { Graph } = requireFromTest(graphPath) as { + Graph: new (options: { entryPoints: Set; transformOptions: { platform: string } }) => MetroGraph; + }; + const projectRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'react-native-boost-graph-')); + temporaryDirectories.push(projectRoot); + const consumerPath = path.join(projectRoot, 'Screen.js'); + const componentPath = path.join(projectRoot, 'Card.js'); + const snapshotPath = path.join(projectRoot, 'snapshot.json'); + const injectionId = 'metro-test'; + let componentSummary: 'safe' | 'text' = 'safe'; + const transforms = new Map(); + + installMetroGraphPatch(graphPath, { injectionId, snapshotPath }); + const graph = new Graph({ entryPoints: new Set([consumerPath]), transformOptions: { platform: 'ios' } }); + const options = { + lazy: false, + onProgress: null, + shallow: false, + resolve: () => ({ filePath: componentPath }), + transform: async (filename: string) => { + transforms.set(filename, (transforms.get(filename) ?? 0) + 1); + const snapshot = fs.existsSync(snapshotPath) ? JSON.parse(fs.readFileSync(snapshotPath, 'utf8')) : undefined; + const resolved = snapshot?.platforms?.ios?.[consumerPath]?.['./Card']?.Card ?? 'unknown'; + return { + dependencies: + filename === consumerPath + ? [ + { + name: './Card', + data: { + key: './Card', + asyncType: null, + isESMImport: true, + isOptional: false, + locs: [], + }, + }, + ] + : [], + output: [ + { + type: 'js/module', + data: { + code: filename === consumerPath ? resolved : componentSummary, + reactNativeBoost: { + injectionId, + analysis: + filename === consumerPath + ? { + exports: {}, + exportAll: [], + references: [{ source: './Card', imported: 'Card', classification: resolved }], + } + : { exports: { Card: componentSummary }, exportAll: [], references: [] }, + }, + }, + }, + ], + getSource: () => Buffer.alloc(0), + unstable_transformResultKey: filename === componentPath ? `${filename}:${componentSummary}` : filename, + }; + }, + }; + + await graph.initialTraverseDependencies(options); + + expect(graph.dependencies.get(consumerPath)!.output[0].data.code).toBe('safe'); + expect(transforms.get(consumerPath)).toBe(2); + expect(transforms.get(componentPath)).toBe(1); + + componentSummary = 'text'; + const delta = await graph.traverseDependencies([componentPath], options); + + expect(graph.dependencies.get(consumerPath)!.output[0].data.code).toBe('text'); + expect(delta.modified.has(consumerPath)).toBe(true); + expect(delta.modified.has(componentPath)).toBe(true); + expect(transforms.get(consumerPath)).toBe(3); + expect(transforms.get(componentPath)).toBe(2); + }); +}); + +function createGraph( + sources: Record, + entry = 'Screen.js', + pluginOptions: PluginOptions = {}, + babelPlugins: PluginItem[] = [] +) { + const graphPath = requireFromTest.resolve('metro/private/DeltaBundler/Graph'); + const { Graph } = requireFromTest(graphPath) as { + Graph: new (options: { entryPoints: Set; transformOptions: { platform: string } }) => MetroGraph; + }; + const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'react-native-boost-graph-')); + temporaryDirectories.push(directory); + const projectRoot = path.join(directory, 'app'); + const snapshotPath = path.join(directory, 'snapshot.json'); + const injectionId = directory; + const filename = (name: string) => path.resolve(projectRoot, name); + const transforms = new Map(); + installMetroGraphPatch(graphPath, { injectionId, snapshotPath }); + const graph = new Graph({ entryPoints: new Set([filename(entry)]), transformOptions: { platform: 'ios' } }); + const options = { + lazy: false, + onProgress: null, + shallow: false, + resolve: (origin: string, dependency: { name: string }) => ({ + filePath: path.resolve(path.dirname(origin), `${dependency.name}.js`), + }), + transform: async (absoluteFilename: string) => { + const name = path.relative(projectRoot, absoluteFilename); + transforms.set(name, (transforms.get(name) ?? 0) + 1); + const source = sources[name]!; + const result = transformSync(source, { + cwd: projectRoot, + filename: absoluteFilename, + babelrc: false, + configFile: false, + ast: true, + caller: { name: 'metro', platform: 'ios' } as TransformCaller, + plugins: [ + '@babel/plugin-syntax-jsx', + ...babelPlugins, + [ + boostPlugin, + { + ...pluginOptions, + logLevel: 'silent', + target: { reactNative: { packageJson: requireFromTest.resolve('react-native/package.json') } }, + __reactNativeBoost: injectionId, + __reactNativeBoostProjectRoot: projectRoot, + __reactNativeBoostSnapshot: snapshotPath, + }, + ], + ], + })!; + const imports = result.ast!.program.body.filter( + (statement): statement is t.ImportDeclaration | t.ExportNamedDeclaration | t.ExportAllDeclaration => + (t.isImportDeclaration(statement) || + t.isExportNamedDeclaration(statement) || + t.isExportAllDeclaration(statement)) && + statement.source?.value.startsWith('.') === true + ); + return { + dependencies: imports.map(({ source }) => ({ + name: source!.value, + data: { key: source!.value, asyncType: null, isESMImport: true, isOptional: false, locs: [] }, + })), + output: [{ type: 'js/module', data: { code: result.code!, ...result.metadata } }], + getSource: () => Buffer.from(source), + unstable_transformResultKey: source, + }; + }, + }; + return { + graph, + options, + transforms, + filename, + code: () => graph.dependencies.get(filename('Screen.js'))!.output[0].data.code, + }; +} + +type MetroGraph = { + dependencies: Map }>; + initialTraverseDependencies: (options: unknown) => Promise; + traverseDependencies: (paths: string[], options: unknown) => Promise<{ modified: Map }>; +}; diff --git a/packages/react-native-boost/src/metro/__tests__/index.test.ts b/packages/react-native-boost/src/metro/__tests__/index.test.ts new file mode 100644 index 00000000..601dbca6 --- /dev/null +++ b/packages/react-native-boost/src/metro/__tests__/index.test.ts @@ -0,0 +1,154 @@ +import fs from 'node:fs'; +import { createRequire } from 'node:module'; +import os from 'node:os'; +import path from 'node:path'; +import type { ConfigT } from 'metro-config'; +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { withBoostConfig } from '..'; + +const requireFromProject = createRequire(import.meta.url); +const metroRequire = createRequire(requireFromProject.resolve('metro')); +const metroWorkerPath = metroRequire.resolve('metro-transform-worker'); +const temporaryDirectories: string[] = []; +const acceptsMetroConfig = (config: ConfigT) => withBoostConfig(config); +void acceptsMetroConfig; + +afterEach(() => { + vi.restoreAllMocks(); + for (const directory of temporaryDirectories.splice(0)) fs.rmSync(directory, { recursive: true, force: true }); +}); + +describe('Metro integration', () => { + it('wraps the configured Babel transformer with the target and Boost options', () => { + const config = withBoostConfig( + { + projectRoot: process.cwd(), + transformerPath: metroWorkerPath, + transformer: { babelTransformerPath: metroRequire.resolve('metro-babel-transformer') }, + }, + { logLevel: 'silent', optimizations: { 'native-text': 'off' } } + ); + const wrapper = fs.readFileSync(config.transformer.babelTransformerPath, 'utf8'); + const workerPath = (config as typeof config & { transformerPath: string }).transformerPath; + const worker = fs.readFileSync(workerPath, 'utf8'); + const reactNativePackageJson = fs.realpathSync(requireFromProject.resolve('react-native/package.json')); + + expect(wrapper).toContain('createTransformer'); + expect(worker).toContain('createWorker'); + expect(wrapper).toContain(JSON.stringify(metroRequire.resolve('metro-babel-transformer'))); + expect(wrapper).not.toContain('module.parent'); + expect(wrapper).toContain(`"packageJson":${JSON.stringify(reactNativePackageJson)}`); + expect(wrapper).toContain('"native-text":"off"'); + }); + + it('preserves the Metro worker and cache when cross-file resolution is disabled', () => { + const cacheStore = { get: () => null, set: () => {} }; + const transformerPath = '/custom/metro-worker.js'; + const config = withBoostConfig( + { + projectRoot: process.cwd(), + cacheStores: [cacheStore], + transformerPath, + transformer: { babelTransformerPath: requireFromProject.resolve('metro-babel-transformer') }, + }, + { crossFileAncestorResolution: false, logLevel: 'silent' } + ); + + expect(config.transformerPath).toBe(transformerPath); + expect(config.cacheStores).toEqual([cacheStore]); + }); + + it('resolves a named Babel transformer from the project', () => { + const projectRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'react-native-boost-metro-')); + temporaryDirectories.push(projectRoot); + fs.writeFileSync(path.join(projectRoot, 'package.json'), '{}'); + const transformerDirectory = path.join(projectRoot, 'node_modules', 'custom-transformer'); + fs.mkdirSync(transformerDirectory, { recursive: true }); + fs.writeFileSync(path.join(transformerDirectory, 'package.json'), JSON.stringify({ main: 'index.js' })); + const transformer = path.join(transformerDirectory, 'index.js'); + fs.writeFileSync(transformer, 'exports.transform = () => ({ ast: {} });'); + + const config = withBoostConfig( + { projectRoot, transformer: { babelTransformerPath: 'custom-transformer' } }, + { logLevel: 'silent' } + ); + const wrapper = fs.readFileSync(config.transformer.babelTransformerPath, 'utf8'); + + expect(wrapper).toContain(JSON.stringify(fs.realpathSync(transformer))); + }); + + it('fails early when the Babel transformer cannot be resolved', () => { + expect(() => + withBoostConfig( + { + projectRoot: process.cwd(), + transformer: { babelTransformerPath: 'missing-react-native-boost-transformer' }, + }, + { logLevel: 'silent' } + ) + ).toThrow('Set transformer.babelTransformerPath with require.resolve(...)'); + }); + + it('rejects duplicate Metro configuration', () => { + const config = withBoostConfig({ + projectRoot: process.cwd(), + transformer: { babelTransformerPath: requireFromProject.resolve('metro-babel-transformer') }, + }); + + expect(() => withBoostConfig(config)).toThrow('applied more than once'); + }); + + it('uses an explicit React Native package and changes the cache entry with its version', () => { + const firstPackage = createPackage('0.86.0'); + const secondPackage = createPackage('0.87.1'); + const baseConfig = { + projectRoot: process.cwd(), + transformer: { babelTransformerPath: requireFromProject.resolve('metro-babel-transformer') }, + }; + + const first = withBoostConfig(baseConfig, { + logLevel: 'silent', + target: { reactNative: { packageJson: firstPackage } }, + }); + const second = withBoostConfig(baseConfig, { + logLevel: 'silent', + target: { reactNative: { packageJson: secondPackage } }, + }); + + expect(first.transformer.babelTransformerPath).not.toBe(second.transformer.babelTransformerPath); + expect(fs.readFileSync(first.transformer.babelTransformerPath, 'utf8')).toContain( + `"packageJson":${JSON.stringify(fs.realpathSync(firstPackage))}` + ); + expect(fs.readFileSync(second.transformer.babelTransformerPath, 'utf8')).toContain( + `"packageJson":${JSON.stringify(fs.realpathSync(secondPackage))}` + ); + }); + + it('warns when React Native cannot be resolved', () => { + const projectRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'react-native-boost-metro-')); + temporaryDirectories.push(projectRoot); + fs.writeFileSync(path.join(projectRoot, 'package.json'), '{}'); + fs.mkdirSync(path.join(projectRoot, 'node_modules/react-native'), { recursive: true }); + fs.writeFileSync(path.join(projectRoot, 'node_modules/react-native/package.json'), '{}'); + const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); + + withBoostConfig( + { + projectRoot, + transformer: { babelTransformerPath: requireFromProject.resolve('metro-babel-transformer') }, + }, + { crossFileAncestorResolution: false } + ); + + expect(warn).toHaveBeenCalledOnce(); + expect(warn).toHaveBeenCalledWith(expect.stringContaining('React Native could not be detected')); + }); +}); + +function createPackage(version: string): string { + const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'react-native-boost-package-')); + temporaryDirectories.push(directory); + const packageJson = path.join(directory, 'package.json'); + fs.writeFileSync(packageJson, JSON.stringify({ name: 'react-native', version })); + return packageJson; +} diff --git a/packages/react-native-boost/src/metro/__tests__/stack.test.ts b/packages/react-native-boost/src/metro/__tests__/stack.test.ts new file mode 100644 index 00000000..0801f758 --- /dev/null +++ b/packages/react-native-boost/src/metro/__tests__/stack.test.ts @@ -0,0 +1,334 @@ +import fs from 'node:fs'; +import { createRequire } from 'node:module'; +import os from 'node:os'; +import path from 'node:path'; +import { afterAll, describe, expect, it } from 'vitest'; + +const requireFromTest = createRequire(import.meta.url); +const repositoryRoot = path.resolve(import.meta.dirname, '../../../../..'); +const packageRoot = path.join(repositoryRoot, 'packages/react-native-boost'); +const { withBoostConfig } = requireFromTest(path.join(packageRoot, 'dist/metro/index.js')) as { + withBoostConfig: (config: MetroConfig, options?: Record) => MetroConfig; +}; +const temporaryDirectories: string[] = []; + +afterAll(() => { + for (const directory of temporaryDirectories) fs.rmSync(directory, { recursive: true, force: true }); +}); + +describe('Metro and Babel stack', () => { + it('resolves an ancestor chain from an entry in a watch folder outside the project', async () => { + const project = createProject(); + const external = createProject(); + const entry = path.join(external.root, 'entry.js'); + fs.writeFileSync(entry, `import ${JSON.stringify(path.join(project.root, 'Screen'))};`); + fs.writeFileSync( + path.join(project.root, 'Screen.js'), + `import { Text } from 'react-native'; import { Shell } from './Shell'; import { Pass } from './Pass'; export default () => Hello;` + ); + fs.writeFileSync( + path.join(project.root, 'Shell.js'), + `import { View } from 'react-native'; export const Shell = ({ children }) => {children};` + ); + fs.writeFileSync(path.join(project.root, 'Pass.js'), `export const Pass = ({ children }) => children;`); + const { config, metro } = await createMetroConfig(project, 1, true); + config.watchFolders = [project.root, external.root]; + + const graph = await metro.buildGraph(config, { entries: [entry], platform: 'ios', dev: true, minify: false }); + + expect(getCode(graph)).toContain('NativeText'); + }); + + it.each([1, 4])('resolves a re-exported wrapper with %i worker(s)', async (maxWorkers) => { + const project = createProject(); + fs.writeFileSync( + path.join(project.root, 'Screen.js'), + `import { Text } from 'react-native'; import { Shell } from './components'; export default () => Hello;` + ); + fs.writeFileSync(path.join(project.root, 'components.js'), `export * from './component-export';`); + fs.writeFileSync(path.join(project.root, 'component-export.js'), `export { Card as Shell } from './Card';`); + fs.writeFileSync( + path.join(project.root, 'Card.js'), + `import { View } from 'react-native'; export function Card({ children }) { return {children}; }` + ); + + const graph = await buildGraph(project, maxWorkers); + + expect(getCode(graph)).toContain('react-native-boost/runtime'); + expect(getCode(graph)).toContain('NativeText'); + }); + + it('resolves a default memo wrapper', async () => { + const project = createProject(); + fs.writeFileSync( + path.join(project.root, 'Screen.js'), + `import { Text } from 'react-native'; import Card from './Card'; export default () => Hello;` + ); + fs.writeFileSync( + path.join(project.root, 'Card.js'), + `import { memo } from 'react'; import { View } from 'react-native'; const Card = memo(({ children }) => {children}); export default Card;` + ); + + expect(getCode(await buildGraph(project, 1))).toContain('NativeText'); + }); + + it('keeps a reassigned wrapper unknown', async () => { + const project = createProject(); + fs.writeFileSync( + path.join(project.root, 'Screen.js'), + `import { Text } from 'react-native'; import { Card } from './Card'; export default () => Hello;` + ); + fs.writeFileSync( + path.join(project.root, 'Card.js'), + `import { Text, View } from 'react-native'; export let Card = ({ children }) => {children}; Card = ({ children }) => {children};` + ); + + expect(getCode(await buildGraph(project, 1))).not.toContain('react-native-boost/runtime'); + }); + + it('uses the active platform file and honors the opt-out', async () => { + const project = createProject(); + fs.writeFileSync( + path.join(project.root, 'Screen.js'), + `import { Text } from 'react-native'; import { Card } from './Card'; export default () => Hello;` + ); + fs.writeFileSync( + path.join(project.root, 'Card.ios.js'), + `import { View } from 'react-native'; export const Card = ({ children }) => {children};` + ); + fs.writeFileSync( + path.join(project.root, 'Card.android.js'), + `import { Text } from 'react-native'; export const Card = ({ children }) => {children};` + ); + + const ios = await buildGraph(project, 1, 'ios'); + const android = await buildGraph(project, 1, 'android'); + const disabled = await buildGraph(project, 1, 'ios', false); + + expect(getCode(ios)).toContain('NativeText'); + expect(getCode(android)).not.toContain('react-native-boost/runtime'); + expect(getCode(disabled)).not.toContain('react-native-boost/runtime'); + }); + + it('does not reuse a cross-file consumer from Metro cache', async () => { + const project = createProject(); + fs.writeFileSync( + path.join(project.root, 'Screen.js'), + `import { Text } from 'react-native'; import { Card } from './Card'; export default () => Hello;` + ); + const cardPath = path.join(project.root, 'Card.js'); + fs.writeFileSync( + cardPath, + `import { View } from 'react-native'; export const Card = ({ children }) => {children};` + ); + const values = new Map(); + const store: CacheStore = { + get: (key) => values.get(key.toString('hex')) ?? null, + set: (key, value) => values.set(key.toString('hex'), value), + }; + + expect(getCode(await buildGraph(project, 1, 'ios', true, undefined, [store]))).toContain('NativeText'); + expect(values.size).toBeGreaterThan(0); + + fs.writeFileSync( + cardPath, + `import { Text } from 'react-native'; export const Card = ({ children }) => {children};` + ); + const changed = await buildGraph(project, 1, 'ios', true, undefined, [store]); + + expect(getCode(changed)).not.toContain('react-native-boost/runtime'); + }); + + it('does not cache a consumer that depends only on spread keys', async () => { + const project = createProject(); + fs.writeFileSync( + path.join(project.root, 'Screen.js'), + `import {View} from 'react-native'; import {props} from './Props'; export default () => ;` + ); + const helper = path.join(project.root, 'Props.js'); + fs.writeFileSync(helper, `export function props() { return {testID: 'cell'}; }`); + const values = new Map(); + let cachedConsumer = false; + const store: CacheStore = { + get: (key) => values.get(key.toString('hex')) ?? null, + set: (key, value) => { + const output = ( + value as { output?: Array<{ data?: { reactNativeBoost?: { analysis?: { spreadReferences?: unknown[] } } } }> } + ).output; + if (output?.some(({ data }) => data?.reactNativeBoost?.analysis?.spreadReferences?.length)) { + cachedConsumer = true; + return; + } + values.set(key.toString('hex'), value); + }, + }; + expect(getCode(await buildGraph(project, 1, 'ios', true, undefined, [store]))).toContain('NativeView'); + expect(cachedConsumer).toBe(false); + fs.writeFileSync(helper, `export function props() { return {'aria-label': 'changed'}; }`); + expect(getCode(await buildGraph(project, 1, 'ios', true, undefined, [store]))).not.toContain( + 'react-native-boost/runtime' + ); + expect(cachedConsumer).toBe(false); + }); + + it('uses Metro custom-resolver edges', async () => { + const project = createProject(); + fs.writeFileSync( + path.join(project.root, 'Screen.js'), + `import { Text } from 'react-native'; import * as UI from '@ui/Card'; export default () => Hello;` + ); + const cardPath = path.join(project.root, 'Card.js'); + fs.writeFileSync( + cardPath, + `import { View } from 'react-native'; export function Card({ children }) { return {children}; }` + ); + + const graph = await buildGraph(project, 1, 'ios', true, cardPath); + + expect(getCode(graph)).toContain('NativeText'); + }); +}); + +function createProject(): { root: string; reactNativePackageJson: string } { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'react-native-boost-stack-')); + temporaryDirectories.push(root); + const babelRuntimeDirectory = path.join(root, 'node_modules/@babel/runtime/helpers'); + const reactDirectory = path.join(root, 'node_modules/react'); + const reactNativeDirectory = path.join(root, 'node_modules/react-native'); + const boostDirectory = path.join(root, 'node_modules/react-native-boost'); + fs.mkdirSync(babelRuntimeDirectory, { recursive: true }); + fs.mkdirSync(reactDirectory, { recursive: true }); + fs.mkdirSync(reactNativeDirectory, { recursive: true }); + fs.mkdirSync(boostDirectory, { recursive: true }); + fs.writeFileSync( + path.join(root, 'node_modules/@babel/runtime/package.json'), + JSON.stringify({ name: '@babel/runtime' }) + ); + fs.writeFileSync( + path.join(babelRuntimeDirectory, 'interopRequireDefault.js'), + 'module.exports = value => value && value.__esModule ? value : { default: value };' + ); + fs.writeFileSync(path.join(reactDirectory, 'package.json'), JSON.stringify({ name: 'react', main: 'index.js' })); + fs.writeFileSync(path.join(reactDirectory, 'index.js'), 'exports.createElement = () => {};'); + fs.writeFileSync(path.join(reactDirectory, 'jsx-runtime.js'), 'exports.jsx = () => {}; exports.jsxs = () => {};'); + const reactNativePackageJson = path.join(reactNativeDirectory, 'package.json'); + const reactNativeVersion = (requireFromTest('react-native/package.json') as { version: string }).version; + fs.writeFileSync( + reactNativePackageJson, + JSON.stringify({ name: 'react-native', version: reactNativeVersion, main: 'index.js' }) + ); + fs.writeFileSync(path.join(reactNativeDirectory, 'index.js'), "exports.Text = 'Text'; exports.View = 'View';"); + fs.writeFileSync( + path.join(boostDirectory, 'package.json'), + JSON.stringify({ name: 'react-native-boost', exports: { './runtime': './runtime.js' } }) + ); + fs.writeFileSync( + path.join(boostDirectory, 'runtime.js'), + "exports.NativeText = 'NativeText'; exports.NativeView = 'NativeView'; exports.NativeViewWithContext = 'NativeViewWithContext';" + ); + fs.writeFileSync( + path.join(root, 'babel.config.js'), + `module.exports = { presets: [${JSON.stringify(requireFromTest.resolve('@react-native/babel-preset'))}] };` + ); + return { root, reactNativePackageJson }; +} + +async function buildGraph( + project: { root: string; reactNativePackageJson: string }, + maxWorkers: number, + platform = 'ios', + crossFileAncestorResolution = true, + customCardPath?: string, + cacheStores?: CacheStore[] +): Promise { + const { config, metro } = await createMetroConfig( + project, + maxWorkers, + crossFileAncestorResolution, + customCardPath, + cacheStores + ); + return metro.buildGraph(config, { + entries: [path.join(project.root, 'Screen.js')], + platform, + dev: true, + minify: false, + }); +} + +async function createMetroConfig( + project: { root: string; reactNativePackageJson: string }, + maxWorkers: number, + crossFileAncestorResolution: boolean, + customCardPath?: string, + cacheStores?: CacheStore[] +): Promise<{ + config: MetroConfig; + metro: { buildGraph: (config: MetroConfig, options: Record) => Promise }; +}> { + const metroPath = requireFromTest.resolve('metro'); + const metroRequire = createRequire(metroPath); + const metro = requireFromTest('metro') as { + buildGraph: (config: MetroConfig, options: Record) => Promise; + }; + const { getDefaultConfig } = metroRequire('metro-config') as { + getDefaultConfig: (root: string) => Promise; + }; + const defaults = await getDefaultConfig(project.root); + const resolver = { + ...defaults.resolver, + ...(customCardPath + ? { + resolveRequest: (context: ResolverContext, moduleName: string, activePlatform: string | null) => + moduleName === '@ui/Card' + ? { type: 'sourceFile', filePath: customCardPath } + : context.resolveRequest(context, moduleName, activePlatform), + } + : {}), + }; + const config = withBoostConfig( + { + ...defaults, + ...(cacheStores ? { cacheStores } : {}), + maxWorkers, + projectRoot: project.root, + watchFolders: [project.root], + resolver, + transformer: { + ...defaults.transformer, + babelTransformerPath: metroRequire.resolve('metro-babel-transformer'), + }, + }, + { + crossFileAncestorResolution, + logLevel: 'silent', + target: { reactNative: { packageJson: project.reactNativePackageJson } }, + } + ); + return { config, metro }; +} + +function getCode(graph: MetroGraph): string { + const screen = [...graph.dependencies].find(([filename]) => path.basename(filename) === 'Screen.js')?.[1]; + if (!screen) throw new Error('Missing Screen.js in Metro graph'); + return screen.output.map((output) => output.data.code).join('\n'); +} + +type MetroConfig = Record & { + resolver?: Record; + transformer?: Record; +}; +type MetroGraph = { + dependencies: Map }>; +}; +type CacheStore = { + get: (key: Buffer) => unknown; + set: (key: Buffer, value: unknown) => unknown; +}; +type ResolverContext = { + resolveRequest: ( + context: ResolverContext, + moduleName: string, + platform: string | null + ) => { type: string; filePath: string }; +}; diff --git a/packages/react-native-boost/src/metro/__tests__/transformer.test.ts b/packages/react-native-boost/src/metro/__tests__/transformer.test.ts new file mode 100644 index 00000000..9097fb53 --- /dev/null +++ b/packages/react-native-boost/src/metro/__tests__/transformer.test.ts @@ -0,0 +1,82 @@ +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { afterEach, describe, expect, it } from 'vitest'; +import { createTransformer } from '../transformer'; +import { createWorker } from '../worker'; + +const temporaryDirectories: string[] = []; + +afterEach(() => { + for (const directory of temporaryDirectories.splice(0)) fs.rmSync(directory, { recursive: true, force: true }); +}); + +describe('Metro transformer', () => { + it('injects Boost options and verifies the transform result', async () => { + const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'react-native-boost-transformer-')); + temporaryDirectories.push(directory); + const delegatePath = path.join(directory, 'delegate.cjs'); + const pluginPath = path.join(directory, 'plugin.cjs'); + fs.writeFileSync( + delegatePath, + `exports.transform = args => ({ ast: {}, metadata: { reactNativeBoost: { injectionId: args.plugins[0][1].__reactNativeBoost, analysis: { exports: {}, exportAll: [], references: [{ source: './Card', imported: 'Card' }] } } } });\nexports.getCacheKey = () => 'delegate';\n` + ); + fs.writeFileSync(pluginPath, 'module.exports = () => ({ visitor: {} });\n'); + const injectionId = 'test-injection'; + const manifest = { + babelTransformerPath: delegatePath, + pluginPath, + pluginOptions: { logLevel: 'silent' as const }, + injectionId, + }; + const transformer = createTransformer(manifest); + + const result = await transformer.transform({ filename: 'case.js', src: '', options: {}, plugins: [] }); + + expect(result.metadata?.reactNativeBoost).toEqual({ + injectionId, + analysis: { + exports: {}, + exportAll: [], + references: [{ source: './Card', imported: 'Card' }], + }, + }); + expect(transformer.getCacheKey()).toBe(transformer.getCacheKey()); + expect(transformer.getCacheKey()).not.toBe( + createTransformer({ ...manifest, injectionId: 'different-injection' }).getCacheKey() + ); + }); + + it('carries Babel analysis through a full Metro worker', async () => { + const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'react-native-boost-worker-')); + temporaryDirectories.push(directory); + const babelDelegatePath = path.join(directory, 'babel.cjs'); + const pluginPath = path.join(directory, 'plugin.cjs'); + const workerDelegatePath = path.join(directory, 'worker.cjs'); + fs.writeFileSync( + babelDelegatePath, + `exports.transform = args => ({ ast: {}, metadata: { reactNativeBoost: { injectionId: args.plugins[0][1].__reactNativeBoost, analysis: { exports: { Card: 'safe' }, exportAll: [], references: [] } } } });` + ); + fs.writeFileSync(pluginPath, 'module.exports = () => ({ visitor: {} });'); + fs.writeFileSync( + workerDelegatePath, + `exports.transform = async () => ({ dependencies: [], output: [{ type: 'js/module', data: { code: '' } }] });` + ); + const injectionId = 'worker-injection'; + const transformer = createTransformer({ + babelTransformerPath: babelDelegatePath, + pluginPath, + pluginOptions: {}, + injectionId, + }); + const worker = createWorker({ transformerPath: workerDelegatePath, injectionId }); + + await transformer.transform({ filename: 'Card.js', src: '', options: {}, plugins: [] }); + const result = await worker.transform({}, directory, 'Card.js', Buffer.alloc(0), {}); + + expect(result.output[0].data.reactNativeBoost).toEqual({ + injectionId, + analysis: { exports: { Card: 'safe' }, exportAll: [], references: [] }, + }); + }); +}); diff --git a/packages/react-native-boost/src/metro/__tests__/uniwind.test.ts b/packages/react-native-boost/src/metro/__tests__/uniwind.test.ts new file mode 100644 index 00000000..a7baf4fa --- /dev/null +++ b/packages/react-native-boost/src/metro/__tests__/uniwind.test.ts @@ -0,0 +1,147 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { createRequire } from 'node:module'; +import type { ConfigT } from 'metro-config'; +import { afterAll, expect, it, vi } from 'vitest'; +import { withBoostConfig } from '..'; + +const require = createRequire(import.meta.url); +const packageRoot = path.resolve(import.meta.dirname, '../../..'); +const metroRequire = createRequire(require.resolve('metro')); +const { withUniwindConfig } = require('uniwind/metro'); +const root = fs.mkdtempSync(path.join(packageRoot, '.uniwind-test-')); +afterAll(() => fs.rmSync(root, { recursive: true, force: true })); +fs.writeFileSync(path.join(root, 'global.css'), '@import "tailwindcss";\n@import "uniwind";\n'); + +function config() { + return { + projectRoot: root, + transformer: { babelTransformerPath: metroRequire.resolve('metro-babel-transformer') }, + }; +} + +it('requires the correct Metro wrapper order when explicitly enabled', () => { + expect(() => withBoostConfig(config(), { integrations: { uniwind: 'on' } })).toThrow('after withUniwindConfig'); +}); + +it('detects active Uniwind and preserves its resolver except for Boost runtime imports', () => { + const uniwind = withUniwindConfig(config(), { + cssEntryFile: path.relative(process.cwd(), path.join(root, 'global.css')), + dtsFile: path.join(root, 'uniwind.d.ts'), + }); + const result = withBoostConfig(uniwind, { crossFileAncestorResolution: false, logLevel: 'silent' }); + const source = fs.readFileSync(result.transformer.babelTransformerPath, 'utf8'); + expect(source).toContain('"uniwind":"on"'); + const resolveRequest = vi.fn((_context, name: string) => ({ + type: 'sourceFile', + filePath: path.isAbsolute(name) ? name : require.resolve(name), + })); + const context = { originModulePath: path.join(packageRoot, 'src/runtime/uniwind.ts'), resolveRequest }; + const resolve = result.resolver.resolveRequest; + const native = resolve(context, 'react-native', 'ios'); + expect(native.filePath).toBe(path.join(path.dirname(require.resolve('react-native/package.json')), 'index.js')); + const hook = resolve(context, 'react-native-boost/uniwind/useStyle', 'ios'); + expect(hook.filePath).toBe( + path.join(path.dirname(require.resolve('uniwind/package.json')), 'src/components/native/useStyle.ts') + ); + const application = resolve({ ...context, originModulePath: path.join(root, 'Screen.tsx') }, 'react-native', 'ios'); + expect(application.filePath).toBe(require.resolve('uniwind/components')); + expect(result.transformerPath).toBe(uniwind.transformerPath); + resolveRequest.mockClear(); + resolve(context, 'react-native', 'web'); + expect(resolveRequest.mock.calls[0][1]).toBe('react-native'); + const disabled = withBoostConfig(uniwind, { + crossFileAncestorResolution: false, + logLevel: 'silent', + integrations: { uniwind: 'off' }, + }); + expect(disabled.resolver.resolveRequest).toBe(uniwind.resolver.resolveRequest); +}); + +it('allows an installed but inactive Uniwind package', () => { + const result = withBoostConfig(config(), { crossFileAncestorResolution: false, logLevel: 'silent' }); + expect(result).not.toHaveProperty('resolver.resolveRequest'); +}); + +it('builds class styles and native adapters through the real Metro and Uniwind workers', async () => { + fs.writeFileSync(path.join(root, 'package.json'), '{}'); + fs.writeFileSync( + path.join(root, 'babel.config.cjs'), + `module.exports = { presets: [[${JSON.stringify(require.resolve('@react-native/babel-preset'))}, {enableBabelRuntime: false}]] };` + ); + fs.writeFileSync( + path.join(root, 'Screen.tsx'), + `import './global.css'; import { Shell } from './Shell'; import { View, Text, Image, ActivityIndicator } from 'react-native'; export default () => hello;` + ); + fs.writeFileSync( + path.join(root, 'Shell.tsx'), + `import { View } from 'react-native'; import { ScopedTheme } from 'uniwind'; export function Shell({ children }) { return {children}; }` + ); + const { getDefaultConfig } = metroRequire('metro-config'); + const base: ConfigT = await getDefaultConfig(root); + const uniwind = withUniwindConfig( + { + ...base, + maxWorkers: 1, + resolver: { + ...base.resolver, + resolverMainFields: ['react-native', 'browser', 'main'], + unstable_conditionNames: ['require', 'react-native'], + extraNodeModules: { 'react-native-boost': packageRoot }, + }, + watchFolders: [packageRoot, path.resolve(packageRoot, '../../node_modules')], + transformer: { + ...base.transformer, + assetRegistryPath: createRequire(require.resolve('react-native/package.json')).resolve( + '@react-native/assets-registry/registry' + ), + babelTransformerPath: metroRequire.resolve('metro-babel-transformer'), + }, + }, + { + cssEntryFile: path.relative(process.cwd(), path.join(root, 'global.css')), + dtsFile: path.join(root, 'uniwind.d.ts'), + } + ); + const built = require(path.join(packageRoot, 'dist/metro/index.js')).withBoostConfig(uniwind, { + logLevel: 'silent', + integrations: { unistyles: 'off' }, + }); + const graph = await require('metro').buildGraph(built, { + entries: [path.join(root, 'Screen.tsx')], + platform: 'ios', + dev: true, + minify: false, + }); + const code = graph.dependencies.get(path.join(root, 'Screen.tsx')).output[0].data.code as string; + expect(code).toContain('react-native-boost/uniwind'); + for (const name of ['NativeView', 'NativeText', 'NativeImage', 'NativeActivityIndicator']) + expect(code).toContain(name); + const runtime = graph.dependencies.get(path.join(packageRoot, 'dist/runtime/uniwind.js')); + expect(runtime).toBeDefined(); + const dependencies = [...runtime.dependencies.values()] as Array<{ absolutePath: string; data: { name: string } }>; + expect(dependencies.find((dependency) => dependency.data.name === 'react-native')?.absolutePath).toBe( + path.join(path.dirname(require.resolve('react-native/package.json')), 'index.js') + ); + expect( + dependencies.find((dependency) => dependency.data.name === 'react-native-boost/uniwind/useStyle')?.absolutePath + ).toContain('/uniwind/src/components/native/useStyle.ts'); + const css = graph.dependencies.get(path.join(root, 'global.css')).output[0].data.code; + expect(css).toContain('text-red-500'); +}, 60_000); + +it.each([ + { name: 'uniwind', version: '1.13.0', license: 'MIT' }, + { name: '@uniwind/pro', version: '1.12.0', license: 'UNLICENSED' }, +])('rejects an unchecked Uniwind package: $name $version', (manifest) => { + const projectRoot = fs.mkdtempSync(path.join(root, 'unsupported-')); + const directory = path.join(projectRoot, 'node_modules/uniwind'); + fs.mkdirSync(directory, { recursive: true }); + fs.writeFileSync(path.join(directory, 'package.json'), JSON.stringify(manifest)); + expect(() => + withBoostConfig( + { ...config(), projectRoot, transformer: { ...config().transformer, uniwind: {} } }, + { logLevel: 'silent' } + ) + ).toThrow('free Uniwind 1.12.x'); +}); diff --git a/packages/react-native-boost/src/metro/bridge.ts b/packages/react-native-boost/src/metro/bridge.ts new file mode 100644 index 00000000..521ce9f7 --- /dev/null +++ b/packages/react-native-boost/src/metro/bridge.ts @@ -0,0 +1,25 @@ +import type { ModuleAncestorAnalysis } from '../ancestor-types'; + +const bridgeKey = Symbol.for('react-native-boost.transform-analysis'); + +type Bridge = Map; + +function getBridge(): Bridge { + const global = globalThis as typeof globalThis & { [bridgeKey]?: Bridge }; + return (global[bridgeKey] ??= new Map()); +} + +export function publishAnalysis(filename: string, metadata: { injectionId?: unknown; analysis?: unknown }): void { + if (typeof metadata.injectionId !== 'string' || !metadata.analysis) return; + getBridge().set(filename, { + injectionId: metadata.injectionId, + analysis: metadata.analysis as ModuleAncestorAnalysis, + }); +} + +export function takeAnalysis(filename: string): { injectionId: string; analysis: ModuleAncestorAnalysis } | undefined { + const bridge = getBridge(); + const analysis = bridge.get(filename); + bridge.delete(filename); + return analysis; +} diff --git a/packages/react-native-boost/src/metro/graph.ts b/packages/react-native-boost/src/metro/graph.ts new file mode 100644 index 00000000..1a2946a1 --- /dev/null +++ b/packages/react-native-boost/src/metro/graph.ts @@ -0,0 +1,347 @@ +import fs from 'node:fs'; +import { isDeepStrictEqual } from 'node:util'; +import { createRequire } from 'node:module'; +import path from 'node:path'; +import type { + AncestorSnapshot, + AncestorSummary, + ComponentAncestorClassification, + ModuleAncestorAnalysis, + SpreadKeys, +} from '../ancestor-types'; + +const patchProperty = '__reactNativeBoostGraphPatch'; +const requireFromGraph = createRequire(import.meta.url); + +type OutputMetadata = { injectionId: string; analysis: ModuleAncestorAnalysis }; +type MetroDependency = { absolutePath?: string; data: { name: string } }; +type MetroModule = { + dependencies: Map; + output: Array<{ data: { reactNativeBoost?: OutputMetadata } }>; + unstable_transformResultKey?: string; +}; +type MetroDelta = { added: Map; deleted: Set; modified: Map }; +type MetroGraph = { + dependencies: Map; + transformOptions?: { platform?: string }; +}; +type Traverse = (this: MetroGraph, paths: string[], options: unknown) => Promise; +type InitialTraverse = (this: MetroGraph, options: unknown) => Promise; +type ConsumerImports = Record>>; + +type ResolvedGraph = { + consumers: ConsumerImports; + spreadConsumers: Record>>; + staleConsumers: string[]; +}; + +type Adapter = { + injectionId: string; + queue: Promise; + revision: number; + snapshotPath: string; + platforms: AncestorSnapshot['platforms']; + spreadPlatforms: NonNullable; +}; + +type Patch = { + adapters: Adapter[]; + graphs: WeakMap; + initial: InitialTraverse; + traverse: Traverse; +}; + +type GraphConstructor = { + prototype: { initialTraverseDependencies: InitialTraverse; traverseDependencies: Traverse }; + [patchProperty]?: Patch; +}; + +export function installMetroGraphPatch( + graphPath: string, + options: { injectionId: string; snapshotPath: string } +): void { + const loaded = requireFromGraph(graphPath) as { Graph?: GraphConstructor; default?: GraphConstructor }; + const Graph = loaded.Graph ?? loaded.default; + if (!Graph?.prototype?.initialTraverseDependencies || !Graph.prototype.traverseDependencies) { + throw new Error('[react-native-boost] This Metro Graph implementation is not supported.'); + } + + let patch = Graph[patchProperty]; + if (!patch) { + patch = { + adapters: [], + graphs: new WeakMap(), + initial: Graph.prototype.initialTraverseDependencies, + traverse: Graph.prototype.traverseDependencies, + }; + Graph[patchProperty] = patch; + Graph.prototype.initialTraverseDependencies = patchedInitialTraverse; + Graph.prototype.traverseDependencies = patchedTraverse; + } + + if (patch.adapters.some((adapter) => adapter.injectionId === options.injectionId)) return; + patch.adapters.push({ ...options, queue: Promise.resolve(), revision: 0, platforms: {}, spreadPlatforms: {} }); + + async function patchedInitialTraverse(this: MetroGraph, transformOptions: unknown): Promise { + const activePatch = Graph![patchProperty]!; + const result = await activePatch.initial.call(this, transformOptions); + await updateAncestors(activePatch, this, result, transformOptions); + return { ...result, added: this.dependencies }; + } + + async function patchedTraverse( + this: MetroGraph, + changedPaths: string[], + transformOptions: unknown + ): Promise { + const activePatch = Graph![patchProperty]!; + const result = await activePatch.traverse.call(this, changedPaths, transformOptions); + return updateAncestors(activePatch, this, result, transformOptions); + } +} + +async function updateAncestors( + patch: Patch, + graph: MetroGraph, + result: MetroDelta, + transformOptions: unknown +): Promise { + const adapter = patch.graphs.get(graph) ?? findAdapter(patch.adapters, graph); + if (!adapter) return result; + patch.graphs.set(graph, adapter); + + return runExclusive(adapter, async () => { + while (true) { + const { consumers, spreadConsumers, staleConsumers } = resolveGraph(graph, adapter.injectionId); + writeSnapshot(adapter, graph.transformOptions?.platform, consumers, spreadConsumers); + if (staleConsumers.length === 0) return result; + + markTransformResultsStale(graph, staleConsumers, adapter.revision); + const next = await patch.traverse.call(graph, staleConsumers, transformOptions); + result = mergeDeltas(graph, result, next); + } + }); +} + +export function resolveGraph(graph: MetroGraph, injectionId: string): ResolvedGraph { + const consumers: ConsumerImports = {}; + const spreadConsumers: ResolvedGraph['spreadConsumers'] = {}; + const staleConsumers: string[] = []; + + for (const [consumerPath, module] of graph.dependencies) { + const analysis = getAnalysis(module, injectionId); + if (!analysis || (analysis.references.length === 0 && !analysis.spreadReferences?.length)) continue; + + const imports = Object.create(null) as ConsumerImports[string]; + let stale = false; + for (const reference of analysis.references) { + const classification = resolveImport( + graph, + consumerPath, + reference.source, + reference.imported, + injectionId, + new Set() + ); + (imports[reference.source] ??= {})[reference.imported] = classification; + if (classification !== reference.classification) stale = true; + } + consumers[consumerPath] = imports; + for (const reference of analysis.spreadReferences ?? []) { + const target = findDependency(module, reference.source, injectionId); + const keys = target ? resolveSpreadExport(graph, target, reference.imported, injectionId, new Set()).keys : null; + ((spreadConsumers[consumerPath] ??= Object.create(null))[reference.source] ??= Object.create(null))[ + reference.imported + ] = keys; + if (!isDeepStrictEqual(keys, reference.keys)) stale = true; + } + if (stale) staleConsumers.push(consumerPath); + } + + return { consumers, spreadConsumers, staleConsumers }; +} + +function resolveImport( + graph: MetroGraph, + modulePath: string, + source: string, + imported: string, + injectionId: string, + visiting: Set +): ComponentAncestorClassification { + const targetPath = findDependency(graph.dependencies.get(modulePath), source, injectionId); + return targetPath ? resolveExport(graph, targetPath, imported, injectionId, visiting).value : 'unknown'; +} + +function resolveExport( + graph: MetroGraph, + modulePath: string, + exported: string, + injectionId: string, + visiting: Set +): { found: boolean; value: ComponentAncestorClassification } { + const key = `${modulePath}\0${exported}`; + if (visiting.has(key)) return { found: true, value: 'unknown' }; + + const analysis = getAnalysis(graph.dependencies.get(modulePath), injectionId); + if (!analysis) return { found: false, value: 'unknown' }; + visiting.add(key); + const summary = analysis.exports[exported]; + if (summary) { + const value = evaluateSummary(graph, modulePath, summary, injectionId, visiting); + visiting.delete(key); + return { found: true, value }; + } + if (exported === 'default') { + visiting.delete(key); + return { found: false, value: 'unknown' }; + } + + const matches = analysis.exportAll + .map((source) => { + const targetPath = findDependency(graph.dependencies.get(modulePath), source, injectionId); + return targetPath + ? resolveExport(graph, targetPath, exported, injectionId, visiting) + : { found: false, value: 'unknown' as const }; + }) + .filter((result) => result.found); + visiting.delete(key); + return matches.length === 1 ? matches[0]! : { found: matches.length > 0, value: 'unknown' }; +} + +function evaluateSummary( + graph: MetroGraph, + modulePath: string, + summary: AncestorSummary, + injectionId: string, + visiting: Set +): ComponentAncestorClassification { + if (typeof summary === 'string') return summary; + if (summary.kind === 'import') { + return resolveImport(graph, modulePath, summary.source, summary.imported, injectionId, visiting); + } + + const values = summary.values.map((value) => evaluateSummary(graph, modulePath, value, injectionId, visiting)); + if (summary.kind === 'ancestors') return values.find((value) => value !== 'transparent') ?? 'transparent'; + if (values.includes('unknown')) return 'unknown'; + return values.every((value) => value === values[0]) ? values[0]! : 'context'; +} + +function resolveSpreadExport( + graph: MetroGraph, + modulePath: string, + exported: string, + injectionId: string, + visiting: Set +): { found: boolean; keys: SpreadKeys } { + const key = `${modulePath}\0${exported}`; + if (visiting.has(key)) return { found: true, keys: null }; + const module = graph.dependencies.get(modulePath); + const analysis = getAnalysis(module, injectionId); + if (!analysis) return { found: false, keys: null }; + visiting.add(key); + const follow = (source: string, imported: string) => { + const target = findDependency(module, source, injectionId); + return target ? resolveSpreadExport(graph, target, imported, injectionId, visiting) : { found: false, keys: null }; + }; + const summary = analysis.spreadExports?.[exported]; + let result: { found: boolean; keys: SpreadKeys }; + if (summary === undefined) { + const matches = + exported === 'default' + ? [] + : analysis.exportAll.map((source) => follow(source, exported)).filter((entry) => entry.found); + result = matches.length === 1 ? matches[0]! : { found: matches.length > 0, keys: null }; + } else { + result = { + found: true, + keys: summary === null || Array.isArray(summary) ? summary : follow(summary.source, summary.imported).keys, + }; + } + visiting.delete(key); + return result; +} + +function findDependency(module: MetroModule | undefined, source: string, injectionId: string): string | undefined { + source = getAnalysis(module, injectionId)?.sources?.[source] ?? source; + let resolved: string | undefined; + for (const dependency of module?.dependencies.values() ?? []) { + if (dependency.data.name !== source || !dependency.absolutePath) continue; + if (resolved && resolved !== dependency.absolutePath) return; + resolved = dependency.absolutePath; + } + return resolved; +} + +function getAnalysis(module: MetroModule | undefined, injectionId: string): ModuleAncestorAnalysis | undefined { + for (const output of module?.output ?? []) { + const metadata = output.data.reactNativeBoost; + if (metadata?.injectionId === injectionId) return metadata.analysis; + } +} + +function findAdapter(adapters: Adapter[], graph: MetroGraph): Adapter | undefined { + return adapters.find((adapter) => + [...graph.dependencies.values()].some((module) => getAnalysis(module, adapter.injectionId)) + ); +} + +// ponytail: serialize passes per project; use graph-specific worker inputs if concurrent builds become slow. +function runExclusive(adapter: Adapter, operation: () => Promise): Promise { + const result = adapter.queue.then(operation); + adapter.queue = result.then( + () => {}, + () => {} + ); + return result; +} + +function writeSnapshot( + adapter: Adapter, + platform: string | undefined, + consumers: ConsumerImports, + spreads: ResolvedGraph['spreadConsumers'] +): void { + adapter.platforms[platform ?? ''] = consumers; + adapter.spreadPlatforms[platform ?? ''] = spreads; + const snapshot: AncestorSnapshot = { + version: 1, + revision: ++adapter.revision, + platforms: adapter.platforms, + spreadPlatforms: adapter.spreadPlatforms, + }; + const temporaryPath = `${adapter.snapshotPath}.${process.pid}.${adapter.revision}`; + fs.mkdirSync(path.dirname(adapter.snapshotPath), { recursive: true }); + fs.writeFileSync(temporaryPath, JSON.stringify(snapshot)); + fs.renameSync(temporaryPath, adapter.snapshotPath); +} + +function markTransformResultsStale(graph: MetroGraph, consumers: string[], revision: number): void { + for (const consumer of consumers) { + const module = graph.dependencies.get(consumer); + if (module) module.unstable_transformResultKey = `${module.unstable_transformResultKey ?? ''}.boost.${revision}`; + } +} + +function mergeDeltas(graph: MetroGraph, first: MetroDelta, second: MetroDelta): MetroDelta { + const added = new Map(first.added); + const modified = new Map(first.modified); + const deleted = new Set([...first.deleted, ...second.deleted]); + + for (const path of second.added.keys()) { + added.set(path, graph.dependencies.get(path)!); + modified.delete(path); + deleted.delete(path); + } + for (const path of second.modified.keys()) { + const module = graph.dependencies.get(path)!; + if (added.has(path)) added.set(path, module); + else modified.set(path, module); + deleted.delete(path); + } + for (const path of deleted) { + added.delete(path); + modified.delete(path); + } + return { added, deleted, modified }; +} diff --git a/packages/react-native-boost/src/metro/index.ts b/packages/react-native-boost/src/metro/index.ts new file mode 100644 index 00000000..1b6d34ac --- /dev/null +++ b/packages/react-native-boost/src/metro/index.ts @@ -0,0 +1,358 @@ +import { createHash } from 'node:crypto'; +import fs from 'node:fs'; +import { createRequire } from 'node:module'; +import os from 'node:os'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import type { PluginOptions } from '../plugin/types'; +import { validatePluginOptions } from '../plugin/utils/options'; +import { resolveReactNativeTarget, type ResolvedReactNativeTarget } from '../plugin/utils/react-native-target'; +import { installMetroGraphPatch } from './graph'; +import type { MetroTransformerManifest, MetroWorkerManifest } from './types'; + +export type { PluginOptions } from '../plugin/types'; + +const moduleDirectory = path.dirname(fileURLToPath(import.meta.url)); +const requireFromMetro = createRequire(import.meta.url); +const transformerDirectory = path.join(os.tmpdir(), 'react-native-boost'); +const supportedMetroMinors = new Set([83, 84, 87]); + +type CacheStore = { + name?: string; + get: (key: Buffer) => unknown; + set: (key: Buffer, value: unknown) => unknown; + clear?: () => unknown; +}; +type CacheStores = readonly CacheStore[] | ((metroCache: unknown) => readonly CacheStore[]); +type MetroConfig = { + projectRoot?: string; + transformerPath?: string; + cacheStores?: unknown; + transformer?: { + babelTransformerPath?: string; + uniwind?: unknown; + }; + resolver?: { + extraNodeModules?: Record; + resolveRequest?: unknown; + }; +}; + +type MetroIntegration = { + graphPath: string; + transformerPath: string; + version: string; +}; + +export function withBoostConfig(config: T, rawOptions: PluginOptions = {}): T { + let options = validatePluginOptions(rawOptions); + const projectRoot = path.resolve(config.projectRoot ?? process.cwd()); + const uniwindEnabled = + options.integrations?.uniwind === 'on' || + (options.integrations?.uniwind !== 'off' && config.transformer?.uniwind !== undefined); + if (uniwindEnabled && config.transformer?.uniwind === undefined) { + throw new Error('[react-native-boost] Apply withBoostConfig after withUniwindConfig.'); + } + if (uniwindEnabled || options.integrations?.uniwind !== undefined) { + options = { ...options, integrations: { ...options.integrations, uniwind: uniwindEnabled ? 'on' : 'off' } }; + } + const delegateRequest = config.transformer?.babelTransformerPath ?? 'metro-babel-transformer'; + if ( + (path.isAbsolute(delegateRequest) && path.dirname(delegateRequest) === transformerDirectory) || + (config.transformerPath && path.dirname(config.transformerPath) === transformerDirectory) + ) { + throw new Error('[react-native-boost] withBoostConfig was applied more than once.'); + } + const delegatePath = resolveModule(delegateRequest, projectRoot, 'Babel transformer'); + const mappedReactNative = config.resolver?.extraNodeModules?.['react-native']; + const configuredPackageJson = + options.target?.reactNative?.packageJson ?? + (mappedReactNative ? path.join(mappedReactNative, 'package.json') : undefined); + const targetResolution = resolveReactNativeTarget( + configuredPackageJson ? { packageJson: configuredPackageJson } : undefined, + [path.join(projectRoot, 'package.json')] + ); + if (configuredPackageJson && !targetResolution.target) { + throw new Error(`[react-native-boost] Cannot read React Native package at ${configuredPackageJson}.`); + } + + if (uniwindEnabled) { + const uniwindRoot = path.dirname(resolveModule('uniwind/package.json', projectRoot, 'Uniwind')); + options = { + ...options, + ignores: [...(options.ignores ?? []), `${uniwindRoot}/**`, `${path.resolve(moduleDirectory, '../runtime')}/**`], + }; + } + const resolver = uniwindEnabled + ? createUniwindResolver(config, projectRoot, targetResolution.target) + : config.resolver; + + const requestedCrossFileResolution = options.crossFileAncestorResolution !== false; + const metro = requestedCrossFileResolution + ? resolveMetroIntegration(config.transformerPath, delegatePath, projectRoot) + : undefined; + let crossFileResolution = Boolean(metro && isSupportedMetro(metro.version)); + if (requestedCrossFileResolution && !crossFileResolution && options.logLevel !== 'silent') { + console.warn( + `[react-native-boost] Cross-file ancestor resolution is disabled because ${metro ? `Metro ${metro.version} is unsupported` : 'Metro could not be resolved'}.` + ); + } + + let pluginOptions: MetroTransformerManifest['pluginOptions'] = createPluginOptions(options, targetResolution.target); + let injectionId = createInjectionId(projectRoot, pluginOptions, targetResolution.target, crossFileResolution); + const snapshotPath = path.join(transformerDirectory, `${injectionId}.json`); + + if (crossFileResolution) { + try { + initializeSnapshot(snapshotPath); + installMetroGraphPatch(metro!.graphPath, { injectionId, snapshotPath }); + pluginOptions = { + ...pluginOptions, + __reactNativeBoostProjectRoot: projectRoot, + __reactNativeBoostSnapshot: snapshotPath, + }; + } catch (error) { + crossFileResolution = false; + pluginOptions = createPluginOptions(options, targetResolution.target); + injectionId = createInjectionId(projectRoot, pluginOptions, targetResolution.target, false); + if (options.logLevel !== 'silent') { + console.warn(`[react-native-boost] Cross-file ancestor resolution is disabled: ${String(error)}`); + } + } + } + + const manifest: MetroTransformerManifest = { + babelTransformerPath: delegatePath, + pluginPath: path.resolve(moduleDirectory, '../plugin/index.js'), + pluginOptions, + injectionId, + }; + const babelTransformerPath = writeTransformer(manifest); + + if (config.resolver?.resolveRequest && !configuredPackageJson && options.logLevel !== 'silent') { + console.warn( + '[react-native-boost] A custom Metro resolver is active. Set target.reactNative.packageJson if it resolves React Native elsewhere.' + ); + } + if (!targetResolution.target && options.logLevel !== 'silent') { + console.warn( + '[react-native-boost] React Native could not be detected. Version-dependent optimizations will be skipped.' + ); + } + + return { + ...config, + resolver, + ...(crossFileResolution && metro + ? { + cacheStores: wrapCacheStores(config.cacheStores as CacheStores | undefined, injectionId), + transformerPath: writeWorker({ transformerPath: metro.transformerPath, injectionId }), + } + : {}), + transformer: { + ...config.transformer, + babelTransformerPath, + }, + }; +} + +function createPluginOptions(options: PluginOptions, installedTarget?: ResolvedReactNativeTarget): PluginOptions { + const { target, ...pluginOptions } = options; + void target; + if (!installedTarget) return pluginOptions; + return { + ...pluginOptions, + target: { reactNative: { packageJson: installedTarget.packageJson } }, + }; +} + +function createInjectionId( + projectRoot: string, + options: PluginOptions, + target: ResolvedReactNativeTarget | undefined, + crossFileResolution: boolean +): string { + return hash(`${projectRoot}:${JSON.stringify(options)}:${target?.fingerprint ?? 'unknown'}:${crossFileResolution}`); +} + +function writeTransformer(manifest: MetroTransformerManifest): string { + const transformerModule = path.resolve(moduleDirectory, 'transformer.js'); + const source = `module.exports = require(${JSON.stringify(transformerModule)}).createTransformer(${JSON.stringify(manifest)});\n`; + return writeWrapper(source); +} + +function writeWorker(manifest: MetroWorkerManifest): string { + const workerModule = path.resolve(moduleDirectory, 'worker.js'); + const source = `module.exports = require(${JSON.stringify(workerModule)}).createWorker(${JSON.stringify(manifest)});\n`; + return writeWrapper(source); +} + +function writeWrapper(source: string): string { + const filename = path.join(transformerDirectory, `${hash(source)}.cjs`); + fs.mkdirSync(transformerDirectory, { recursive: true }); + if (!fs.existsSync(filename)) fs.writeFileSync(filename, source); + return filename; +} + +function resolveModule(request: string, projectRoot: string, label: string): string { + try { + return createRequire(path.join(projectRoot, 'package.json')).resolve(request); + } catch (error) { + throw new Error( + `[react-native-boost] Cannot resolve ${label} ${JSON.stringify(request)} from ${projectRoot}. Set transformer.babelTransformerPath with require.resolve(...).`, + { cause: error } + ); + } +} + +function resolveMetroIntegration( + configuredTransformerPath: string | undefined, + babelTransformerPath: string, + projectRoot: string +): MetroIntegration | undefined { + const babelVersion = findPackage(babelTransformerPath, 'metro-babel-transformer')?.version; + const loadedMetro = findLoadedMetro(babelVersion); + if (loadedMetro) { + try { + const transformerRequest = configuredTransformerPath ?? 'metro-transform-worker'; + const transformerPath = path.isAbsolute(transformerRequest) + ? transformerRequest + : resolveFrom([path.join(projectRoot, 'package.json'), loadedMetro.packageJson], transformerRequest); + return { graphPath: loadedMetro.graphPath, transformerPath, version: loadedMetro.version }; + } catch {} + } + + for (const origin of [babelTransformerPath, path.join(projectRoot, 'package.json')]) { + try { + const metroRequire = createRequire(origin); + const transformerPath = configuredTransformerPath + ? path.isAbsolute(configuredTransformerPath) + ? configuredTransformerPath + : metroRequire.resolve(configuredTransformerPath) + : metroRequire.resolve('metro-transform-worker'); + if (babelVersion && findPackage(transformerPath, 'metro-transform-worker')?.version !== babelVersion) continue; + const packageJsonPath = metroRequire.resolve('metro/package.json'); + const version = (JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')) as { version: string }).version; + if (babelVersion && version !== babelVersion) continue; + return { + graphPath: metroRequire.resolve('metro/private/DeltaBundler/Graph'), + transformerPath, + version, + }; + } catch {} + } +} + +function resolveFrom(origins: string[], request: string): string { + for (const origin of origins) { + try { + return createRequire(origin).resolve(request); + } catch {} + } + throw new Error(`Cannot resolve ${request}.`); +} + +function findLoadedMetro( + expectedVersion: string | undefined +): { graphPath: string; packageJson: string; version: string } | undefined { + for (const filename of Object.keys(requireFromMetro.cache)) { + if (!filename.endsWith(`${path.sep}metro${path.sep}src${path.sep}DeltaBundler${path.sep}Graph.js`)) continue; + const metroPackage = findPackage(filename, 'metro'); + if (!metroPackage || (expectedVersion && metroPackage.version !== expectedVersion)) continue; + return { graphPath: filename, packageJson: metroPackage.packageJson, version: metroPackage.version }; + } +} + +function findPackage(filename: string, name: string): { packageJson: string; version: string } | undefined { + let directory = path.dirname(filename); + while (directory !== path.dirname(directory)) { + const packageJson = path.join(directory, 'package.json'); + try { + const manifest = JSON.parse(fs.readFileSync(packageJson, 'utf8')) as { name?: string; version?: string }; + if (manifest.name === name && manifest.version) return { packageJson, version: manifest.version }; + } catch {} + directory = path.dirname(directory); + } +} + +function isSupportedMetro(version: string): boolean { + const match = /^0\.(\d+)\./.exec(version); + return Boolean(match && supportedMetroMinors.has(Number(match[1]))); +} + +function initializeSnapshot(snapshotPath: string): void { + fs.mkdirSync(path.dirname(snapshotPath), { recursive: true }); + fs.writeFileSync(snapshotPath, JSON.stringify({ version: 1, revision: 0, platforms: {} })); +} + +function wrapCacheStores(stores: CacheStores | undefined, injectionId: string): CacheStores { + if (typeof stores === 'function') return (metroCache) => stores(metroCache).map((store) => wrap(store)); + return (stores ?? []).map((store) => wrap(store)); + + function wrap(store: CacheStore): CacheStore { + return { + name: store.name, + get: (key) => store.get.call(store, key), + set: (key, value) => (hasCrossFileAnalysis(value, injectionId) ? undefined : store.set.call(store, key, value)), + ...(store.clear ? { clear: () => store.clear!.call(store) } : {}), + }; + } +} + +function hasCrossFileAnalysis(value: unknown, injectionId: string): boolean { + if (!value || typeof value !== 'object') return false; + const output = (value as { output?: Array<{ data?: { reactNativeBoost?: unknown } }> }).output; + return Boolean( + output?.some(({ data }) => { + const metadata = data?.reactNativeBoost as + | { injectionId?: unknown; analysis?: { references?: unknown[]; spreadReferences?: unknown[] } } + | undefined; + return ( + metadata?.injectionId === injectionId && + Boolean(metadata.analysis?.references?.length || metadata.analysis?.spreadReferences?.length) + ); + }) + ); +} + +function hash(value: string): string { + return createHash('sha256').update(value).digest('hex'); +} + +function createUniwindResolver(config: MetroConfig, projectRoot: string, target?: ResolvedReactNativeTarget) { + const packageJson = resolveModule('uniwind/package.json', projectRoot, 'Uniwind'); + const manifest = JSON.parse(fs.readFileSync(packageJson, 'utf8')) as { + name: string; + version: string; + license: string; + }; + if (manifest.name !== 'uniwind' || !manifest.version.startsWith('1.12.') || manifest.license !== 'MIT') { + throw new Error('[react-native-boost] The Uniwind integration supports free Uniwind 1.12.x. Pro is not supported.'); + } + if (!target) throw new Error('[react-native-boost] Uniwind requires a resolved React Native target.'); + const root = path.dirname(packageJson); + const runtimeRoot = path.resolve(moduleDirectory, '../runtime') + path.sep; + const reactNativeIndex = path.join(path.dirname(target.packageJson), 'index.js'); + const hooks: Record = { + 'react-native-boost/uniwind/useStyle': path.join(root, 'src/components/native/useStyle.ts'), + 'react-native-boost/uniwind/useAccentColor': path.join(root, 'src/components/native/useAccentColor.ts'), + }; + for (const filename of Object.values(hooks)) { + if (!fs.existsSync(filename)) throw new Error(`[react-native-boost] Missing Uniwind runtime hook: ${filename}`); + } + const delegate = config.resolver?.resolveRequest as + | NonNullable + | undefined; + const resolveRequest: NonNullable = ( + context, + name, + platform + ) => { + if (platform !== 'web' && context.originModulePath.startsWith(runtimeRoot)) { + if (name === 'react-native') return context.resolveRequest(context, reactNativeIndex, platform); + if (hooks[name]) return context.resolveRequest(context, hooks[name], platform); + } + return (delegate ?? context.resolveRequest)(context, name, platform); + }; + return { ...config.resolver, resolveRequest }; +} diff --git a/packages/react-native-boost/src/metro/transformer.ts b/packages/react-native-boost/src/metro/transformer.ts new file mode 100644 index 00000000..15002db1 --- /dev/null +++ b/packages/react-native-boost/src/metro/transformer.ts @@ -0,0 +1,76 @@ +import { createHash } from 'node:crypto'; +import fs from 'node:fs'; +import { createRequire } from 'node:module'; +import { fileURLToPath } from 'node:url'; +import type { TransformOptions } from '@babel/core'; +import type { MetroTransformerManifest } from './types'; +import { publishAnalysis } from './bridge'; + +const requireFromTransformer = createRequire(import.meta.url); +const moduleFilename = fileURLToPath(import.meta.url); + +type BabelTransformerArgs = { + filename: string; + options: Record; + plugins?: TransformOptions['plugins']; + src: string; +}; + +type TransformResult = { + ast: unknown; + metadata?: Record; +}; + +type BabelTransformer = { + transform: (arguments_: BabelTransformerArgs) => TransformResult | Promise; + getCacheKey?: (options?: Record) => string; +}; + +export function createTransformer(manifest: MetroTransformerManifest) { + const delegate = loadTransformer(manifest.babelTransformerPath); + const boostPlugin = requireFromTransformer(manifest.pluginPath) as unknown; + const pluginOptions = { + ...manifest.pluginOptions, + __reactNativeBoost: manifest.injectionId, + }; + + return { + async transform(arguments_: BabelTransformerArgs): Promise { + const result = validateResult( + await delegate.transform({ + ...arguments_, + plugins: [[boostPlugin, pluginOptions], ...(arguments_.plugins ?? [])], + }) + ); + const metadata = result.metadata?.reactNativeBoost as { injectionId?: unknown; analysis?: unknown } | undefined; + if (metadata?.analysis) publishAnalysis(arguments_.filename, metadata); + return result; + }, + getCacheKey(options?: Record): string { + const delegateKey = delegate.getCacheKey?.(options) ?? ''; + return createHash('sha256') + .update(JSON.stringify(manifest)) + .update(delegateKey) + .update(fs.readFileSync(moduleFilename)) + .update(fs.readFileSync(manifest.pluginPath)) + .digest('hex'); + }, + }; + + function validateResult(result: TransformResult): TransformResult { + const metadata = result.metadata?.reactNativeBoost as { injectionId?: unknown } | undefined; + if (metadata?.injectionId !== manifest.injectionId) { + throw new Error('[react-native-boost] The configured Babel transformer did not run the Boost plugin.'); + } + return result; + } +} + +function loadTransformer(modulePath: string): BabelTransformer { + const loaded = requireFromTransformer(modulePath) as BabelTransformer | { default?: BabelTransformer }; + const transformer = 'transform' in loaded ? loaded : loaded.default; + if (!transformer || typeof transformer.transform !== 'function') { + throw new Error(`[react-native-boost] ${modulePath} is not a Babel transformer.`); + } + return transformer; +} diff --git a/packages/react-native-boost/src/metro/types.ts b/packages/react-native-boost/src/metro/types.ts new file mode 100644 index 00000000..a576985d --- /dev/null +++ b/packages/react-native-boost/src/metro/types.ts @@ -0,0 +1,16 @@ +import type { PluginOptions } from '../plugin/types'; + +export interface MetroTransformerManifest { + babelTransformerPath: string; + pluginPath: string; + pluginOptions: PluginOptions & { + __reactNativeBoostProjectRoot?: string; + __reactNativeBoostSnapshot?: string; + }; + injectionId: string; +} + +export interface MetroWorkerManifest { + transformerPath: string; + injectionId: string; +} diff --git a/packages/react-native-boost/src/metro/worker.ts b/packages/react-native-boost/src/metro/worker.ts new file mode 100644 index 00000000..ad412545 --- /dev/null +++ b/packages/react-native-boost/src/metro/worker.ts @@ -0,0 +1,67 @@ +import { createHash } from 'node:crypto'; +import fs from 'node:fs'; +import { createRequire } from 'node:module'; +import { fileURLToPath } from 'node:url'; +import { takeAnalysis } from './bridge'; +import type { MetroWorkerManifest } from './types'; + +const requireFromWorker = createRequire(import.meta.url); +const moduleFilename = fileURLToPath(import.meta.url); + +type MetroWorker = { + transform: ( + config: Record, + projectRoot: string, + filename: string, + data: Buffer, + options: Record + ) => Promise; + getCacheKey?: (config: Record, options?: { projectRoot?: string }) => string; +}; + +type MetroTransformResult = { + dependencies: unknown[]; + output: Array<{ data: Record; type: string }>; +}; + +export function createWorker(manifest: MetroWorkerManifest): MetroWorker { + const delegate = loadWorker(manifest.transformerPath); + + return { + async transform(config, projectRoot, filename, data, options) { + const result = await delegate.transform(config, projectRoot, filename, data, options); + const metadata = takeAnalysis(filename); + if (!metadata) { + if (/\.[cm]?[jt]sx?$/.test(filename)) { + throw new Error( + '[react-native-boost] The configured Metro transform worker did not run the Boost Babel transformer.' + ); + } + return result; + } + if (metadata.injectionId !== manifest.injectionId) { + throw new Error('[react-native-boost] Metro worker received analysis from a different Boost configuration.'); + } + for (const output of result.output) { + if (output.type.startsWith('js/')) output.data.reactNativeBoost = metadata; + } + return result; + }, + getCacheKey(config, options) { + return createHash('sha256') + .update(JSON.stringify(manifest)) + .update(delegate.getCacheKey?.(config, options) ?? '') + .update(fs.readFileSync(moduleFilename)) + .digest('hex'); + }, + }; +} + +function loadWorker(modulePath: string): MetroWorker { + const loaded = requireFromWorker(modulePath) as MetroWorker | { default?: MetroWorker }; + const worker = 'transform' in loaded ? loaded : loaded.default; + if (!worker || typeof worker.transform !== 'function') { + throw new Error(`[react-native-boost] ${modulePath} is not a Metro transform worker.`); + } + return worker; +} diff --git a/packages/react-native-boost/src/plugin/__tests__/build-time-accessible-default.test.ts b/packages/react-native-boost/src/plugin/__tests__/build-time-accessible-default.test.ts index d6f73a98..f99caee7 100644 --- a/packages/react-native-boost/src/plugin/__tests__/build-time-accessible-default.test.ts +++ b/packages/react-native-boost/src/plugin/__tests__/build-time-accessible-default.test.ts @@ -11,7 +11,7 @@ const compile = (platform?: string): string => caller: (platform ? { name: 'metro', platform } : { name: 'test' }) as TransformCaller, plugins: [ '@babel/plugin-syntax-jsx', - [boostPlugin, { silent: true, dangerouslyOptimizeTextWithUnknownAncestors: true }], + [boostPlugin, { logLevel: 'silent', assumptions: { unknownAncestorsDoNotRenderText: true } }], ], })!.code!; diff --git a/packages/react-native-boost/src/plugin/__tests__/fixtures-nesting-animated/intrinsic-ancestors/code.js b/packages/react-native-boost/src/plugin/__tests__/fixtures-nesting-animated/intrinsic-ancestors/code.js new file mode 100644 index 00000000..1731245b --- /dev/null +++ b/packages/react-native-boost/src/plugin/__tests__/fixtures-nesting-animated/intrinsic-ancestors/code.js @@ -0,0 +1,49 @@ +import { Animated as NativeAnimated, Text, View, Pressable } from 'react-native'; +import * as Native from 'react-native'; +import Reanimated, { createAnimatedComponent as animate } from 'react-native-reanimated'; +import * as Motion from 'react-native-reanimated'; + +const AnimatedPressable = animate(Pressable); +const AnimatedView = Reanimated.View; +const Custom = Reanimated.createAnimatedComponent(Unknown); + + + + native animated + + + namespace native animated + + + + reanimated + + + + namespace reanimated + + + factory + + + alias + + + scroll stays unknown + +; + + + + inline + + + inline native + + + unknown + + + not a default export member + +; diff --git a/packages/react-native-boost/src/plugin/__tests__/fixtures-nesting-animated/intrinsic-ancestors/output.js b/packages/react-native-boost/src/plugin/__tests__/fixtures-nesting-animated/intrinsic-ancestors/output.js new file mode 100644 index 00000000..7086dbcb --- /dev/null +++ b/packages/react-native-boost/src/plugin/__tests__/fixtures-nesting-animated/intrinsic-ancestors/output.js @@ -0,0 +1,87 @@ +import { + textDefaultOverflowStyle as _textDefaultOverflowStyle, + NativeText as _NativeText, + NativeView as _NativeView, +} from 'react-native-boost/runtime'; +import { Animated as NativeAnimated, Text, View, Pressable } from 'react-native'; +import * as Native from 'react-native'; +import Reanimated, { createAnimatedComponent as animate } from 'react-native-reanimated'; +import * as Motion from 'react-native-reanimated'; +const AnimatedPressable = animate(Pressable); +const AnimatedView = Reanimated.View; +const Custom = Reanimated.createAnimatedComponent(Unknown); + + + <_NativeText + style={[_textDefaultOverflowStyle, void 0]} + allowFontScaling={true} + ellipsizeMode={'tail'} + accessible={true}> + native animated + + + + <_NativeText + style={[_textDefaultOverflowStyle, void 0]} + allowFontScaling={true} + ellipsizeMode={'tail'} + accessible={true}> + namespace native animated + + + + <_NativeView> + <_NativeText + style={[_textDefaultOverflowStyle, void 0]} + allowFontScaling={true} + ellipsizeMode={'tail'} + accessible={true}> + reanimated + + + + + <_NativeText + style={[_textDefaultOverflowStyle, void 0]} + allowFontScaling={true} + ellipsizeMode={'tail'} + accessible={true}> + namespace reanimated + + + + <_NativeText + style={[_textDefaultOverflowStyle, void 0]} + allowFontScaling={true} + ellipsizeMode={'tail'} + accessible={true}> + factory + + + + <_NativeText + style={[_textDefaultOverflowStyle, void 0]} + allowFontScaling={true} + ellipsizeMode={'tail'} + accessible={true}> + alias + + + + scroll stays unknown + +; +<_NativeView> + + inline + + + inline native + + + unknown + + + not a default export member + +; diff --git a/packages/react-native-boost/src/plugin/__tests__/fixtures-nesting-animated/scroll-view-keeps-text-context/code.js b/packages/react-native-boost/src/plugin/__tests__/fixtures-nesting-animated/scroll-view-keeps-text-context/code.js new file mode 100644 index 00000000..d1c9b313 --- /dev/null +++ b/packages/react-native-boost/src/plugin/__tests__/fixtures-nesting-animated/scroll-view-keeps-text-context/code.js @@ -0,0 +1,7 @@ +import { Animated, Text } from 'react-native'; + + + + Label + +; diff --git a/packages/react-native-boost/src/plugin/__tests__/fixtures-nesting-animated/scroll-view-keeps-text-context/output.js b/packages/react-native-boost/src/plugin/__tests__/fixtures-nesting-animated/scroll-view-keeps-text-context/output.js new file mode 100644 index 00000000..daeef870 --- /dev/null +++ b/packages/react-native-boost/src/plugin/__tests__/fixtures-nesting-animated/scroll-view-keeps-text-context/output.js @@ -0,0 +1,6 @@ +import { Animated, Text, ScrollView as _AnimatedWrapperRemovalScrollView } from 'react-native'; + + <_AnimatedWrapperRemovalScrollView scrollEventThrottle={0.0001} collapsable={false} style={undefined}> + Label + +; diff --git a/packages/react-native-boost/src/plugin/__tests__/fixtures-nesting-unistyles/cascades-view-text/output.js b/packages/react-native-boost/src/plugin/__tests__/fixtures-nesting-unistyles/cascades-view-text/output.js index 4b0f32dd..910ecb28 100644 --- a/packages/react-native-boost/src/plugin/__tests__/fixtures-nesting-unistyles/cascades-view-text/output.js +++ b/packages/react-native-boost/src/plugin/__tests__/fixtures-nesting-unistyles/cascades-view-text/output.js @@ -1,6 +1,6 @@ +import _UnistylesNativeView from 'react-native-unistyles/components/native/NativeView'; import { NativeText as _UnistylesNativeText } from 'react-native-unistyles/components/native/NativeText'; import { getDefaultTextAccessible as _getDefaultTextAccessible } from 'react-native-boost/runtime'; -import _UnistylesNativeView from 'react-native-unistyles/components/native/NativeView'; import { Text, View } from 'react-native'; import { StyleSheet } from 'react-native-unistyles'; const styles = StyleSheet.create({ @@ -10,7 +10,7 @@ const styles = StyleSheet.create({ t: {}, }); const C = () => ( - <_UnistylesNativeView style={styles.box}> + <_UnistylesNativeText style={styles.t} allowFontScaling={true} @@ -27,5 +27,5 @@ const C = () => ( deep - + ); diff --git a/packages/react-native-boost/src/plugin/__tests__/fixtures-nesting-unistyles/mixed-origins-under-lean-view/output.js b/packages/react-native-boost/src/plugin/__tests__/fixtures-nesting-unistyles/mixed-origins-under-lean-view/output.js index 5e6f68c7..fd1a1aba 100644 --- a/packages/react-native-boost/src/plugin/__tests__/fixtures-nesting-unistyles/mixed-origins-under-lean-view/output.js +++ b/packages/react-native-boost/src/plugin/__tests__/fixtures-nesting-unistyles/mixed-origins-under-lean-view/output.js @@ -1,7 +1,6 @@ -import { getDefaultTextStyle as _getDefaultTextStyle, NativeText as _NativeText } from 'react-native-boost/runtime'; +import { processTextStyle as _processTextStyle, NativeText as _NativeText } from 'react-native-boost/runtime'; import { NativeText as _UnistylesNativeText } from 'react-native-unistyles/components/native/NativeText'; import { getDefaultTextAccessible as _getDefaultTextAccessible } from 'react-native-boost/runtime'; -import _UnistylesNativeView from 'react-native-unistyles/components/native/NativeView'; import { Text, View } from 'react-native'; import { StyleSheet } from 'react-native-unistyles'; const styles = StyleSheet.create({ @@ -9,7 +8,7 @@ const styles = StyleSheet.create({ label: {}, }); const C = (props) => ( - <_UnistylesNativeView style={styles.box}> + <_UnistylesNativeText style={styles.label} allowFontScaling={true} @@ -18,17 +17,14 @@ const C = (props) => ( unistyles <_NativeText - style={[ - _getDefaultTextStyle(), - { - color: 'red', - }, - ]} allowFontScaling={true} ellipsizeMode={'tail'} + {..._processTextStyle({ + color: 'red', + })} accessible={_getDefaultTextAccessible()}> plain literal unknown bails - + ); diff --git a/packages/react-native-boost/src/plugin/__tests__/fixtures-nesting/cascades-view-text/output.js b/packages/react-native-boost/src/plugin/__tests__/fixtures-nesting/cascades-view-text/output.js index a6344eb4..b60c689d 100644 --- a/packages/react-native-boost/src/plugin/__tests__/fixtures-nesting/cascades-view-text/output.js +++ b/packages/react-native-boost/src/plugin/__tests__/fixtures-nesting/cascades-view-text/output.js @@ -1,17 +1,18 @@ import { - NativeView as _NativeView, + NativeViewWithContext as _NativeViewWithContext, getDefaultTextStyle as _getDefaultTextStyle, getDefaultTextAccessible as _getDefaultTextAccessible, NativeText as _NativeText, + NativeView as _NativeView, } from 'react-native-boost/runtime'; import { Text, View } from 'react-native'; const C = () => ( - <_NativeView + <_NativeViewWithContext style={{ flex: 1, }}> <_NativeText - style={_getDefaultTextStyle()} + style={_getDefaultTextStyle() ? [_getDefaultTextStyle(), void 0] : void 0} allowFontScaling={true} ellipsizeMode={'tail'} accessible={_getDefaultTextAccessible()}> @@ -22,12 +23,12 @@ const C = () => ( gap: 4, }}> <_NativeText - style={_getDefaultTextStyle()} + style={_getDefaultTextStyle() ? [_getDefaultTextStyle(), void 0] : void 0} allowFontScaling={true} ellipsizeMode={'tail'} accessible={_getDefaultTextAccessible()}> deep - + ); diff --git a/packages/react-native-boost/src/plugin/__tests__/fixtures-nesting/intrinsic-ancestors/code.js b/packages/react-native-boost/src/plugin/__tests__/fixtures-nesting/intrinsic-ancestors/code.js new file mode 100644 index 00000000..ed9be8ea --- /dev/null +++ b/packages/react-native-boost/src/plugin/__tests__/fixtures-nesting/intrinsic-ancestors/code.js @@ -0,0 +1,76 @@ +import { + Text, + View, + Pressable, + TouchableOpacity, + TouchableHighlight, + KeyboardAvoidingView, + ImageBackground, + Modal, + DrawerLayoutAndroid, + RefreshControl, + TextInput, + TouchableWithoutFeedback, + ScrollView, +} from 'react-native'; +import * as Native from 'react-native'; + +const Root = () => ( + + + label + + +); +const Pass = () => ( + + + label + + +); + + + + pressable + + + opacity + + + highlight + + + keyboard + + + background + + + modal + + + drawer + + + namespace + + + still inline + +; + + + + not inline + + + inline input + + + injected props + + + custom header + +; diff --git a/packages/react-native-boost/src/plugin/__tests__/fixtures-nesting/intrinsic-ancestors/output.js b/packages/react-native-boost/src/plugin/__tests__/fixtures-nesting/intrinsic-ancestors/output.js new file mode 100644 index 00000000..617a41a3 --- /dev/null +++ b/packages/react-native-boost/src/plugin/__tests__/fixtures-nesting/intrinsic-ancestors/output.js @@ -0,0 +1,146 @@ +import { + NativeView as _NativeView, + getDefaultTextStyle as _getDefaultTextStyle, + getDefaultTextAccessible as _getDefaultTextAccessible, + NativeText as _NativeText, + NativeViewWithContext as _NativeViewWithContext, +} from 'react-native-boost/runtime'; +import { + Text, + View, + Pressable, + TouchableOpacity, + TouchableHighlight, + KeyboardAvoidingView, + ImageBackground, + Modal, + DrawerLayoutAndroid, + RefreshControl, + TextInput, + TouchableWithoutFeedback, + ScrollView, +} from 'react-native'; +import * as Native from 'react-native'; +const Root = () => ( + + <_NativeView> + <_NativeText + style={_getDefaultTextStyle() ? [_getDefaultTextStyle(), void 0] : void 0} + allowFontScaling={true} + ellipsizeMode={'tail'} + accessible={_getDefaultTextAccessible()}> + label + + + +); +const Pass = () => ( + + <_NativeViewWithContext> + <_NativeText + style={_getDefaultTextStyle() ? [_getDefaultTextStyle(), void 0] : void 0} + allowFontScaling={true} + ellipsizeMode={'tail'} + accessible={_getDefaultTextAccessible()}> + label + + + +); + + + <_NativeText + style={_getDefaultTextStyle() ? [_getDefaultTextStyle(), void 0] : void 0} + allowFontScaling={true} + ellipsizeMode={'tail'} + accessible={_getDefaultTextAccessible()}> + pressable + + + + <_NativeText + style={_getDefaultTextStyle() ? [_getDefaultTextStyle(), void 0] : void 0} + allowFontScaling={true} + ellipsizeMode={'tail'} + accessible={_getDefaultTextAccessible()}> + opacity + + + + <_NativeText + style={_getDefaultTextStyle() ? [_getDefaultTextStyle(), void 0] : void 0} + allowFontScaling={true} + ellipsizeMode={'tail'} + accessible={_getDefaultTextAccessible()}> + highlight + + + + <_NativeText + style={_getDefaultTextStyle() ? [_getDefaultTextStyle(), void 0] : void 0} + allowFontScaling={true} + ellipsizeMode={'tail'} + accessible={_getDefaultTextAccessible()}> + keyboard + + + + <_NativeText + style={_getDefaultTextStyle() ? [_getDefaultTextStyle(), void 0] : void 0} + allowFontScaling={true} + ellipsizeMode={'tail'} + accessible={_getDefaultTextAccessible()}> + background + + + + <_NativeText + style={_getDefaultTextStyle() ? [_getDefaultTextStyle(), void 0] : void 0} + allowFontScaling={true} + ellipsizeMode={'tail'} + accessible={_getDefaultTextAccessible()}> + modal + + + + <_NativeText + style={_getDefaultTextStyle() ? [_getDefaultTextStyle(), void 0] : void 0} + allowFontScaling={true} + ellipsizeMode={'tail'} + accessible={_getDefaultTextAccessible()}> + drawer + + + + <_NativeText + style={_getDefaultTextStyle() ? [_getDefaultTextStyle(), void 0] : void 0} + allowFontScaling={true} + ellipsizeMode={'tail'} + accessible={_getDefaultTextAccessible()}> + namespace + + + + still inline + +; +<_NativeView> + + <_NativeText + style={_getDefaultTextStyle() ? [_getDefaultTextStyle(), void 0] : void 0} + allowFontScaling={true} + ellipsizeMode={'tail'} + accessible={_getDefaultTextAccessible()}> + not inline + + + + inline input + + + injected props + + + custom header + +; diff --git a/packages/react-native-boost/src/plugin/__tests__/fixtures-nesting/text-in-text-stays-bailed/code.js b/packages/react-native-boost/src/plugin/__tests__/fixtures-nesting/text-in-text-stays-bailed/code.js deleted file mode 100644 index 55fdea2c..00000000 --- a/packages/react-native-boost/src/plugin/__tests__/fixtures-nesting/text-in-text-stays-bailed/code.js +++ /dev/null @@ -1,7 +0,0 @@ -import { Text } from 'react-native'; -const C = () => ( - - outer - inner - -); diff --git a/packages/react-native-boost/src/plugin/__tests__/fixtures-nesting/text-in-text-stays-bailed/output.js b/packages/react-native-boost/src/plugin/__tests__/fixtures-nesting/text-in-text-stays-bailed/output.js deleted file mode 100644 index 55fdea2c..00000000 --- a/packages/react-native-boost/src/plugin/__tests__/fixtures-nesting/text-in-text-stays-bailed/output.js +++ /dev/null @@ -1,7 +0,0 @@ -import { Text } from 'react-native'; -const C = () => ( - - outer - inner - -); diff --git a/packages/react-native-boost/src/plugin/__tests__/literal-spreads.test.ts b/packages/react-native-boost/src/plugin/__tests__/literal-spreads.test.ts new file mode 100644 index 00000000..4e92b89d --- /dev/null +++ b/packages/react-native-boost/src/plugin/__tests__/literal-spreads.test.ts @@ -0,0 +1,175 @@ +import { transformSync, types as t, type PluginObj, type TransformCaller } from '@babel/core'; +import { openLiteralSpreads } from '../utils/literal-spreads'; +import compiler from 'babel-plugin-react-compiler'; +import { describe, expect, it } from 'vitest'; +import boost from '../index'; +import type { PluginOptions } from '../types'; + +function transform(jsx: string, compile = false, options: PluginOptions = {}, platform = 'ios', preamble = '') { + return transformSync( + `import {Text, View, Image, ActivityIndicator} from 'react-native'; ${preamble} + export default function Case() { return ${jsx}; }`, + { + filename: 'literal-spreads.jsx', + configFile: false, + babelrc: false, + caller: { name: 'test', platform } as TransformCaller, + parserOpts: { plugins: ['jsx'] }, + plugins: [...(compile ? [compiler] : []), [boost, { logLevel: 'silent', ...options }]], + } + )!.code!; +} + +for (const compile of [false, true]) { + describe(`literal JSX spreads (compiler=${compile})`, () => { + it.each([ + ['Text', '{style: [{fontWeight: 400}, {fontWeight: 700}], numberOfLines: -2}'], + ['View', '{id: "card", tabIndex: 0}'], + ['Image', '{source: {uri: "https://example.com/a.png"}, style: {width: 20}}'], + ['ActivityIndicator', '{size: "large", animating: false}'], + ])('opens %s before host optimization', (component, props) => { + const output = transform(`<${component} {...${props}} />`, compile); + expect(output).toContain(`Native${component}`); + expect(output).not.toContain('...{'); + if (component === 'View') expect(output).toContain('nativeID="card"'); + }); + + it.each([ + '{id: "first", id: "last"}', + '{key: "key", id: "card"}', + '{ref: null, id: "card"}', + '{__proto__: null, id: "card"}', + '{get id() {return "card"}}', + '{id() {}}', + '{["id"]: "card"}', + '{...{id: "card"}}', + '{id: value}', + '{id: helper()}', + '{style: {__proto__: {opacity: 0.5}}, id: "card"}', + '{style: {...value}, id: "card"}', + '{style: {opacity: 0.5, opacity: 1}, id: "card"}', + '{style: [,,], id: "card"}', + '{"bad:key": 1, id: "card"}', + ])('leaves unsafe spread %s closed', (props) => { + const output = transform(``, compile); + expect(output).not.toContain('NativeText'); + expect(output).toContain('...'); + }); + + it.each([ + 'id="direct" {...{id: "spread"}}', + '{...{id: "spread"}} id="direct"', + '{...{id: "a"}} {...{id: "b"}}', + '{...{id: "a"}} {...unknown}', + '{...{id: "a"}} {...helper()}', + 'key="x" {...{id: "a"}}', + 'ref={null} {...{id: "a"}}', + 'testID={effect()} {...{id: "a"}}', + ])('does not open collisions or unsafe neighbors: %s', (attributes) => { + const output = transform(`label`, compile); + expect(output).not.toContain('NativeText'); + }); + + it('keeps Image callback and Text mutation checks', () => { + expect(transform(' {}} />', compile)).toContain('NativeImage'); + expect(transform('', compile)).toContain('NativeImage'); + expect(transform('', compile, {}, 'android')).not.toContain( + 'onLoad' + ); + expect(transform(' {}}} />', compile)).not.toContain('NativeImage'); + expect(transform('', compile)).not.toContain('NativeImage'); + expect( + transform( + 'label', + compile, + {}, + 'ios', + 'const state = {disabled: true};' + ) + ).not.toContain('NativeText'); + expect( + transform('label', compile) + ).toContain('NativeText'); + }); + + it('keeps unknown ancestors unsafe after compiler lowering', () => { + const output = transform( + 'label {}} />', + compile, + {}, + 'ios', + 'import Parent from "unknown-parent";' + ); + expect(output).not.toContain('NativeText'); + expect(output).not.toContain('NativeImage'); + }); + + it('respects ignore, off, web and integration routing', () => { + expect(transform('{/* @boost-ignore */}label', compile)).not.toContain( + 'NativeText' + ); + const off = transform('label', compile, { + optimizations: { 'native-text': 'off' }, + }); + expect(off).not.toContain('NativeText'); + expect(off).toContain('...'); + for (const platform of ['web', 'unknown']) { + expect(transform('label', compile, {}, platform)).not.toContain('NativeText'); + } + expect(transform('', compile, {}, 'web')).not.toContain('NativeImage'); + expect(transform('', compile, {}, 'unknown')).not.toContain('NativeImage'); + expect( + transform('label', compile, { integrations: { uniwind: 'on' } }) + ).toContain('react-native-boost/uniwind'); + expect( + transform('label', compile, { integrations: { unistyles: 'on' } }) + ).toContain('NativeText'); + }); + }); +} + +it('opens in place without cloning, hoisting, or changing string expression shape', () => { + let checked = false; + transformSync( + String.raw`const Case = () => ;`, + { + configFile: false, + babelrc: false, + parserOpts: { plugins: ['jsx'] }, + plugins: [ + (): PluginObj => ({ + visitor: { + JSXOpeningElement(path) { + const [first, spread, last] = path.node.attributes; + if (!t.isJSXSpreadAttribute(spread) || !t.isObjectExpression(spread.argument)) + throw new Error('spread expected'); + const values = spread.argument.properties.map((property) => (property as t.ObjectProperty).value); + openLiteralSpreads(path); + expect(path.node.attributes[0]).toBe(first); + expect(path.node.attributes[3]).toBe(last); + for (const [index, value] of values.entries()) { + const attribute = path.node.attributes[index + 1] as t.JSXAttribute; + expect(t.isJSXExpressionContainer(attribute.value) && attribute.value.expression).toBe(value); + } + expect(path.getFunctionParent()).not.toBeNull(); + checked = true; + }, + }, + }), + ], + } + ); + expect(checked).toBe(true); +}); + +it('preserves disjoint attributes and wrapper alias precedence', () => { + const output = transform( + 'label' + ); + expect(output).toContain('nativeID={"winner"}'); + expect(output).not.toContain('fallback'); + expect(output).toContain('selectable={true}'); + expect(output).toContain('color: "red"'); + expect(output).toContain('color: "blue"'); + expect(output).not.toContain('processTextStyle'); +}); diff --git a/packages/react-native-boost/src/plugin/__tests__/native-attribute-conformance.test.ts b/packages/react-native-boost/src/plugin/__tests__/native-attribute-conformance.test.ts index dbfb97f5..05f89c38 100644 --- a/packages/react-native-boost/src/plugin/__tests__/native-attribute-conformance.test.ts +++ b/packages/react-native-boost/src/plugin/__tests__/native-attribute-conformance.test.ts @@ -1,9 +1,9 @@ import { describe, it, expect } from 'vitest'; import { transformSync, types as t, type PluginObj } from '@babel/core'; import { generateTestPlugin } from '../utils/generate-test-plugin'; -import { imageOptimizer } from '../optimizers/image'; -import { textOptimizer } from '../optimizers/text'; -import { viewOptimizer } from '../optimizers/view'; +import { nativeImageOptimizer } from '../optimizers/native-image'; +import { nativeTextOptimizer } from '../optimizers/native-text'; +import { nativeViewOptimizer } from '../optimizers/native-view'; import { Optimizer, TargetPlatform } from '../types'; import { NATIVE_IMAGE_ATTRIBUTES, NATIVE_TEXT_ATTRIBUTES, NATIVE_VIEW_ATTRIBUTES } from './native-valid-attributes'; @@ -177,46 +177,48 @@ describe('native attribute conformance', () => { } }); - it('exercises the optimized path (otherwise conformance would pass vacuously)', () => { - expect(optimizeAndInspect(viewSource('testID="element"'), viewOptimizer, 'View')?.optimized).toBe(true); - expect(optimizeAndInspect(textSource('numberOfLines={1}'), textOptimizer, 'Text')?.optimized).toBe(true); - expect(optimizeAndInspect(imageSource(IMAGE_BASE_SOURCE), imageOptimizer, 'Image', 'ios')?.optimized).toBe(true); - }); - describe('View', () => { - it.each([...VIEW_WRAPPER_ONLY_PROPS, ...VIEW_PASSTHROUGH_PROPS])( - 'leaves only native attributes on the host for ', - (attributes) => { - const result = optimizeAndInspect(viewSource(attributes), viewOptimizer, 'View'); - if (!result?.optimized) return; // bailed out: nothing reaches the native component - const leaked = result.attributes.filter((attribute) => !NATIVE_VIEW_ATTRIBUTES.has(attribute)); - expect(leaked, `optimized leaks non-native attribute(s): ${leaked.join(', ')}`).toEqual( - [] - ); - } - ); + it.each(VIEW_WRAPPER_ONLY_PROPS)('leaves only native attributes on the host for ', (attributes) => { + const result = optimizeAndInspect(viewSource(attributes), nativeViewOptimizer, 'View'); + if (!result?.optimized) return; + const leaked = result.attributes.filter((attribute) => !NATIVE_VIEW_ATTRIBUTES.has(attribute)); + expect(leaked, `optimized leaks non-native attribute(s): ${leaked.join(', ')}`).toEqual( + [] + ); + }); + + it.each(VIEW_PASSTHROUGH_PROPS)('optimizes and leaves only native attributes on ', (attributes) => { + const result = optimizeAndInspect(viewSource(attributes), nativeViewOptimizer, 'View'); + expect(result?.optimized).toBe(true); + const leaked = result?.attributes.filter((attribute) => !NATIVE_VIEW_ATTRIBUTES.has(attribute)); + expect(leaked, `optimized leaks non-native attribute(s): ${leaked?.join(', ')}`).toEqual( + [] + ); + }); }); describe('Text', () => { - it.each([...TEXT_WRAPPER_ONLY_PROPS, ...TEXT_PASSTHROUGH_PROPS])( - 'leaves only native attributes on the host for ', - (attributes) => { - const result = optimizeAndInspect(textSource(attributes), textOptimizer, 'Text'); - if (!result?.optimized) return; // bailed out: nothing reaches the native component - const leaked = result.attributes.filter((attribute) => !NATIVE_TEXT_ATTRIBUTES.has(attribute)); - expect(leaked, `optimized leaks non-native attribute(s): ${leaked.join(', ')}`).toEqual( - [] - ); - } - ); + it.each(TEXT_WRAPPER_ONLY_PROPS)('leaves only native attributes on the host for ', (attributes) => { + const result = optimizeAndInspect(textSource(attributes), nativeTextOptimizer, 'Text'); + if (!result?.optimized) return; + const leaked = result.attributes.filter((attribute) => !NATIVE_TEXT_ATTRIBUTES.has(attribute)); + expect(leaked, `optimized leaks non-native attribute(s): ${leaked.join(', ')}`).toEqual([]); + }); + + it.each(TEXT_PASSTHROUGH_PROPS)('optimizes and leaves only native attributes on ', (attributes) => { + const result = optimizeAndInspect(textSource(attributes), nativeTextOptimizer, 'Text'); + expect(result?.optimized).toBe(true); + const leaked = result?.attributes.filter((attribute) => !NATIVE_TEXT_ATTRIBUTES.has(attribute)); + expect(leaked, `optimized leaks non-native attribute(s): ${leaked?.join(', ')}`).toEqual([]); + }); }); describe('Image', () => { it.each(IMAGE_WRAPPER_ONLY_PROPS)( 'leaves only native attributes on the host for when optimized', (attributes) => { - const result = optimizeAndInspect(imageSource(attributes), imageOptimizer, 'Image', 'ios'); - if (!result?.optimized) return; // bailed out: nothing reaches the native component + const result = optimizeAndInspect(imageSource(attributes), nativeImageOptimizer, 'Image', 'ios'); + if (!result?.optimized) return; const leaked = result.attributes.filter((attribute) => !NATIVE_IMAGE_ATTRIBUTES.has(attribute)); expect(leaked, `optimized leaks non-native attribute(s): ${leaked.join(', ')}`).toEqual( [] @@ -227,7 +229,7 @@ describe('native attribute conformance', () => { it.each(IMAGE_PASSTHROUGH_PROPS)( 'optimizes and leaves only native attributes on the host for ', (attributes) => { - const result = optimizeAndInspect(imageSource(attributes), imageOptimizer, 'Image', 'ios'); + const result = optimizeAndInspect(imageSource(attributes), nativeImageOptimizer, 'Image', 'ios'); expect(result?.optimized).toBe(true); const leaked = result?.attributes.filter((attribute) => !NATIVE_IMAGE_ATTRIBUTES.has(attribute)); expect( diff --git a/packages/react-native-boost/src/plugin/__tests__/native-valid-attributes.ts b/packages/react-native-boost/src/plugin/__tests__/native-valid-attributes.ts index a461cdd3..8fa6aefe 100644 --- a/packages/react-native-boost/src/plugin/__tests__/native-valid-attributes.ts +++ b/packages/react-native-boost/src/plugin/__tests__/native-valid-attributes.ts @@ -45,7 +45,7 @@ const hermesParser = (() => { parse(source: string, options: Record): t.File; }; } catch { - return undefined; + return; } })(); @@ -92,6 +92,21 @@ function parseReactNativeSource(subpath: string): t.File { * file `...spread` references (an object literal, or a call wrapping one such as * `ConditionallyIgnoredEventHandlers({ ... })`) and peeling `as const` casts. */ +function unwrap(node: t.Node | undefined | null): t.Node | undefined | null { + let current = node; + while ( + current && + (t.isTSAsExpression(current) || + t.isTSSatisfiesExpression(current) || + t.isTSTypeAssertion(current) || + t.isTypeCastExpression(current) || + t.isParenthesizedExpression(current)) + ) { + current = current.expression; + } + return current; +} + function extractValidAttributeKeys(subpath: string): Set { const ast = parseReactNativeSource(subpath); @@ -110,22 +125,6 @@ function extractValidAttributeKeys(subpath: string): Set { const keys = new Set(); - // Peel type-cast / parenthesized wrappers (e.g. `{ ... } as const`) to reach the underlying object. - const unwrap = (node: t.Node | undefined | null): t.Node | undefined | null => { - let current = node; - while ( - current && - (t.isTSAsExpression(current) || - t.isTSSatisfiesExpression(current) || - t.isTSTypeAssertion(current) || - t.isTypeCastExpression(current) || - t.isParenthesizedExpression(current)) - ) { - current = current.expression; - } - return current; - }; - const collectFromExpression = (node: t.Node | undefined | null, seen: Set): void => { const expression = unwrap(node); if (t.isObjectExpression(expression)) collectKeys(expression, seen); diff --git a/packages/react-native-boost/src/plugin/__tests__/nesting.test.ts b/packages/react-native-boost/src/plugin/__tests__/nesting.test.ts index 0bc38ffc..b688ccb1 100644 --- a/packages/react-native-boost/src/plugin/__tests__/nesting.test.ts +++ b/packages/react-native-boost/src/plugin/__tests__/nesting.test.ts @@ -1,11 +1,12 @@ import path from 'node:path'; +import type { TransformCaller } from '@babel/core'; import { pluginTester } from 'babel-plugin-tester'; import plugin from '..'; import { formatTestResult } from '../utils/format-test-result'; pluginTester({ plugin, - pluginOptions: { silent: true }, + pluginOptions: { logLevel: 'silent' }, title: 'nesting', fixtures: path.resolve(import.meta.dirname, 'fixtures-nesting'), babelOptions: { @@ -16,7 +17,19 @@ pluginTester({ pluginTester({ plugin, - pluginOptions: { silent: true, unistyles: true }, + pluginOptions: { logLevel: 'silent', optimizations: { 'animated-wrapper-removal': 'on' } }, + title: 'nesting animated', + fixtures: path.resolve(import.meta.dirname, 'fixtures-nesting-animated'), + babelOptions: { + caller: { name: 'test', platform: 'ios' } as TransformCaller, + plugins: ['@babel/plugin-syntax-jsx'], + }, + formatResult: formatTestResult, +}); + +pluginTester({ + plugin, + pluginOptions: { logLevel: 'silent', integrations: { unistyles: 'on' } }, title: 'nesting unistyles', fixtures: path.resolve(import.meta.dirname, 'fixtures-nesting-unistyles'), babelOptions: { diff --git a/packages/react-native-boost/src/plugin/__tests__/parity/boost.ts b/packages/react-native-boost/src/plugin/__tests__/parity/boost.ts index 2e9bf8ff..09c7d094 100644 --- a/packages/react-native-boost/src/plugin/__tests__/parity/boost.ts +++ b/packages/react-native-boost/src/plugin/__tests__/parity/boost.ts @@ -1,8 +1,9 @@ import { transformSync, type TransformCaller } from '@babel/core'; import * as React from 'react'; +import compiler from 'babel-plugin-react-compiler'; import boostPlugin from '../../index'; // src/plugin/index.ts β€” the full Boost plugin import { RUNTIME_MODULE_NAME } from '../../utils/constants'; -import { renderAndCaptureSingle } from './capture'; +import { renderAndCaptureAll, type Capture } from './capture'; import { writeAndImportFresh } from './generated'; import { setPlatformOS, type PlatformOS } from './mocks/Platform'; @@ -21,10 +22,16 @@ interface BoostOptimized { * per platform, so the plugin inlines build-time defaults exactly as in production). The generated * component is rendered at the test root, so that runtime parent is safe unless a test says otherwise. */ -function transformBoostCase(os: PlatformOS, jsxBody: string, preamble = '', runtimeParentIsSafe = true): string { +function transformBoostCase( + os: PlatformOS, + jsxBody: string, + preamble = '', + runtimeParentIsSafe = true, + compile = false +): string { setPlatformOS(os); const source = - `import { Image, Text, View } from 'react-native';\n${preamble}\n` + + `import { ActivityIndicator, Animated, Image, Text, View } from 'react-native';\n${preamble}\n` + `export default function Case(){ return ${jsxBody}; }`; const out = transformSync(source, { configFile: false, @@ -32,7 +39,17 @@ function transformBoostCase(os: PlatformOS, jsxBody: string, preamble = '', runt filename: 'boost-case.jsx', caller: { name: 'metro', platform: os } as TransformCaller, presets: [['@babel/preset-react', { runtime: 'automatic' }]], - plugins: [[boostPlugin, { silent: true, dangerouslyOptimizeTextWithUnknownAncestors: runtimeParentIsSafe }]], + plugins: [ + ...(compile ? [compiler] : []), + [ + boostPlugin, + { + logLevel: 'silent', + assumptions: { unknownAncestorsDoNotRenderText: runtimeParentIsSafe }, + optimizations: { 'animated-wrapper-removal': 'on' }, + }, + ], + ], }); return out!.code!; } @@ -42,8 +59,12 @@ function transformBoostCase(os: PlatformOS, jsxBody: string, preamble = '', runt * element. A compile-only check (no render), so nested snippets that produce multiple hosts can assert * deferral without tripping {@link renderAndCaptureSingle}. */ -export function boostOptimizes(os: PlatformOS, jsxBody: string, preamble = '', runtimeParentIsSafe = true): boolean { - return transformBoostCase(os, jsxBody, preamble, runtimeParentIsSafe).includes(RUNTIME_MODULE_NAME); +export function boostOptimizes(os: PlatformOS, jsxBody: string): boolean { + return transformBoostCase(os, jsxBody).includes(RUNTIME_MODULE_NAME); +} + +export function boostUsesRuntimeSelectionColor(os: PlatformOS, jsxBody: string, preamble = ''): boolean { + return transformBoostCase(os, jsxBody, preamble).includes('processSelectionColor'); } /** @@ -56,11 +77,23 @@ export async function captureBoost( jsxBody: string, preamble = '' ): Promise { - const code = transformBoostCase(os, jsxBody, preamble); - // Single-element snippet: the runtime import is injected iff that element was optimized. + const result = await captureBoostHosts(os, jsxBody, preamble); + if (!result.optimized) return result; + if (result.hosts.length !== 1) throw new Error(`Expected one Boost host, received ${result.hosts.length}`); + const { which, props } = result.hosts[0]; + return { optimized: true, which, props }; +} + +export async function captureBoostHosts( + os: PlatformOS, + jsxBody: string, + preamble = '', + runtimeParentIsSafe = true, + compile = false +): Promise { + const code = transformBoostCase(os, jsxBody, preamble, runtimeParentIsSafe, compile); if (!code.includes(RUNTIME_MODULE_NAME)) return { optimized: false }; const mod = await writeAndImportFresh('boost', code); - const { which, props } = renderAndCaptureSingle(React.createElement(mod.default)); - return { optimized: true, which, props }; + return { optimized: true, hosts: renderAndCaptureAll(React.createElement(mod.default)) }; } diff --git a/packages/react-native-boost/src/plugin/__tests__/parity/capture.tsx b/packages/react-native-boost/src/plugin/__tests__/parity/capture.tsx index 01680474..3ecd6b61 100644 --- a/packages/react-native-boost/src/plugin/__tests__/parity/capture.tsx +++ b/packages/react-native-boost/src/plugin/__tests__/parity/capture.tsx @@ -32,6 +32,7 @@ export const NativeTextCapturer = makeCapturer('NativeText'); export const NativeVirtualTextCapturer = makeCapturer('NativeVirtualText'); export const NativeViewCapturer = makeCapturer('NativeView'); export const NativeImageCapturer = makeCapturer('NativeImage'); +export const NativeActivityIndicatorCapturer = makeCapturer('NativeActivityIndicator'); export const TextInlineImageCapturer = makeCapturer('TextInlineImage'); /** Render an element and return every native host it produced, in render order. */ diff --git a/packages/react-native-boost/src/plugin/__tests__/parity/fibers.collect.ts b/packages/react-native-boost/src/plugin/__tests__/parity/fibers.collect.ts index ac4a003a..d0f501b1 100644 --- a/packages/react-native-boost/src/plugin/__tests__/parity/fibers.collect.ts +++ b/packages/react-native-boost/src/plugin/__tests__/parity/fibers.collect.ts @@ -1,5 +1,5 @@ import { writeFileSync } from 'node:fs'; -import { fileURLToPath } from 'node:url'; +import { fileURLToPath, URL } from 'node:url'; import { transformSync, type TransformCaller } from '@babel/core'; import * as React from 'react'; import { expect, test } from 'vitest'; @@ -47,11 +47,12 @@ async function compile(source: string, tag: string, boost: boolean): Promise ({ - NativeText: (await import('../capture')).NativeTextCapturer, -})); -vi.mock('../../../../runtime/components/native-view', async () => ({ - NativeView: (await import('../capture')).NativeViewCapturer, -})); -vi.mock('../../../../runtime/components/native-image', async () => ({ - NativeImage: (await import('../capture')).NativeImageCapturer, -})); - -import { captureBoost } from '../boost'; -import { captureWrapper } from '../wrapper'; +vi.mock('../../../../runtime/components/native-text', async () => { + const { NativeTextCapturer } = await import('../capture'); + return { NativeText: NativeTextCapturer }; +}); +vi.mock('../../../../runtime/components/native-view', async () => { + const { NativeViewCapturer } = await import('../capture'); + return { NativeView: NativeViewCapturer }; +}); +vi.mock('../../../../runtime/components/native-image', async () => { + const { NativeImageCapturer } = await import('../capture'); + return { NativeImage: NativeImageCapturer }; +}); +vi.mock('../../../../runtime/components/native-activity-indicator', async () => { + const { NativeActivityIndicatorCapturer } = await import('../capture'); + return { NativeActivityIndicator: NativeActivityIndicatorCapturer }; +}); + +import { captureBoostHosts } from '../boost'; +import { captureWrapperHosts } from '../wrapper'; import { normalize, normalizeImage } from '../normalize'; +import type { Capture } from '../capture'; import { reactNativeVersion, type PlatformOS } from '../mocks/Platform'; import { elementSpecArb, platformArb, render, type Tag } from './generator'; import { divergingKeys } from './diff'; -const SEED = Number(process.env.FUZZ_SEED ?? 0xb0051); +const SEED = Number(process.env.FUZZ_SEED ?? 0xb_00_51); const NUM_RUNS = Number(process.env.FUZZ_RUNS ?? 500); const DISCOVER = process.env.FUZZ_DISCOVER === '1'; const DISCOVER_SAMPLES = Number(process.env.FUZZ_DISCOVER_SAMPLES ?? 3000); +function normalizeHost(host: Capture) { + return { + which: host.which, + props: host.which === 'NativeImage' ? normalizeImage(host.props, reactNativeVersion.minor) : normalize(host.props), + }; +} + interface Skipped { status: 'skipped'; } @@ -45,26 +60,24 @@ type CaseResult = Skipped | Matched | Diverged; /** * Run one generated case through both oracles. Returns `skipped` when Boost bails (equivalent to the * wrapper by construction), `match` when the optimized output equals the wrapper, or `divergence` with - * the differing keys + both bags. Throws only on a genuine harness error (e.g. a value that makes BOTH - * sides throw, or a wrapper render that is not single-host) β€” never on a parity difference. + * the differing keys + both bags. Throws only on a genuine harness error, never on a parity difference. */ async function runCase(os: PlatformOS, jsxBody: string, preamble: string): Promise { - const boost = await captureBoost(os, jsxBody, preamble); + const boost = await captureBoostHosts(os, jsxBody, preamble); if (!boost.optimized) return { status: 'skipped' }; - const wrapper = await captureWrapper(os, jsxBody, preamble); - const isImage = boost.which === 'NativeImage' || wrapper.which === 'NativeImage'; - const boostNorm = isImage ? normalizeImage(boost.props, reactNativeVersion.minor) : normalize(boost.props); - const wrapperNorm = isImage ? normalizeImage(wrapper.props, reactNativeVersion.minor) : normalize(wrapper.props); - const keys = divergingKeys(boostNorm, wrapperNorm); + const wrapper = await captureWrapperHosts(os, jsxBody, preamble); + const boostProps = { hosts: boost.hosts.map((host) => normalizeHost(host)) }; + const wrapperProps = { hosts: wrapper.map((host) => normalizeHost(host)) }; + const keys = divergingKeys(boostProps, wrapperProps); + if (keys.length === 0) return { status: 'match' }; - if (boost.which === wrapper.which && keys.length === 0) return { status: 'match' }; return { status: 'divergence', - whichBoost: boost.which, - whichWrapper: wrapper.which, - boost: boostNorm, - wrapper: wrapperNorm, + whichBoost: boost.hosts.map((host) => host.which).join(' β†’ '), + whichWrapper: wrapper.map((host) => host.which).join(' β†’ '), + boost: boostProps, + wrapper: wrapperProps, keys, }; } @@ -75,7 +88,7 @@ function formatDivergence(os: PlatformOS, jsxBody: string, preamble: string, res return ( `parity divergence [${os}] on keys [${result.keys.join(', ')}]\n` + ` jsx: ${jsxBody}\n` + - ` preamble: ${preamble.replace(/\n/g, ' ⏎ ') || '(none)'}\n` + + ` preamble: ${preamble.replaceAll('\n', ' ⏎ ') || '(none)'}\n` + whichNote + ` boost: ${JSON.stringify(result.boost)}\n` + ` wrapper: ${JSON.stringify(result.wrapper)}` @@ -90,6 +103,7 @@ describe.skipIf(DISCOVER)('parity fuzzing', () => { let optimized = 0; let skipped = 0; const byTag: Record = { + ActivityIndicator: { optimized: 0, skipped: 0 }, Image: { optimized: 0, skipped: 0 }, Text: { optimized: 0, skipped: 0 }, View: { optimized: 0, skipped: 0 }, @@ -118,6 +132,7 @@ describe.skipIf(DISCOVER)('parity fuzzing', () => { `[fuzz] cases=${total} optimized=${optimized} skipped=${skipped} ` + `optimize-rate=${(rate * 100).toFixed(1)}% elapsed=${elapsed.toFixed(0)}ms ` + `(${(total / (elapsed / 1000)).toFixed(1)} cases/s) ` + + `activity=${byTag.ActivityIndicator.optimized}/${byTag.ActivityIndicator.optimized + byTag.ActivityIndicator.skipped} ` + `image=${byTag.Image.optimized}/${byTag.Image.optimized + byTag.Image.skipped} ` + `text=${byTag.Text.optimized}/${byTag.Text.optimized + byTag.Text.skipped} ` + `view=${byTag.View.optimized}/${byTag.View.optimized + byTag.View.skipped}` @@ -125,10 +140,9 @@ describe.skipIf(DISCOVER)('parity fuzzing', () => { // Anti-vacuous-green guard: a generator drifting into all-bail would pass trivially. expect(rate).toBeGreaterThan(0.5); + for (const counts of Object.values(byTag)) expect(counts.optimized).toBeGreaterThan(0); const imageTotal = byTag.Image.optimized + byTag.Image.skipped; - const imageRate = byTag.Image.optimized / imageTotal; - expect(imageTotal).toBeGreaterThan(0); - expect(imageRate).toBeGreaterThan(0.2); + expect(byTag.Image.optimized / imageTotal).toBeGreaterThan(0.2); }, Math.max(30_000, NUM_RUNS * 80) ); @@ -155,6 +169,7 @@ describe.skipIf(DISCOVER).each(['ios', 'android'] as const)('disabled reconcilia const jsx = buildDisabledCase(combo); const result = await runCase(os, jsx, ''); if (result.status === 'divergence') throw new Error(formatDivergence(os, jsx, '', result)); + expect(result.status).toBe('match'); }); }); diff --git a/packages/react-native-boost/src/plugin/__tests__/parity/fuzz/generator.ts b/packages/react-native-boost/src/plugin/__tests__/parity/fuzz/generator.ts index 1c92191d..5a71b0c2 100644 --- a/packages/react-native-boost/src/plugin/__tests__/parity/fuzz/generator.ts +++ b/packages/react-native-boost/src/plugin/__tests__/parity/fuzz/generator.ts @@ -1,5 +1,6 @@ import fc from 'fast-check'; import { + ACTIVITY_INDICATOR_VOCAB, IMAGE_SOURCE_VOCAB, IMAGE_VOCAB, TEXT_VOCAB, @@ -12,7 +13,7 @@ import { // attr, simplify a value, collapse dynamicβ†’static) and the renderer turns it into valid JSX by // construction, so shrinking never explores malformed snippets. -export type Tag = 'Image' | 'Text' | 'View'; +export type Tag = 'ActivityIndicator' | 'Image' | 'Text' | 'View'; /** One attribute: the value's source code, and whether it is inlined or hoisted to a preamble const. */ export interface GenAttr { @@ -135,6 +136,14 @@ const viewSpecArb: fc.Arbitrary = fc.record({ child: fc.constant(null), }); +const activityIndicatorSpecArb: fc.Arbitrary = fc.record({ + tag: fc.constant('ActivityIndicator'), + attrs: attrsArb(ACTIVITY_INDICATOR_VOCAB), + blacklisted: fc.constant(null), + spreads: spreadsArb(ACTIVITY_INDICATOR_VOCAB), + child: fc.constant(null), +}); + const imageSourceArb: fc.Arbitrary = fc .constantFrom(...IMAGE_SOURCE_VOCAB) .chain((spec) => spec.arb.map((code): GenAttr => ({ name: spec.name, code, dynamic: false }))); @@ -147,7 +156,12 @@ const imageSpecArb: fc.Arbitrary = fc.record({ child: fc.constant(null), }); -export const elementSpecArb: fc.Arbitrary = fc.oneof(textSpecArb, viewSpecArb, imageSpecArb); +export const elementSpecArb: fc.Arbitrary = fc.oneof( + activityIndicatorSpecArb, + textSpecArb, + viewSpecArb, + imageSpecArb +); export const platformArb = fc.constantFrom('ios' as const, 'android' as const); @@ -191,6 +205,10 @@ export function render(spec: ElementSpec): RenderedCase { return { preamble: declarations.join('\n'), jsxBody: `` }; } + if (spec.tag === 'ActivityIndicator') { + return { preamble: declarations.join('\n'), jsxBody: `` }; + } + if (spec.tag === 'Image') { return { preamble: declarations.join('\n'), jsxBody: `` }; } diff --git a/packages/react-native-boost/src/plugin/__tests__/parity/fuzz/vocabulary.ts b/packages/react-native-boost/src/plugin/__tests__/parity/fuzz/vocabulary.ts index 33a80e4d..5d418590 100644 --- a/packages/react-native-boost/src/plugin/__tests__/parity/fuzz/vocabulary.ts +++ b/packages/react-native-boost/src/plugin/__tests__/parity/fuzz/vocabulary.ts @@ -308,6 +308,29 @@ export const VIEW_VOCAB: PropSpec[] = [ { name: 'shouldRasterizeIOS', arb: bool, disposition: 'probe pass-through' }, ]; +// ── ActivityIndicator ────────────────────────────────────────────────────────────────────────────── +export const ACTIVITY_INDICATOR_VOCAB: PropSpec[] = [ + { name: 'animating', arb: withNullish(bool), disposition: 'undefined default true; null passes through' }, + { name: 'color', arb: withNullish(fc.constantFrom('"red"', '"blue"')), disposition: 'platform default' }, + { name: 'hidesWhenStopped', arb: withNullish(bool), disposition: 'undefined default true' }, + { + name: 'size', + arb: withNullish(fc.constantFrom('"small"', '"large"', '16', '24')), + disposition: 'size prop and inner dimensions', + }, + { name: 'style', arb: styleValue, disposition: 'outer container style composition' }, + { name: 'onLayout', arb: fc.constant('() => {}'), disposition: 'moved to outer View' }, + { name: 'testID', arb: withNullish(str), disposition: 'inner host pass-through' }, + { name: 'accessibilityLabel', arb: withNullish(str), disposition: 'inner host pass-through' }, + { + name: 'accessibilityRole', + arb: fc.constantFrom('"progressbar"', '"none"'), + disposition: 'inner host pass-through', + }, + { name: 'accessible', arb: withNullish(bool), disposition: 'inner host pass-through' }, + { name: 'collapsable', arb: bool, disposition: 'inner host pass-through' }, +]; + // ── Image ───────────────────────────────────────────────────────────────────────────────────────── export const IMAGE_SOURCE_VOCAB: PropSpec[] = [ { @@ -328,6 +351,17 @@ export const IMAGE_SOURCE_VOCAB: PropSpec[] = [ arb: fc.constantFrom('"https://example.com/logo.png"'), disposition: 'required static src', }, + { + name: 'srcSet', + arb: fc.constantFrom( + '"logo.png 1x, logo@2x.png 2x"', + '"logo.png, logo@3x.png 3x"', + '"logo@2x.png 2x, logo@3x.png 3x"', + '"logo.png 1.5x"', + '"logo.png 1x,logo@2x.png 2x"' + ), + disposition: 'required static srcSet', + }, ]; export const IMAGE_VOCAB: PropSpec[] = [ diff --git a/packages/react-native-boost/src/plugin/__tests__/parity/generated.ts b/packages/react-native-boost/src/plugin/__tests__/parity/generated.ts index 7ce1fc25..54acd9ad 100644 --- a/packages/react-native-boost/src/plugin/__tests__/parity/generated.ts +++ b/packages/react-native-boost/src/plugin/__tests__/parity/generated.ts @@ -1,5 +1,5 @@ import { writeFileSync } from 'node:fs'; -import { fileURLToPath, pathToFileURL } from 'node:url'; +import { fileURLToPath, pathToFileURL, URL } from 'node:url'; import type { ComponentType } from 'react'; let counter = 0; @@ -14,7 +14,7 @@ let counter = 0; * reused file is race-free. */ export async function writeAndImportFresh(label: string, code: string): Promise<{ default: ComponentType }> { - const file = fileURLToPath(new URL(`./__generated__/${label}-fuzz-${process.pid}.js`, import.meta.url)); + const file = fileURLToPath(new URL(`__generated__/${label}-fuzz-${process.pid}.js`, import.meta.url)); writeFileSync(file, code); return import(/* @vite-ignore */ `${pathToFileURL(file).href}?v=${counter++}`); } diff --git a/packages/react-native-boost/src/plugin/__tests__/parity/image-callbacks.test.ts b/packages/react-native-boost/src/plugin/__tests__/parity/image-callbacks.test.ts new file mode 100644 index 00000000..6a4dfbf2 --- /dev/null +++ b/packages/react-native-boost/src/plugin/__tests__/parity/image-callbacks.test.ts @@ -0,0 +1,209 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { createElement, type ComponentType } from 'react'; +import { transformSync, type TransformCaller } from '@babel/core'; +import compiler from 'babel-plugin-react-compiler'; +import { act, create as mount, type ReactTestRenderer } from 'react-test-renderer'; +import { create, diff } from 'react-native/Libraries/ReactNative/ReactFabricPublicInstance/ReactNativeAttributePayload'; +import boostPlugin from '../../index'; +import { NativeImageCapturer, renderAndCaptureAll } from './capture'; +import { captureBoostHosts } from './boost'; +import { captureWrapperHosts } from './wrapper'; +import { writeAndImportFresh } from './generated'; +import { setPlatformOS } from './mocks/Platform'; + +vi.mock('../../../runtime/components/native-text', async () => { + const { NativeTextCapturer } = await import('./capture'); + return { NativeText: NativeTextCapturer }; +}); +vi.mock('../../../runtime/components/native-view', async () => { + const { NativeViewCapturer } = await import('./capture'); + return { NativeView: NativeViewCapturer }; +}); +vi.mock('../../../runtime/components/native-image', async () => { + const { NativeImageCapturer } = await import('./capture'); + return { NativeImage: NativeImageCapturer }; +}); +vi.mock('../../../runtime/components/native-activity-indicator', async () => { + const { NativeActivityIndicatorCapturer } = await import('./capture'); + return { NativeActivityIndicator: NativeActivityIndicatorCapturer }; +}); + +afterEach(() => vi.unstubAllGlobals()); +const callbacks = ['onLoadStart', 'onLoad', 'onLoadEnd', 'onError']; +const eventAttributes = Object.fromEntries([...callbacks, 'shouldNotifyLoadEvents'].map((name) => [name, true])); +const eventProps = (props: Record) => + Object.fromEntries(Object.entries(props).filter(([name]) => Object.hasOwn(eventAttributes, name))); + +for (const platform of ['ios', 'android'] as const) { + describe(`raw Image callbacks (${platform})`, () => { + it.each([false, true])( + 'rejects parents that inspect or inject props, even on static images (compiler=%s)', + (compile) => { + for (const callback of ['', 'onLoad={() => {}}']) { + const source = `import {cloneElement} from 'react'; import {Image} from 'react-native'; + const Parent = ({children}) => cloneElement(children, {onLoad: () => {}}); + export default function Case() { return ; }`; + const code = transformSync(source, { + configFile: false, + babelrc: false, + filename: 'image-parent.jsx', + caller: { name: 'metro', platform } as TransformCaller, + presets: [['@babel/preset-react', { runtime: 'automatic' }]], + plugins: [ + ...(compile ? [compiler] : []), + [boostPlugin, { logLevel: 'silent', assumptions: { unknownAncestorsDoNotRenderText: true } }], + ], + })!.code!; + expect(code).not.toContain('NativeImage'); + } + } + ); + + it('keeps impure callback and source evaluation in order on the wrapper', async () => { + setPlatformOS(platform); + const state = { calls: [] as unknown[], handler: undefined as unknown }; + vi.stubGlobal('imageCallbackState', state); + const source = `import {Image, View} from 'react-native'; + const handler = () => {}; globalThis.imageCallbackState.handler = handler; + const mark = value => {globalThis.imageCallbackState.calls.push(value); return value}; + export default () => {mark('sibling') && null};`; + for (const optimized of [false, true]) { + let code = transformSync(source, { + configFile: false, + babelrc: false, + filename: 'image-order.jsx', + caller: { name: 'metro', platform } as TransformCaller, + presets: [['@babel/preset-react', { runtime: 'automatic' }]], + plugins: optimized ? [[boostPlugin, { logLevel: 'silent' }]] : [], + })!.code!; + expect(code).not.toContain('NativeImage'); + expect(code).toContain("import { Image, View } from 'react-native';"); + code = code.replace( + "import { Image, View } from 'react-native';", + `import Image from 'react-native/Libraries/Image/Image.${platform}'; import View from 'react-native/Libraries/Components/View/View';` + ); + const { default: Case } = await writeAndImportFresh('image-order', code); + state.calls.length = 0; + const hosts = renderAndCaptureAll(createElement(Case)); + expect(state.calls).toStrictEqual(['source', 'callback', 'testID', 'sibling']); + expect(hosts[1].props.onLoad).toBe(state.handler); + } + }); + + it.each([ + [false, false], + [true, false], + [false, true], + [true, true], + ])('keeps function identity, own keys, and renderer patches (compiler=%s, spread=%s)', async (compile, spread) => { + const state = { handler: undefined as unknown, events: [] as unknown[] }; + vi.stubGlobal('imageCallbackState', state); + const preamble = `const load = event => globalThis.imageCallbackState.events.push(event); + globalThis.imageCallbackState.handler = load;`; + let previousExpected = {}; + let previousActual = {}; + for (const value of ['load', '() => {}', 'null', 'undefined']) { + const source = spread ? "{...{source:{uri:'logo.png'}}}" : "source={{uri:'logo.png'}}"; + const jsx = ` `${name}={${value}}`).join(' ')} />`; + const expectedHosts = await captureWrapperHosts(platform, jsx, preamble); + const expected = eventProps(expectedHosts[1].props); + const expectedHandler = state.handler; + const result = await captureBoostHosts(platform, jsx, preamble, false, compile); + if (!result.optimized) throw new Error('Image must optimize'); + const actual = eventProps(result.hosts[1].props); + expect(Object.keys(actual).sort()).toStrictEqual(Object.keys(expected).sort()); + for (const name of Object.keys(expected)) { + if (typeof expected[name] === 'function') { + expect(actual[name]).toBeTypeOf('function'); + if (value === 'load') { + expect(expected[name]).toBe(expectedHandler); + expect(actual[name]).toBe(state.handler); + const event = { nativeEvent: { source: { uri: 'logo.png' }, target: 42 } }; + (actual[name] as (event: unknown) => void)(event); + expect(state.events.at(-1)).toBe(event); + } + } else expect(actual[name]).toBe(expected[name]); + } + expect(create(actual, eventAttributes)).toStrictEqual(create(expected, eventAttributes)); + expect(diff(previousActual, actual, eventAttributes)).toStrictEqual( + diff(previousExpected, expected, eventAttributes) + ); + previousActual = actual; + previousExpected = expected; + } + }); + + it.each([false, true])( + 'updates and removes mounted JS handlers without wrapping them (compiler=%s)', + async (compile) => { + setPlatformOS(platform); + vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); + const state = { handler: undefined as unknown, events: [] as unknown[] }; + vi.stubGlobal('imageCallbackState', state); + const results: Record[][] = []; + for (const optimized of [false, true]) { + const source = `${ + optimized + ? "import {Image, View} from 'react-native';" + : `import Image from 'react-native/Libraries/Image/Image.${platform}'; import View from 'react-native/Libraries/Components/View/View';` + } + export default function Case({mode}) { + const load = event => globalThis.imageCallbackState.events.push([mode, event]); + globalThis.imageCallbackState.handler = load; + return {mode < 2 ? : + mode === 2 ? : + }; + }`; + const code = transformSync(source, { + configFile: false, + babelrc: false, + filename: 'image-mounted.jsx', + caller: { name: 'metro', platform } as TransformCaller, + presets: [['@babel/preset-react', { runtime: 'automatic' }]], + plugins: [...(compile ? [compiler] : []), ...(optimized ? [[boostPlugin, { logLevel: 'silent' }]] : [])], + })!.code!; + if (optimized) { + expect(code).toContain('NativeImage'); + expect(code).not.toContain('(Image,'); + } + const module = await writeAndImportFresh('image-mounted', code); + const Case = module.default as ComponentType<{ mode: number }>; + let renderer: ReactTestRenderer; + const sequence: Record[] = []; + await act(async () => { + renderer = mount(createElement(Case, { mode: 0 })); + }); + for (const mode of [0, 1, 2, 3]) { + if (mode > 0) + await act(async () => { + renderer.update(createElement(Case, { mode })); + }); + const props = renderer!.root.findByType(NativeImageCapturer).props; + sequence.push(eventProps(props)); + if (mode < 2) { + expect(props.onLoad).toBe(state.handler); + const event = { nativeEvent: { target: 42 } }; + props.onLoad(event); + expect(state.events.at(-1)).toStrictEqual([mode, event]); + } + } + await act(async () => { + renderer.unmount(); + }); + results.push(sequence); + } + for (let index = 0; index < results[0].length; index++) { + const expected = results[0][index]; + const actual = results[1][index]; + expect(Object.keys(actual).sort()).toStrictEqual(Object.keys(expected).sort()); + expect(create(actual, eventAttributes)).toStrictEqual(create(expected, eventAttributes)); + if (index > 0) + expect(diff(results[1][index - 1], actual, eventAttributes)).toStrictEqual( + diff(results[0][index - 1], expected, eventAttributes) + ); + } + } + ); + }); +} diff --git a/packages/react-native-boost/src/plugin/__tests__/parity/mocks/ActivityIndicatorViewNativeComponent.ts b/packages/react-native-boost/src/plugin/__tests__/parity/mocks/ActivityIndicatorViewNativeComponent.ts new file mode 100644 index 00000000..345cff78 --- /dev/null +++ b/packages/react-native-boost/src/plugin/__tests__/parity/mocks/ActivityIndicatorViewNativeComponent.ts @@ -0,0 +1 @@ +export { NativeActivityIndicatorCapturer as default } from '../capture'; diff --git a/packages/react-native-boost/src/plugin/__tests__/parity/mocks/NativeAnimatedHelper.ts b/packages/react-native-boost/src/plugin/__tests__/parity/mocks/NativeAnimatedHelper.ts new file mode 100644 index 00000000..d20d5684 --- /dev/null +++ b/packages/react-native-boost/src/plugin/__tests__/parity/mocks/NativeAnimatedHelper.ts @@ -0,0 +1,14 @@ +const noop = () => {}; + +const API = new Proxy({}, { get: () => noop }); + +export default { + API, + assertNativeAnimatedModule: noop, + generateNewAnimationId: () => 1, + generateNewNodeTag: () => 1, + nativeEventEmitter: { addListener: () => ({ remove: noop }) }, + shouldSignalBatch: false, + shouldUseNativeDriver: () => false, + transformDataType: (value: unknown) => value, +}; diff --git a/packages/react-native-boost/src/plugin/__tests__/parity/mocks/PixelRatio.ts b/packages/react-native-boost/src/plugin/__tests__/parity/mocks/PixelRatio.ts new file mode 100644 index 00000000..46dc3660 --- /dev/null +++ b/packages/react-native-boost/src/plugin/__tests__/parity/mocks/PixelRatio.ts @@ -0,0 +1,5 @@ +// Older StyleSheet modules read device density at import time. +export default { + get: () => 2, + roundToNearestPixel: (value: number) => Math.round(value * 2) / 2, +}; diff --git a/packages/react-native-boost/src/plugin/__tests__/parity/mocks/ProgressBarAndroidNativeComponent.ts b/packages/react-native-boost/src/plugin/__tests__/parity/mocks/ProgressBarAndroidNativeComponent.ts new file mode 100644 index 00000000..345cff78 --- /dev/null +++ b/packages/react-native-boost/src/plugin/__tests__/parity/mocks/ProgressBarAndroidNativeComponent.ts @@ -0,0 +1 @@ +export { NativeActivityIndicatorCapturer as default } from '../capture'; diff --git a/packages/react-native-boost/src/plugin/__tests__/parity/mocks/ReactNativeFeatureFlags.ts b/packages/react-native-boost/src/plugin/__tests__/parity/mocks/ReactNativeFeatureFlags.ts index 49387fdc..640c0e87 100644 --- a/packages/react-native-boost/src/plugin/__tests__/parity/mocks/ReactNativeFeatureFlags.ts +++ b/packages/react-native-boost/src/plugin/__tests__/parity/mocks/ReactNativeFeatureFlags.ts @@ -1,7 +1,14 @@ // Release defaults used by the supported RN wrappers in parity tests. export const defaultTextToOverflowHidden = () => true; +export const enableNativeCSSParsing = () => false; export const enableNativeViewPropTransformations = () => false; export const fixImageSrcDimensionPropagation = () => true; export const reduceDefaultPropsInImage = () => false; export const reduceDefaultPropsInText = () => false; export const shouldUseLinkRoleForPressableText = () => true; +export const animatedShouldDebounceQueueFlush = () => false; +export const animatedShouldUseSingleOp = () => false; +export const cxxNativeAnimatedEnabled = () => false; +export const shouldUseAnimatedObjectForTransform = () => false; +export const shouldUseSetNativePropsInFabric = () => false; +export const useSharedAnimatedBackend = () => false; diff --git a/packages/react-native-boost/src/plugin/__tests__/parity/mocks/ReactNativePrivateInterface.ts b/packages/react-native-boost/src/plugin/__tests__/parity/mocks/ReactNativePrivateInterface.ts new file mode 100644 index 00000000..e86cac60 --- /dev/null +++ b/packages/react-native-boost/src/plugin/__tests__/parity/mocks/ReactNativePrivateInterface.ts @@ -0,0 +1 @@ +export const getNodeFromPublicInstance = () => null; diff --git a/packages/react-native-boost/src/plugin/__tests__/parity/mocks/RendererProxy.ts b/packages/react-native-boost/src/plugin/__tests__/parity/mocks/RendererProxy.ts new file mode 100644 index 00000000..4fbc4eeb --- /dev/null +++ b/packages/react-native-boost/src/plugin/__tests__/parity/mocks/RendererProxy.ts @@ -0,0 +1 @@ +export const findNodeHandle = () => null; diff --git a/packages/react-native-boost/src/plugin/__tests__/parity/mocks/StyleSheet.ts b/packages/react-native-boost/src/plugin/__tests__/parity/mocks/StyleSheet.ts index f9b42ccf..ba524379 100644 --- a/packages/react-native-boost/src/plugin/__tests__/parity/mocks/StyleSheet.ts +++ b/packages/react-native-boost/src/plugin/__tests__/parity/mocks/StyleSheet.ts @@ -3,6 +3,7 @@ import { flattenStyle } from '../normalize'; const StyleSheet = { create: (styles: T): T => styles, flatten: flattenStyle, + compose: (first: unknown, second: unknown) => (second ? [first, second] : first), }; export default StyleSheet; diff --git a/packages/react-native-boost/src/plugin/__tests__/parity/mocks/processColor.ts b/packages/react-native-boost/src/plugin/__tests__/parity/mocks/processColor.ts deleted file mode 100644 index 6ee09645..00000000 --- a/packages/react-native-boost/src/plugin/__tests__/parity/mocks/processColor.ts +++ /dev/null @@ -1,14 +0,0 @@ -// Stand-in for `react-native/Libraries/StyleSheet/processColor`. The real implementation does a CJS -// `require('../Utilities/Platform')` that escapes vite resolution and pulls raw Flow into node, so it must -// be mocked. It maps a named color to a packed int so the `selectionColor` parity case proves both sides -// actually *call* processColor: a passthrough stub would let a raw forward (Boost forgetting the call, or -// dropping the prop) go undetected. -// -// Both sides resolve to this one function so the expected value is unambiguous: the wrapper via its default -// import (`import processColor from '.../processColor'`, redirected by basename), the Boost side via the -// named re-export in `mocks/react-native.ts` (which backs the runtime index's `import { processColor }`). -export function processColor(color: unknown): unknown { - return color === 'red' ? 0xffff0000 : color; -} - -export default processColor; diff --git a/packages/react-native-boost/src/plugin/__tests__/parity/mocks/react-native.ts b/packages/react-native-boost/src/plugin/__tests__/parity/mocks/react-native.ts index 159491ff..01991e12 100644 --- a/packages/react-native-boost/src/plugin/__tests__/parity/mocks/react-native.ts +++ b/packages/react-native-boost/src/plugin/__tests__/parity/mocks/react-native.ts @@ -1,26 +1,31 @@ import Platform from './Platform'; -import { NativeImageCapturer, NativeTextCapturer, NativeViewCapturer } from '../capture'; -import { flattenStyle } from '../normalize'; -import { processColor } from './processColor'; +import View from 'react-native/Libraries/Components/View/View'; +import Text from 'react-native/Libraries/Text/Text'; +import processColor from 'react-native/Libraries/StyleSheet/processColor'; +import { + NativeActivityIndicatorCapturer, + NativeImageCapturer, + NativeTextCapturer, + NativeViewCapturer, +} from '../capture'; +import flattenStyle from 'react-native/Libraries/StyleSheet/flattenStyle'; -// Bare-specifier `react-native` surface for the Boost side. It backs the runtime index's -// `import { StyleSheet, processColor } from 'react-native'` plus the dead leftover `import { Text, View }` -// the plugin leaves in generated code. The host components resolve to the shared capturers so any path -// that bottoms out here is still captured. -export { Platform }; -// Same function the wrapper resolves (via the basename redirect to `mocks/processColor.ts`), so the -// `selectionColor` parity case has one unambiguous expected value across both sides. -export { processColor }; +// Keep real Text and View wrappers for unoptimized elements so the oracle preserves nested context. +export { Platform, processColor, Text, View }; +export { default as unstable_TextAncestorContext } from 'react-native/Libraries/Text/TextAncestorContext'; export const unstable_NativeText = NativeTextCapturer; export const unstable_NativeView = NativeViewCapturer; -export const Text = NativeTextCapturer; -export const View = NativeViewCapturer; +export const Animated = { Text, View }; export const Image = Object.assign(NativeImageCapturer, { resolveAssetSource: (source: T): T => source, }); +export const ActivityIndicator = NativeActivityIndicatorCapturer; -// `processTextStyle` (the runtime under test) calls `StyleSheet.flatten`, so it must faithfully -// reproduce RN's flatten semantics β€” an identity stub would silently break every dynamic-`style` parity -// comparison (arrays would never merge, the top-level conversions would never fire). It shares the one -// `flattenStyle` the comparison normalizer uses, so the Boost side flattens identically to the wrapper. -export const StyleSheet = { flatten: flattenStyle }; +// Both sides must inspect styles with the installed RN implementation. +export const StyleSheet: { + flatten: typeof import('react-native').StyleSheet.flatten; + compose: (first: unknown, second: unknown) => unknown; +} = { + flatten: flattenStyle, + compose: (first: unknown, second: unknown) => (second ? [first, second] : first), +}; diff --git a/packages/react-native-boost/src/plugin/__tests__/parity/normalize.ts b/packages/react-native-boost/src/plugin/__tests__/parity/normalize.ts index 03ee5196..531f4b3b 100644 --- a/packages/react-native-boost/src/plugin/__tests__/parity/normalize.ts +++ b/packages/react-native-boost/src/plugin/__tests__/parity/normalize.ts @@ -26,6 +26,8 @@ export const flattenStyle = (style: unknown): unknown => { * override (then get dropped here) rather than being dropped inside its own element first. */ export const normalize = (props: Record) => { + // JSON serialization intentionally drops undefined and function values. + // eslint-disable-next-line unicorn/prefer-structured-clone const normalized = JSON.parse( JSON.stringify('style' in props ? { ...props, style: flattenStyle(props.style) } : props) ); @@ -58,8 +60,7 @@ export const normalizeImage = (props: Record, reactNativeMinor: 'aria-selected', 'crossOrigin', 'referrerPolicy', - 'width', - 'height', + 'srcSet', ]) { delete normalized[key]; } diff --git a/packages/react-native-boost/src/plugin/__tests__/parity/parity.test.ts b/packages/react-native-boost/src/plugin/__tests__/parity/parity.test.ts index 10e09eb1..3ebf9ae9 100644 --- a/packages/react-native-boost/src/plugin/__tests__/parity/parity.test.ts +++ b/packages/react-native-boost/src/plugin/__tests__/parity/parity.test.ts @@ -4,23 +4,57 @@ import { describe, it, expect, vi } from 'vitest'; // (`processTextAccessibilityProps` / `processTextStyle`) under test. This is also what stops // native-text.tsx / native-view.tsx from running their CJS `require('react-native')` (see Β§4.5 of // the implementation plan), which would otherwise pull raw Flow source into node. -vi.mock('../../../runtime/components/native-text', async () => ({ - NativeText: (await import('./capture')).NativeTextCapturer, -})); -vi.mock('../../../runtime/components/native-view', async () => ({ - NativeView: (await import('./capture')).NativeViewCapturer, -})); -vi.mock('../../../runtime/components/native-image', async () => ({ - NativeImage: (await import('./capture')).NativeImageCapturer, -})); +vi.mock('../../../runtime/components/native-text', async () => { + const { NativeTextCapturer } = await import('./capture'); + return { NativeText: NativeTextCapturer }; +}); +vi.mock('../../../runtime/components/native-view', async () => { + const { NativeViewCapturer } = await import('./capture'); + return { NativeView: NativeViewCapturer }; +}); +vi.mock('../../../runtime/components/native-image', async () => { + const { NativeImageCapturer } = await import('./capture'); + return { NativeImage: NativeImageCapturer }; +}); +vi.mock('../../../runtime/components/native-activity-indicator', async () => { + const { NativeActivityIndicatorCapturer } = await import('./capture'); + return { NativeActivityIndicator: NativeActivityIndicatorCapturer }; +}); import { captureWrapper, captureWrapperHosts } from './wrapper'; -import { captureBoost, boostOptimizes } from './boost'; +import { captureBoost, captureBoostHosts, boostOptimizes, boostUsesRuntimeSelectionColor } from './boost'; import { reactNativeVersion } from './mocks/Platform'; import { normalize, normalizeImage } from './normalize'; const PLATFORMS = ['ios', 'android'] as const; const REACT_NATIVE_MINOR = reactNativeVersion.minor; +const UNKNOWN_TEXT_WRAPPER_PREAMBLE = ` + import React from 'react'; + import RealText from 'react-native/Libraries/Text/Text'; + class UnknownTextWrapper extends React.Component { + render() { + return React.createElement(RealText, null, this.props.children); + } + } +`; + +const STATIC_SELECTION_COLOR_CASES = [ + 'hello', + 'hello', + 'hello', + 'hello', + 'hello', + 'hello', + 'hello', + 'hello', + 'hello', + 'hello', + 'hello', + 'hello', + 'hello', + 'hello', + 'hello', +]; // `` cases use a primitive child (string, number, or template literal) so they render to // NativeText (not NativeVirtualText). @@ -46,9 +80,6 @@ const TEXT_CASES = [ // RN 0.85+ prepends `overflow: 'hidden'`, so the user's own overflow must win. 'hello', 'hello', - // `selectionColor` runs through `processColor` (a non-identity mock packs "red" β†’ an int), so both - // sides must emit the packed value β€” proving Boost calls processColor, not a raw forward. - 'hello', // Fully static styles are normalized at build time (object literal, no `processTextStyle`). Each // exercises a conversion the wrapper does at runtime; the flattened prop bags must still match. 'hello', // numeric fontWeight β†’ string @@ -60,17 +91,19 @@ const TEXT_CASES = [ 'hello', 'hello', 'hello', - // Bailed (deferred to the wrapper): `id`/`nativeID` via spread, and a dynamic `id` alongside `nativeID`. + // A dynamic `id` alongside `nativeID` still needs the wrapper. 'hello', 'hello', ]; // Text cases Boost is expected to bail on. Asserting the bail explicitly stops an unexpected bailout β€” // a silent loss of optimization β€” from masquerading as a passing parity test. -const BAILED_TEXT_CASES = new Set([ - 'hello', - 'hello', -]); +const BAILED_TEXT_CASES = new Set(['hello']); + +const ANIMATED_WRAPPER_REMOVAL_CASES = [ + '', + 'hello', +]; const VIEW_CASES = [ '', @@ -107,11 +140,33 @@ const VIEW_CASES = [ '', '', '', + '', + '', ]; // View cases Boost is expected to bail on. Asserting the bail explicitly stops an unexpected bailout β€” // a silent loss of optimization β€” from masquerading as a passing parity test. -const BAILED_VIEW_CASES = new Set(['', '']); +const BAILED_VIEW_CASES = new Set(['']); + +const ACTIVITY_INDICATOR_CASES: Array<[string, string?]> = [ + [''], + [''], + [''], + [''], + [''], + [''], + [''], + [' {}} testID="spinner" accessibilityLabel="Loading" />'], + [''], + [ + '', + 'const animating = undefined; const color = undefined; const hides = undefined; const size = 28; const style = { margin: 3 };', + ], + [''], + ['child'], +]; + +const BAILED_ACTIVITY_INDICATOR_CASES = new Set(['child']); const IMAGE_CASES = [ '', @@ -120,6 +175,10 @@ const IMAGE_CASES = [ '', '', '', + '', + '', + '', + '', '', '', '', @@ -130,19 +189,18 @@ const IMAGE_CASES = [ '', 'Logo', '{null}', + '', '', '', '', '', '', '', - '', ]; const BAILED_IMAGE_CASES = new Set([ - '', + '', '', - '', ]); const DYNAMIC_IMAGE_CASES: Array<[string, string]> = [ @@ -174,6 +232,33 @@ const IMAGE_PROP_ASSERTIONS = new Map, o '', (props) => expect(getFirstImageSource(props)).toMatchObject({ width: 16, height: 16 }), ], + [ + '', + (props) => + expect(props.source).toMatchObject([ + { uri: 'logo.png', scale: 1 }, + { uri: 'logo@2x.png', scale: 2 }, + ]), + ], + [ + '', + (props) => + expect(props.source).toMatchObject([ + { uri: 'logo@2x.png', scale: 2, width: 16, height: 8 }, + { uri: 'logo@3x.png', scale: 3, width: 16, height: 8 }, + { uri: 'logo.png', scale: 1, width: 16, height: 8 }, + ]), + ], + [ + '', + (props) => { + expect(getFirstImageSource(props)).toMatchObject({ uri: 'logo.png', scale: 1 }); + expect(getFirstImageSource(props).headers).toEqual({ + 'Access-Control-Allow-Credentials': 'true', + 'Referrer-Policy': 'origin', + }); + }, + ], [ '', (props) => @@ -199,7 +284,8 @@ const IMAGE_PROP_ASSERTIONS = new Map, o [ '', (props, os) => { - if (os === 'android') expect(props.accessibilityState).toEqual({ selected: true, busy: true }); + if (os === 'android' || REACT_NATIVE_MINOR >= 88) + expect(normalize(props).accessibilityState).toEqual({ selected: true, busy: true }); else expect(props.accessibilityState).toEqual({ selected: true }); }, ], @@ -241,6 +327,31 @@ describe('differential parity', () => { expect(normalize(boost.props)).toEqual(normalize(wrapper.props)); }); + it.each(ANIMATED_WRAPPER_REMOVAL_CASES)('Animated wrapper removal: %s', async (jsx) => { + const boost = await captureBoost(os, jsx); + if (!boost.optimized) throw new Error('expected animated wrapper removal case to optimize'); + const wrapper = await captureWrapper(os, jsx); + expect(boost.which).toEqual(wrapper.which); + expect(normalize(boost.props)).toEqual(normalize(wrapper.props)); + }); + + it.each(ACTIVITY_INDICATOR_CASES)('ActivityIndicator: %s', async (jsx, preamble = '') => { + const boost = await captureBoostHosts(os, jsx, preamble); + expect(boost.optimized).toBe(!BAILED_ACTIVITY_INDICATOR_CASES.has(jsx)); + if (!boost.optimized) return; + + const wrapper = await captureWrapperHosts(os, jsx, preamble); + expect(boost.hosts.map((host) => host.which)).toEqual(['NativeView', 'NativeActivityIndicator']); + expect(boost.hosts.map((host) => host.which)).toEqual(wrapper.map((host) => host.which)); + if (jsx.includes('onLayout')) { + expect(typeof boost.hosts[0].props.onLayout).toBe('function'); + expect(typeof wrapper[0].props.onLayout).toBe('function'); + expect(boost.hosts[1].props.onLayout).toBeUndefined(); + expect(wrapper[1].props.onLayout).toBeUndefined(); + } + expect(boost.hosts.map((host) => normalize(host.props))).toEqual(wrapper.map((host) => normalize(host.props))); + }); + it.each(IMAGE_CASES)('Image: %s', async (jsx) => { const boost = await captureBoost(os, jsx); expect(boost.optimized).toBe(!BAILED_IMAGE_CASES.has(jsx)); @@ -255,7 +366,6 @@ describe('differential parity', () => { it.each(DYNAMIC_IMAGE_CASES)('Image dynamic: %s', async (jsx, preamble) => { const boost = await captureBoost(os, jsx, preamble); - expect(boost.optimized).toBe(true); if (!boost.optimized) throw new Error('expected Image dynamic case to optimize'); const wrapper = await captureWrapper(os, jsx, preamble); expect(boost.which).toEqual(wrapper.which); @@ -265,11 +375,75 @@ describe('differential parity', () => { DYNAMIC_IMAGE_PROP_ASSERTIONS.get(jsx)?.(boost.props); }); + it.each([ + [false, 'logo.png'], + [true, 'logo.png'], + [false, ''], + [true, ''], + ] as const)('Image: preserves source headers and collisions (dynamic=%s, uri=%s)', async (dynamic, uri) => { + for (const headers of [ + 'null', + '{}', + '{ Authorization: "Bearer token", "Referrer-Policy": "no-referrer", "Access-Control-Allow-Credentials": "false" }', + ]) { + const source = `{ uri: ${JSON.stringify(uri)}, width: 16, height: 8, headers: ${headers} }`; + const preamble = dynamic ? `const source = ${source};` : ''; + const jsx = ``; + const boost = await captureBoost(os, jsx, preamble); + if (!boost.optimized) throw new Error('Image headers must optimize'); + const wrapper = await captureWrapper(os, jsx, preamble); + expect(boost.props.source).toStrictEqual(wrapper.props.source); + expect(boost.props.headers).toStrictEqual(wrapper.props.headers); + expect(normalizeImage(boost.props, REACT_NATIVE_MINOR)).toEqual( + normalizeImage(wrapper.props, REACT_NATIVE_MINOR) + ); + } + }); + + it.each([false, true])('Image: preserves native dimensions (dynamic=%s)', async (dynamic) => { + for (const source of [ + '{ uri: "logo.png", width: 16, height: 8 }', + '[{ uri: "logo.png", width: 16, height: 8 }]', + '[{ uri: "logo.png" }, { uri: "large.png", scale: 2 }]', + ]) { + const preamble = dynamic ? `const source = ${source};` : ''; + const jsx = ``; + const boost = await captureBoost(os, jsx, preamble); + if (!boost.optimized) throw new Error('Image dimensions must optimize'); + const wrapper = await captureWrapper(os, jsx, preamble); + for (const key of ['width', 'height']) { + expect(Object.hasOwn(boost.props, key)).toBe(Object.hasOwn(wrapper.props, key)); + expect(boost.props[key]).toBe(wrapper.props[key]); + } + expect(normalizeImage(boost.props, REACT_NATIVE_MINOR)).toEqual( + normalizeImage(wrapper.props, REACT_NATIVE_MINOR) + ); + } + }); + + it.each(STATIC_SELECTION_COLOR_CASES)('Text: precomputes selectionColor: %s', async (jsx) => { + expect(boostUsesRuntimeSelectionColor(os, jsx)).toBe(false); + const boost = await captureBoost(os, jsx); + if (!boost.optimized) throw new Error('expected static Text selectionColor case to optimize'); + const wrapper = await captureWrapper(os, jsx); + expect(boost.which).toBe(wrapper.which); + expect(normalize(boost.props)).toEqual(normalize(wrapper.props)); + }); + + it('Text: dynamic selectionColor uses runtime processing', async () => { + const jsx = 'hello'; + const preamble = 'const accent = getAccent(); function getAccent() { return "red"; }'; + expect(boostUsesRuntimeSelectionColor(os, jsx, preamble)).toBe(true); + const boost = await captureBoost(os, jsx, preamble); + if (!boost.optimized) throw new Error('expected Text dynamic selectionColor case to optimize'); + const wrapper = await captureWrapper(os, jsx, preamble); + expect(normalize(boost.props)).toEqual(normalize(wrapper.props)); + }); + it('Text: mixed dynamic style preserves userSelect flatten order', async () => { const jsx = 'hello'; const preamble = 'const dynamicStyle = { userSelect: "none" };'; const boost = await captureBoost(os, jsx, preamble); - expect(boost.optimized).toBe(true); if (!boost.optimized) throw new Error('expected Text mixed style case to optimize'); const wrapper = await captureWrapper(os, jsx, preamble); expect(boost.which).toEqual(wrapper.which); @@ -280,7 +454,6 @@ describe('differential parity', () => { const jsx = 'hello'; const preamble = 'const undefined = true;'; const boost = await captureBoost(os, jsx, preamble); - expect(boost.optimized).toBe(true); if (!boost.optimized) throw new Error('expected Text shadowed undefined case to optimize'); const wrapper = await captureWrapper(os, jsx, preamble); expect(boost.which).toEqual(wrapper.which); @@ -292,7 +465,6 @@ describe('differential parity', () => { 'hello'; const preamble = 'const _b0 = false;\nconst _s0 = { selectable: true };'; const boost = await captureBoost(os, jsx, preamble); - expect(boost.optimized).toBe(true); if (!boost.optimized) throw new Error('expected Text static userSelect spread case to optimize'); const wrapper = await captureWrapper(os, jsx, preamble); expect(boost.which).toEqual(wrapper.which); @@ -322,11 +494,143 @@ describe('differential parity', () => { it('Text returned by a renderer defers when mounted under Text', async () => { const jsx = '{renderBreak()}'; - const preamble = "const renderBreak = () => {'\\n'};"; + const preamble = String.raw`const renderBreak = () => {'\n'};`; const hosts = await captureWrapperHosts(os, jsx, preamble); expect(hosts.map((host) => host.which)).toEqual(['NativeText', 'NativeVirtualText']); - expect(boostOptimizes(os, jsx, preamble, false)).toBe(false); + const boost = await captureBoostHosts(os, jsx, preamble, false); + expect(boost.optimized).toBe(false); + }); + + it('keeps a nested Text virtual through a transparent component', async () => { + const jsx = ''; + const preamble = ` + import RealText from 'react-native/Libraries/Text/Text'; + // Keep the wrapper later so the analyzer sees its untransformed JSX. + const Label = () => label; + const Passthrough = ({ children }) => <>{children}; + `; + const wrapper = await captureWrapperHosts(os, jsx, preamble); + expect(wrapper.map((host) => host.which)).toEqual(['NativeText', 'NativeVirtualText']); + + const boost = await captureBoostHosts(os, jsx, preamble, false); + expect(boost.optimized).toBe(false); + }); + + it.each(['RealText', 'RealView'])('preserves root View context under %s', async (parent) => { + const jsx = `<${parent}>`; + const preamble = ` + import RealText from 'react-native/Libraries/Text/Text'; + import RealView from 'react-native/Libraries/Components/View/View'; + const Box = ({ children }) => {children}; + const Label = () => label; + `; + const wrapper = await captureWrapperHosts(os, jsx, preamble); + expect(wrapper.map((host) => host.which)).toEqual([ + parent === 'RealText' ? 'NativeText' : 'NativeView', + 'NativeView', + 'NativeText', + ]); + + const boost = await captureBoostHosts(os, jsx, preamble, false); + if (!boost.optimized) throw new Error('expected a context-preserving View'); + expect(boost.hosts.map((host) => host.which)).toEqual(wrapper.map((host) => host.which)); + expect(boost.hosts.map((host) => normalize(host.props))).toEqual(wrapper.map((host) => normalize(host.props))); + }); + + it.each([ + ['Text', true], + ['Text', false], + ['View', true], + ['View', false], + ])('preserves View context under %s with conditional Text: %s', async (parent, inline) => { + const jsx = `<${parent}>label`; + const preamble = `function Parent({children, inline}) { + if (inline) return {children}; + return children; + }`; + const wrapper = await captureWrapperHosts(os, jsx, preamble); + const boost = await captureBoostHosts(os, jsx, preamble, false); + expect(boost.optimized).toBe(true); + if (!boost.optimized) throw new Error('expected a context-preserving View'); + expect(boost.hosts.map((host) => host.which)).toEqual(wrapper.map((host) => host.which)); + expect(boost.hosts.map((host) => normalize(host.props))).toEqual(wrapper.map((host) => normalize(host.props))); + expect(wrapper.at(-1)?.which).toBe('NativeText'); + }); + + it.each([ + 'label', + 'label', + 'label', + 'label', + ])('preserves hosts and props with React Compiler: %s', async (jsx) => { + const preamble = ` + import React from 'react'; + function Box({children}) { return {children}; } + function Inspect({children}) { return React.cloneElement(children, {'aria-label': children.type.displayName}); } + `; + const wrapper = await captureWrapperHosts(os, jsx, preamble); + const boost = await captureBoostHosts(os, jsx, preamble, false, true); + if (!boost.optimized) throw new Error('expected an optimized outer View'); + expect(boost.hosts.map((host) => host.which)).toEqual(wrapper.map((host) => host.which)); + expect(boost.hosts.map((host) => normalize(host.props))).toEqual(wrapper.map((host) => normalize(host.props))); + }); + + it.each([false, true])('evaluates spread helpers once, in place (compiler: %s)', async (compile) => { + const calls: string[] = []; + vi.stubGlobal('recordSpreadCall', (value: string) => { + calls.push(value); + return value; + }); + try { + const jsx = ''; + const preamble = `function getProps() { recordSpreadCall('spread'); const props = {testID: 'cell'}; props.nativeID = 'from-spread'; return props; }`; + const wrapper = await captureWrapperHosts(os, jsx, preamble); + expect(calls).toEqual(['first', 'spread', 'last']); + calls.length = 0; + const boost = await captureBoostHosts(os, jsx, preamble, false, compile); + if (!boost.optimized) throw new Error('expected a proved spread'); + expect(calls).toEqual(['first', 'spread', 'last']); + expect(boost.hosts.map((host) => normalize(host.props))).toEqual(wrapper.map((host) => normalize(host.props))); + } finally { + Reflect.deleteProperty(globalThis, 'recordSpreadCall'); + } + }); + + it.each([false, true])('preserves spread-provided children under Text (compiler: %s)', async (compile) => { + const jsx = ''; + const preamble = `function getProps() { return {children: cell}; }`; + const wrapper = await captureWrapperHosts(os, jsx, preamble); + const boost = await captureBoostHosts(os, jsx, preamble, false, compile); + if (!boost.optimized) throw new Error('expected a context-preserving View'); + expect(boost.hosts.map((host) => host.which)).toEqual(['NativeText', 'NativeView', 'NativeText']); + expect(boost.hosts.map((host) => normalize(host.props))).toEqual(wrapper.map((host) => normalize(host.props))); + }); + + it('preserves View context directly under Text', async () => { + const jsx = 'label'; + const wrapper = await captureWrapperHosts(os, jsx); + const boost = await captureBoostHosts(os, jsx, '', false); + expect(boost.optimized).toBe(true); + if (!boost.optimized) throw new Error('expected a context-preserving View'); + expect(boost.hosts.map((host) => host.which)).toEqual(['NativeText', 'NativeView', 'NativeText']); + expect(boost.hosts.map((host) => normalize(host.props))).toEqual(wrapper.map((host) => normalize(host.props))); + }); + + it('optimizes ActivityIndicator when an unknown ancestor provides Text context', async () => { + const jsx = ''; + const wrapper = await captureWrapperHosts(os, jsx, UNKNOWN_TEXT_WRAPPER_PREAMBLE); + const boost = await captureBoostHosts(os, jsx, UNKNOWN_TEXT_WRAPPER_PREAMBLE, false); + if (!boost.optimized) throw new Error('expected ActivityIndicator to optimize'); + expect(boost.hosts.map((host) => host.which)).toEqual(wrapper.map((host) => host.which)); + expect(boost.hosts.map((host) => normalize(host.props))).toEqual(wrapper.map((host) => normalize(host.props))); + }); + + it('keeps Image under an unknown ancestor that could inject props', async () => { + const jsx = + ''; + const boost = await captureBoostHosts(os, jsx, UNKNOWN_TEXT_WRAPPER_PREAMBLE, false); + expect(boost.optimized).toBe(false); }); }); }); diff --git a/packages/react-native-boost/src/plugin/__tests__/parity/react-native-internals.d.ts b/packages/react-native-boost/src/plugin/__tests__/parity/react-native-internals.d.ts new file mode 100644 index 00000000..e277b088 --- /dev/null +++ b/packages/react-native-boost/src/plugin/__tests__/parity/react-native-internals.d.ts @@ -0,0 +1,56 @@ +declare module 'react-native/Libraries/StyleSheet/StyleSheetExports' { + const StyleSheet: typeof import('react-native').StyleSheet; + export default StyleSheet; +} + +declare module 'react-native/Libraries/StyleSheet/flattenStyle' { + const flatten: typeof import('react-native').StyleSheet.flatten; + export default flatten; +} + +declare module 'react-native/Libraries/Components/View/ReactNativeStyleAttributes' { + const attributes: Record unknown }>; + export default attributes; +} + +declare module 'react-native/Libraries/ReactNative/ReactFabricPublicInstance/ReactNativeAttributePayload' { + export function create(props: object, attributes: object): Record | null; + export function diff(previous: object, next: object, attributes: object): Record | null; +} + +declare module 'react-native/Libraries/Components/View/View' { + const View: typeof import('react-native').View; + export default View; +} + +declare module 'react-native/Libraries/Text/Text' { + const Text: typeof import('react-native').Text; + export default Text; +} + +declare module 'react-native/Libraries/Text/TextAncestorContext' { + const TextAncestorContext: import('react').Context; + export default TextAncestorContext; +} + +declare module 'react-native/Libraries/StyleSheet/processColor' { + const processColor: typeof import('react-native').processColor; + export default processColor; +} + +declare module 'react-native/Libraries/Text/Text' { + const Text: typeof import('react-native').Text; + export default Text; +} +declare module 'react-native/Libraries/Image/Image.ios' { + const Image: typeof import('react-native').Image; + export default Image; +} +declare module 'react-native/Libraries/Image/Image.android' { + const Image: typeof import('react-native').Image; + export default Image; +} +declare module 'react-native/Libraries/Components/ActivityIndicator/ActivityIndicator' { + const ActivityIndicator: typeof import('react-native').ActivityIndicator; + export default ActivityIndicator; +} diff --git a/packages/react-native-boost/src/plugin/__tests__/parity/text-raw.test.ts b/packages/react-native-boost/src/plugin/__tests__/parity/text-raw.test.ts new file mode 100644 index 00000000..fd20492c --- /dev/null +++ b/packages/react-native-boost/src/plugin/__tests__/parity/text-raw.test.ts @@ -0,0 +1,440 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { createElement } from 'react'; +import Text from 'react-native/Libraries/Text/Text'; +import StyleSheet from 'react-native/Libraries/StyleSheet/StyleSheetExports'; +import attributes from 'react-native/Libraries/Components/View/ReactNativeStyleAttributes'; +import { create, diff } from 'react-native/Libraries/ReactNative/ReactFabricPublicInstance/ReactNativeAttributePayload'; +import { processTextAccessibilityProps, processTextStyle } from '../../../runtime'; +import { renderAndCaptureSingle } from './capture'; +import { captureBoostHosts } from './boost'; +import { captureWrapper, captureWrapperHosts } from './wrapper'; +import { reactNativeVersion, setPlatformOS } from './mocks/Platform'; + +vi.mock('../../../runtime/components/native-text', async () => { + const { NativeTextCapturer } = await import('./capture'); + return { NativeText: NativeTextCapturer }; +}); +vi.mock('../../../runtime/components/native-view', async () => { + const { NativeViewCapturer } = await import('./capture'); + return { NativeView: NativeViewCapturer }; +}); +vi.mock('../../../runtime/components/native-image', async () => { + const { NativeImageCapturer } = await import('./capture'); + return { NativeImage: NativeImageCapturer }; +}); +vi.mock('../../../runtime/components/native-activity-indicator', async () => { + const { NativeActivityIndicatorCapturer } = await import('./capture'); + return { NativeActivityIndicator: NativeActivityIndicatorCapturer }; +}); + +const accessibilityProps = (props: Record) => + Object.fromEntries( + Object.entries(props).filter( + ([key]) => + key.startsWith('accessib') || + key.startsWith('aria-') || + key === 'alt' || + key === 'role' || + key === 'disabled' || + key === 'importantForAccessibility' + ) + ); + +const originalAttributes = { ...attributes }; +afterEach(() => Object.assign(attributes, originalAttributes)); +const wrapper = (props: Record) => renderAndCaptureSingle(createElement(Text, props, 'label')).props; + +for (const platform of ['ios', 'android'] as const) { + describe(`raw Text parity (${platform})`, () => { + it.each([false, true])( + 'matches static accessibility own keys and renderer updates (compiler=%s)', + async (compile) => { + const validAttributes = { + accessible: true, + disabled: true, + accessibilityLabel: true, + accessibilityLabelledBy: true, + accessibilityElementsHidden: true, + importantForAccessibility: true, + accessibilityState: { busy: true, checked: true, disabled: true, expanded: true, selected: true }, + accessibilityValue: { min: true, max: true, now: true, text: true }, + }; + for (const component of ['Text', 'View', 'Image']) { + let previousExpected = {}; + let previousActual = {}; + const groups = [ + 'aria-busy={true} aria-selected={false}', + 'accessibilityState={{disabled:false,extra:"kept?"}}', + 'accessibilityState={null}', + 'aria-busy={null} aria-checked={false} accessibilityState={{busy:null, disabled:true, selected:false, extra:"kept?"}}', + 'aria-busy={undefined} accessibilityState={null}', + 'aria-busy={null} accessibilityState={undefined}', + 'aria-busy={false} accessibilityState={{busy:true, checked:null}}', + 'aria-label={null} accessibilityLabel={null}', + 'aria-label={undefined} accessibilityLabel="fallback"', + 'aria-hidden={null} accessibilityElementsHidden={true} importantForAccessibility={null}', + 'aria-hidden={true} accessible={true} importantForAccessibility="auto"', + 'aria-hidden={false} accessible={null}', + ...(component === 'Text' + ? [ + 'disabled={false} accessibilityState={{disabled:true,extra:"kept"}}', + 'disabled={true} accessibilityState={null}', + 'disabled={false} accessibilityState={null}', + 'aria-busy={true} accessibilityRole={null} role={undefined}', + 'aria-busy={true} accessibilityRole="button" role="link"', + ] + : []), + ...(component === 'Image' + ? [ + 'alt={null}', + 'alt={null} accessible={false}', + 'accessible={null}', + 'accessible={undefined}', + 'accessibilityLabel={null}', + 'accessibilityLabelledBy={null}', + 'aria-labelledby="winner" accessibilityLabelledBy="fallback"', + 'alt="fallback" aria-label="winner" aria-hidden={true}', + 'aria-labelledby={null} accessibilityLabelledBy={null}', + 'aria-labelledby={null} accessibilityLabelledBy={["first","second"]}', + ] + : []), + ...(component === 'View' + ? [ + 'aria-valuenow={0} aria-valuetext={null} accessibilityValue={{min:0,max:10,text:"fallback"}}', + 'aria-valuenow={undefined} accessibilityValue={null}', + ] + : []), + ]; + if (component === 'Text') { + for (const disabled of ['null', 'undefined', 'false', 'true']) + for (const state of ['null', 'undefined', 'false', 'true']) + for (const aria of ['null', 'undefined', 'false', 'true']) + groups.push( + `disabled={${disabled}} accessibilityState={{disabled:${state}}} aria-disabled={${aria}}` + ); + } + for (const group of groups) { + const jsx = `<${component} ${component === 'Image' ? 'src="logo.png"' : ''} ${group} />`; + const expectedHosts = await captureWrapperHosts(platform, jsx); + const expected = expectedHosts[0].props; + const result = await captureBoostHosts(platform, jsx, '', true, compile); + if (!result.optimized) throw new Error(`${component} must optimize: ${group}`); + const actual = result.hosts[0].props; + expect(accessibilityProps(actual), `${component} ${group}`).toStrictEqual(accessibilityProps(expected)); + expect(create(actual, validAttributes)).toStrictEqual(create(expected, validAttributes)); + expect(diff(previousActual, actual, validAttributes)).toStrictEqual( + diff(previousExpected, expected, validAttributes) + ); + previousExpected = expected; + previousActual = actual; + } + } + }, + 30_000 + ); + + it('keeps dynamic state identity and prop evaluation order', async () => { + const state = { busy: false, disabled: false }; + const calls: string[] = []; + vi.stubGlobal('accessibilityProbe', { state, calls }); + try { + for (const component of ['Text', 'View', 'Image']) { + const preamble = + 'const state=globalThis.accessibilityProbe.state; const effect=name => {globalThis.accessibilityProbe.calls.push(name); return name;};'; + const jsx = `<${component} ${component === 'Image' ? 'src="logo.png"' : ''} aria-busy={true} accessibilityState={state} testID={effect("testID")} nativeID={effect("nativeID")} />`; + calls.length = 0; + const expectedHosts = await captureWrapperHosts(platform, jsx, preamble); + const expected = expectedHosts[0].props; + const expectedCalls = [...calls]; + calls.length = 0; + const result = await captureBoostHosts(platform, jsx, preamble); + // View retains its wrapper for the impure neighbors. + if (result.optimized) { + const actual = result.hosts[0].props; + expect(actual.accessibilityState).toStrictEqual(expected.accessibilityState); + expect(actual.accessibilityState === state).toBe(expected.accessibilityState === state); + expect(calls).toStrictEqual(expectedCalls); + } else expect(component).toBe('View'); + } + } finally { + vi.unstubAllGlobals(); + } + }); + + it.runIf(reactNativeVersion.minor >= 88)( + 'preserves dynamic Image accessibility own keys and state copies', + async () => { + for (const properties of [ + { alt: null, accessible: false }, + { accessible: null, accessibilityLabel: null, accessibilityLabelledBy: null }, + { accessible: undefined, accessibilityLabel: undefined, accessibilityLabelledBy: undefined }, + { 'aria-labelledby': 'label', 'accessibilityLabelledBy': 'fallback' }, + { 'aria-label': null, 'accessibilityLabel': null, 'alt': null }, + { 'aria-hidden': true, 'alt': 'logo', 'accessible': true }, + { accessibilityState: null }, + { accessibilityState: { busy: false, extra: 'removed' } }, + { 'aria-busy': true, 'accessibilityState': { busy: false, selected: true } }, + ]) { + vi.stubGlobal('imageAccessibilityInput', properties); + try { + const jsx = ` `${key}={input[${JSON.stringify(key)}]}`) + .join(' ')} />`; + const preamble = 'const input = globalThis.imageAccessibilityInput;'; + const expected = await captureWrapper(platform, jsx, preamble); + const result = await captureBoostHosts(platform, jsx, preamble); + if (!result.optimized) throw new Error('Dynamic Image accessibility must optimize'); + const actual = result.hosts[0].props; + expect(accessibilityProps(actual)).toStrictEqual(accessibilityProps(expected.props)); + if (properties.accessibilityState != null) + expect(actual.accessibilityState).not.toBe(properties.accessibilityState); + } finally { + vi.unstubAllGlobals(); + } + } + } + ); + + it('keeps literal spread string whitespace and entities', async () => { + for (const component of ['Text', 'View']) { + const jsx = `<${component} {...{id:"a\\n b &", testID:"a\\n b &"}} />`; + const expected = await captureWrapperHosts(platform, jsx); + const result = await captureBoostHosts(platform, jsx); + if (!result.optimized) throw new Error(`${component} must optimize`); + expect(result.hosts[0].props.nativeID).toBe(expected[0].props.nativeID); + expect(result.hosts[0].props.testID).toBe(expected[0].props.testID); + } + }); + + it.each([false, true])( + 'preserves literal spread host payloads and preprocessing (compiler=%s)', + async (compile) => { + const observations: unknown[] = []; + for (const key of ['color', 'fontWeight', 'userSelect', 'opacity']) { + StyleSheet.setStyleAttributePreprocessor(key, (value) => { + observations.push([key, value]); + return `${key}:${String(value)}:${observations.length}`; + }); + } + const validAttributes = { + style: attributes, + nativeID: true, + selectable: true, + accessibilityState: { busy: true, checked: true, disabled: true, expanded: true, selected: true }, + animating: true, + size: true, + source: true, + resizeMode: true, + }; + for (const component of ['Text', 'View', 'Image', 'ActivityIndicator']) { + let previousExpected = {}; + let previousActual = {}; + for (const color of ['red', 'blue']) { + const props = + component === 'Text' + ? `id:"winner", disabled:false, accessibilityState:{disabled:true}, style:[{color:"${color}",fontWeight:400},{fontWeight:700,userSelect:"text"}]` + : component === 'Image' + ? `source:{uri:"logo.png",width:20,height:30}, style:[{opacity:0.2},{opacity:0.8}]` + : component === 'ActivityIndicator' + ? `size:"large", animating:false, style:[{opacity:0.2},{opacity:0.8}]` + : `id:"winner", style:[{opacity:0.2},{opacity:0.8}]`; + const jsx = `<${component} {...{${props}}} />`; + const expected = await captureWrapperHosts(platform, jsx); + const result = await captureBoostHosts(platform, jsx, '', true, compile); + expect(result.optimized).toBe(true); + if (!result.optimized) throw new Error(`${component} must optimize`); + expect(result.hosts.length).toBe(expected.length); + for (let index = 0; index < expected.length; index++) { + const expectedProps = expected[index].props; + const actualProps = result.hosts[index].props; + expect(actualProps.style).toStrictEqual(expectedProps.style); + if (component === 'Text') + expect(actualProps.accessibilityState).toStrictEqual(expectedProps.accessibilityState); + observations.length = 0; + const expectedPayload = create(expectedProps, validAttributes); + const expectedCalls = [...observations]; + observations.length = 0; + expect(create(actualProps, validAttributes)).toStrictEqual(expectedPayload); + expect(observations).toStrictEqual(expectedCalls); + observations.length = 0; + const patch = diff(previousExpected, expectedProps, validAttributes); + const updateCalls = [...observations]; + observations.length = 0; + expect(diff(previousActual, actualProps, validAttributes)).toStrictEqual(patch); + expect(observations).toStrictEqual(updateCalls); + previousExpected = expectedProps; + previousActual = actualProps; + } + } + } + } + ); + + it.each([null, undefined, false, true])('preserves aria-hidden %s and native own keys', (hidden) => { + setPlatformOS(platform); + const props = { 'aria-hidden': hidden, 'accessibilityElementsHidden': true, 'importantForAccessibility': null }; + const expected = wrapper(props); + const actual = processTextAccessibilityProps(props); + for (const key of [ + 'accessibilityElementsHidden', + 'importantForAccessibility', + 'accessibilityLabel', + 'accessibilityState', + ]) { + expect(Object.hasOwn(actual, key), key).toBe(Object.hasOwn(expected, key)); + expect(actual[key], key).toBe(expected[key]); + } + }); + + it('preserves release-specific state identity and mutations across renders', () => { + setPlatformOS(platform); + for (const aria of [{}, { 'aria-busy': true }]) { + const expectedState = { disabled: true }; + const actualState = { disabled: true }; + for (const disabled of [false, true, undefined]) { + const expected = wrapper({ accessibilityState: expectedState, disabled, ...aria }); + const actual = processTextAccessibilityProps({ accessibilityState: actualState, disabled, ...aria }); + expect(actualState).toStrictEqual(expectedState); + expect(actual.accessibilityState === actualState).toBe(expected.accessibilityState === expectedState); + expect(actual.accessibilityState).toStrictEqual(expected.accessibilityState); + expect(actual.disabled).toBe(expected.disabled); + } + } + }); + + it('preserves release-specific frozen state errors and ARIA copies', () => { + setPlatformOS(platform); + const state = Object.freeze({ disabled: true }); + if (reactNativeVersion.minor >= 85 && reactNativeVersion.minor < 88) { + expect(() => wrapper({ accessibilityState: state, disabled: false })).toThrow(TypeError); + expect(() => processTextAccessibilityProps({ accessibilityState: state, disabled: false })).toThrow(TypeError); + } else { + expect( + processTextAccessibilityProps({ accessibilityState: state, disabled: false }).accessibilityState + ).toStrictEqual(wrapper({ accessibilityState: state, disabled: false }).accessibilityState); + } + const props = { 'accessibilityState': state, 'disabled': false, 'aria-busy': true }; + expect(processTextAccessibilityProps(props).accessibilityState).toStrictEqual(wrapper(props).accessibilityState); + }); + + it('keeps style identity, undefined override keys, and reads mutations on each call', () => { + setPlatformOS(platform); + const style = [{ color: 'red', fontWeight: 400, userSelect: 'none', verticalAlign: 'middle' }]; + for (const weight of [400, 700]) { + style[0].fontWeight = weight; + const expected = wrapper({ style }); + const actual = processTextStyle(style as never); + expect(actual.style).toStrictEqual(expected.style); + const normalized = reactNativeVersion.minor >= 85 ? (actual.style as unknown[])[1] : actual.style; + expect((normalized as unknown[])[0]).toBe(style); + expect(actual.selectable).toBe(expected.selectable); + } + const plain = [{ color: 'red' }, { color: 'blue' }]; + const actual = processTextStyle(plain).style; + expect(reactNativeVersion.minor >= 85 ? (actual as unknown[])[1] : actual).toBe(plain); + }); + + it.each(['', 'style=""', 'style={null}', 'style={false}', 'style={undefined}'])( + 'keeps empty style shape: %s', + async (attribute) => { + const jsx = `label`; + const { props: expected } = await captureWrapper(platform, jsx); + const result = await captureBoostHosts(platform, jsx); + if (!result.optimized) throw new Error('Text must stay optimized'); + expect(result.hosts[0].props.style).toStrictEqual(expected.style); + } + ); + + it.each([false, true])( + 'does not move caller-state mutation before sibling JSX evaluation (compiler=%s)', + async (compile) => { + const preamble = 'const state = {disabled:true};'; + const jsx = + 'firstsecond'; + const expected = await captureWrapperHosts(platform, jsx, preamble); + const result = await captureBoostHosts(platform, jsx, preamble, true, compile); + if (!result.optimized) throw new Error('The safe sibling must stay optimized'); + expect(result.hosts[2].props.testID).toBe(expected[2].props.testID); + expect(result.hosts[1].props.accessibilityState).toStrictEqual(expected[1].props.accessibilityState); + } + ); + + it('observes late preprocessor replacement and exceptions on raw numeric weights', async () => { + const jsx = 'label'; + const { props: expected } = await captureWrapper(platform, jsx); + const result = await captureBoostHosts(platform, jsx); + if (!result.optimized) throw new Error('Text must stay optimized'); + const actual = result.hosts[0].props; + const validAttributes = { style: attributes }; + for (const replacement of ['first', 'second']) { + const calls: unknown[] = []; + StyleSheet.setStyleAttributePreprocessor('fontWeight', (value) => { + calls.push(value); + return `${replacement}:${String(value)}`; + }); + const expectedPayload = create(expected, validAttributes); + expect(calls).toStrictEqual([400, '400']); + calls.length = 0; + expect(create(actual, validAttributes)).toStrictEqual(expectedPayload); + expect(calls).toStrictEqual([400, '400']); + } + StyleSheet.setStyleAttributePreprocessor('fontWeight', (value) => { + if (typeof value === 'number') throw new Error('raw weight'); + return value; + }); + expect(() => create(expected, validAttributes)).toThrow('raw weight'); + expect(() => create(actual, validAttributes)).toThrow('raw weight'); + }); + + for (const dynamic of [false, true]) { + for (const compile of [false, true]) { + it(`preserves renderer preprocessing and update patches (dynamic=${dynamic}, compiler=${compile})`, async () => { + const observations: unknown[] = []; + for (const key of ['color', 'fontWeight', 'userSelect', 'verticalAlign', 'textAlignVertical', 'opacity']) { + StyleSheet.setStyleAttributePreprocessor(key, (value: unknown) => { + observations.push([key, value]); + return `${key}:${String(value)}:${observations.length}`; + }); + } + const validAttributes = { style: attributes, selectable: true }; + let previousExpected = {}; + let previousActual = {}; + const styles = [ + '[{color:"red", opacity:0.2}, {color:"blue", opacity:0.8}]', + '[{color:"green", opacity:0.4}, {color:"blue", opacity:0.8}]', + '[{fontWeight:400,userSelect:"none",verticalAlign:"middle"},{color:"red"}]', + '[{fontWeight:700,userSelect:"text",verticalAlign:"top"},{color:null}]', + '[{color:"red"}, {color:undefined}]', + '[{color:"red"}, false]', + '{color:"blue",userSelect:null,verticalAlign:null}', + 'null', + ]; + for (const style of styles) { + const preamble = dynamic ? `const input = ${style};` : ''; + const jsx = `label`; + const { props: expected } = await captureWrapper(platform, jsx, preamble); + const result = await captureBoostHosts(platform, jsx, preamble, true, compile); + expect(result.optimized).toBe(true); + if (!result.optimized) throw new Error('Text must stay optimized'); + const actual = result.hosts[0].props; + observations.length = 0; + const expectedCreate = create(expected, validAttributes); + const expectedCalls = [...observations]; + observations.length = 0; + expect(create(actual, validAttributes)).toStrictEqual(expectedCreate); + expect(observations).toStrictEqual(expectedCalls); + observations.length = 0; + const expectedPatch = diff(previousExpected, expected, validAttributes); + const expectedUpdateCalls = [...observations]; + observations.length = 0; + expect(diff(previousActual, actual, validAttributes)).toStrictEqual(expectedPatch); + expect(observations).toStrictEqual(expectedUpdateCalls); + expect(actual.style).toStrictEqual(expected.style); + previousExpected = expected; + previousActual = actual; + } + }); + } + } + }); +} diff --git a/packages/react-native-boost/src/plugin/__tests__/parity/uniwind/react-native.ts b/packages/react-native-boost/src/plugin/__tests__/parity/uniwind/react-native.ts new file mode 100644 index 00000000..46d90d8d --- /dev/null +++ b/packages/react-native-boost/src/plugin/__tests__/parity/uniwind/react-native.ts @@ -0,0 +1,21 @@ +export * from '../mocks/react-native'; +export { default as Text } from 'react-native/Libraries/Text/Text'; +import * as React from 'react'; +import ImageIOS from 'react-native/Libraries/Image/Image.ios'; +import ImageAndroid from 'react-native/Libraries/Image/Image.android'; +import Platform from '../mocks/Platform'; +export const Image: React.ComponentType & + Pick = Object.assign( + (props: React.ComponentProps) => + React.createElement(Platform.OS === 'android' ? ImageAndroid : ImageIOS, props), + { resolveAssetSource: ImageIOS.resolveAssetSource } +); +export { default as ActivityIndicator } from 'react-native/Libraries/Components/ActivityIndicator/ActivityIndicator'; +export const Dimensions = { get: () => ({ width: 400, height: 800 }), addEventListener: () => ({ remove() {} }) }; +export const Appearance = { + getColorScheme: () => 'light', + addChangeListener: () => ({ remove() {} }), + setColorScheme() {}, +}; +export const I18nManager = { isRTL: false }; +export const PixelRatio = { get: () => 2, getFontScale: () => 1 }; diff --git a/packages/react-native-boost/src/plugin/__tests__/parity/uniwind/uniwind.test.tsx b/packages/react-native-boost/src/plugin/__tests__/parity/uniwind/uniwind.test.tsx new file mode 100644 index 00000000..a289b840 --- /dev/null +++ b/packages/react-native-boost/src/plugin/__tests__/parity/uniwind/uniwind.test.tsx @@ -0,0 +1,366 @@ +import * as React from 'react'; +import { afterAll, describe, expect, it, vi } from 'vitest'; +import { createRequire } from 'node:module'; +import { dirname, join } from 'node:path'; +import { fileURLToPath, URL } from 'node:url'; +import { mkdirSync, rmSync, writeFileSync } from 'node:fs'; +import { transformSync, type TransformCaller } from '@babel/core'; +import boost from '../../../index'; +import { renderAndCaptureAll } from '../capture'; +import { normalize, normalizeImage } from '../normalize'; +import { reactNativeVersion, setPlatformOS } from '../mocks/Platform'; +import * as optimized from '../../../../runtime/uniwind'; + +vi.mock('../../../../runtime/components/native-text', () => + import('../capture').then(({ NativeTextCapturer }) => ({ NativeText: NativeTextCapturer })) +); +vi.mock('../../../../runtime/components/native-view', () => + import('../capture').then(({ NativeViewCapturer }) => ({ NativeView: NativeViewCapturer })) +); +vi.mock('../../../../runtime/components/native-image', () => + import('../capture').then(({ NativeImageCapturer }) => ({ NativeImage: NativeImageCapturer })) +); +vi.mock('../../../../runtime/components/native-activity-indicator', () => + import('../capture').then(({ NativeActivityIndicatorCapturer }) => ({ + NativeActivityIndicator: NativeActivityIndicatorCapturer, + })) +); + +const require = createRequire(import.meta.url); +const root = join(dirname(require.resolve('uniwind/package.json')), 'src'); +const load = (name: string) => import(/* @vite-ignore */ `${root}/${name}`); +const { UniwindStore } = await load('core/native/store.ts'); +const { UniwindContext } = await load('core/context.ts'); +const { StyleDependency } = await load('common/consts.ts'); +const { default: View } = await load('components/native/View.tsx'); +const { default: Text } = await load('components/native/Text.tsx'); +const { default: Image } = await load('components/native/Image.tsx'); +const { default: ActivityIndicator } = await load('components/native/ActivityIndicator.tsx'); +const original = { NativeView: View, NativeText: Text, NativeImage: Image, NativeActivityIndicator: ActivityIndicator }; +const style = (values: Record, condition = {}) => ({ + entries: Object.entries(values).map(([key, value]) => [key, typeof value === 'function' ? value : () => value]), + minWidth: 0, + maxWidth: Infinity, + theme: null, + orientation: null, + rtl: null, + active: null, + focus: null, + disabled: null, + dataAttributes: null, + dependencies: null, + importantProperties: [], + complexity: 0, + ...condition, +}); +UniwindStore.reinit( + () => ({ + vars: { '--color': () => 'red' }, + scopedVars: {}, + stylesheet: { + box: [style({ width: 40, padding: 8 })], + themed: [ + style({ backgroundColor: 'red' }), + style({ backgroundColor: 'blue' }, { theme: 'dark', dependencies: [StyleDependency.Theme] }), + ], + variable: [ + style( + { backgroundColor: (vars: Record unknown>) => vars['--color'](vars) }, + { dependencies: [StyleDependency.Variables] } + ), + ], + selected: [style({ opacity: 0.5 }, { dataAttributes: { 'data-selected': 'true' } })], + text: [ + style({ + fontWeight: 700, + verticalAlign: 'middle', + userSelect: 'none', + WebkitLineClamp: 2, + overflow: 'visible', + }), + ], + disabled: [style({ color: 'blue' }, { disabled: true })], + accent: [style({ accentColor: '#ff0000' })], + image: [style({ objectFit: 'contain', width: 70 })], + }, + }), + ['light', 'dark'] +); + +const capture = (Component: React.ElementType, props: object) => + renderAndCaptureAll(React.createElement(Component, props)).map((item) => ({ + which: item.which, + props: item.which === 'NativeImage' ? normalizeImage(item.props, reactNativeVersion.minor) : normalize(item.props), + })); +const generated = fileURLToPath(new URL('__generated__/', import.meta.url)); +mkdirSync(generated, { recursive: true }); +afterAll(() => rmSync(generated, { recursive: true, force: true })); +let sequence = 0; +async function compile(jsx: string, platform = 'ios', options = {}) { + const result = transformSync( + `import {useRef} from 'react'; import {View, Text, Image, ActivityIndicator, Animated, Platform, StyleSheet} from 'react-native'; export default () => (${jsx});`, + { + configFile: false, + babelrc: false, + filename: join(generated, 'input.jsx'), + caller: { name: 'parity', platform } as TransformCaller, + presets: [[require.resolve('@babel/preset-react'), { runtime: 'automatic' }]], + plugins: [ + [ + boost, + { + logLevel: 'silent', + integrations: { unistyles: 'off', uniwind: 'on' }, + target: { reactNative: { packageJson: require.resolve('react-native/package.json') } }, + assumptions: { unknownAncestorsDoNotRenderText: true }, + ...options, + }, + ], + ], + } + )!; + const filename = join(generated, `case-${sequence++}.js`); + writeFileSync(filename, result.code!); + const { default: Component } = await import(/* @vite-ignore */ filename); + return { code: result.code!, Component }; +} + +describe('free Uniwind native parity', () => { + it('preserves Image callback references and keeps nullable callbacks on the wrapper', async () => { + const load = vi.fn(); + const props = { + source: { uri: 'logo.png' }, + className: 'image', + onLoad: load, + onLoadStart: load, + onLoadEnd: load, + onError: load, + }; + for (const platform of ['ios', 'android'] as const) { + setPlatformOS(platform); + const expected = renderAndCaptureAll(React.createElement(original.NativeImage, props))[0].props; + const actual = renderAndCaptureAll(React.createElement(optimized.NativeImage, props))[0].props; + for (const name of ['onLoadStart', 'onLoad', 'onLoadEnd', 'onError', 'shouldNotifyLoadEvents']) { + expect(Object.hasOwn(actual, name)).toBe(Object.hasOwn(expected, name)); + expect(actual[name]).toBe(expected[name]); + } + const result = await compile( + ' {}} />', + platform + ); + expect(result.code).toContain('NativeImage'); + const captured = renderAndCaptureAll(React.createElement(result.Component)); + expect(captured[1].props.onLoad).toBeTypeOf('function'); + const nullable = await compile('', platform); + expect(nullable.code).not.toContain('NativeImage'); + } + setPlatformOS('ios'); + }); + + it.each([ + ['NativeView', { 'className': 'box selected', 'data-selected': true, 'style': { width: 20 }, 'aria-label': 'Box' }], + [ + 'NativeText', + { + className: 'text disabled', + disabled: true, + numberOfLines: 5, + selectionColorClassName: 'accent', + children: 'hello', + }, + ], + ['NativeText', { 'className': 'disabled', 'aria-disabled': true, 'selectionColor': null, 'children': 'hello' }], + [ + 'NativeImage', + { + source: { uri: 'logo.png', width: 20, height: 10 }, + className: 'image', + tintColorClassName: 'accent', + alt: 'Logo', + }, + ], + ['NativeActivityIndicator', { className: 'box', colorClassName: 'accent', color: null, size: 28 }], + ['NativeActivityIndicator', { color: null }], + ] as const)('%s preserves native props', (name, props) => { + for (const platform of ['ios', 'android'] as const) { + setPlatformOS(platform); + expect(capture(optimized[name], props), platform).toEqual(capture(original[name], props)); + } + setPlatformOS('ios'); + }); + + it.each([false, true])('preserves Text state ownership and errors (frozen=%s)', (frozen) => { + const expectedState = { disabled: true }; + const actualState = { disabled: true }; + if (frozen) { + Object.freeze(expectedState); + Object.freeze(actualState); + } + const expected = () => + capture(original.NativeText, { accessibilityState: expectedState, disabled: false, children: 'label' }); + const actual = () => + capture(optimized.NativeText, { accessibilityState: actualState, disabled: false, children: 'label' }); + if (frozen && reactNativeVersion.minor >= 85 && reactNativeVersion.minor < 88) { + expect(expected).toThrow(TypeError); + expect(actual).toThrow(TypeError); + } else { + expect(actual()).toEqual(expected()); + expect(actualState).toStrictEqual(expectedState); + } + }); + + it.each(['NativeView', 'NativeText', 'NativeImage', 'NativeActivityIndicator'] as const)( + '%s preserves scoped themes, variables, and style precedence', + (name) => { + for (const context of [ + { scopedTheme: 'dark', rtl: null, variables: null }, + { scopedTheme: null, rtl: null, variables: { '--color': 'green' } }, + ]) { + const props = { className: context.variables ? 'variable' : 'themed', style: { width: 22 } }; + const render = (Component: React.ElementType) => + capture( + () => React.createElement(UniwindContext, { value: context }, React.createElement(Component, props)), + {} + ); + expect(render(optimized[name])).toEqual(render(original[name])); + expect(render(optimized[name])[0].props.style.backgroundColor).toBe(context.variables ? '#008000' : 'blue'); + } + } + ); + + it.each([false, true])( + 'compiles every native optimizer and retains generated class styles (spread=%s)', + async (spread) => { + const { code, Component } = await compile( + spread + ? 'hello' + : 'hello' + ); + expect(code).toContain('react-native-boost/uniwind'); + for (const name of ['NativeView', 'NativeText', 'NativeImage', 'NativeActivityIndicator']) + expect(code).toContain(name); + const hosts = capture(Component, {}); + expect(hosts.map((host) => host.which)).toEqual([ + 'NativeView', + 'NativeText', + 'NativeImage', + 'NativeView', + 'NativeActivityIndicator', + ]); + expect(hosts[0].props.style.width).toBe(40); + expect(hosts[1].props.style.fontWeight).toBe('700'); + expect(hosts[1].props.numberOfLines).toBe(2); + expect(hosts[2].props.resizeMode).toBe('contain'); + expect(hosts[2].props.style.width).toBe(70); + expect(hosts[3].props.style.padding).toBe(8); + expect(hosts[4].props.color).toBe('#ff0000'); + } + ); + + it('retains wrappers for pressable Text and unsupported Image srcSet', async () => { + const { code } = await compile( + ' {}} className="text">hello' + ); + expect(code).not.toContain('NativeText'); + expect(code).not.toContain('NativeImage'); + }); +}); + +it.each(['NativeView', 'NativeText', 'NativeImage', 'NativeActivityIndicator'] as const)( + '%s updates mounted classes and theme subscriptions', + async (name) => { + const { act, create } = await import('react-test-renderer'); + const { UniwindListener } = await load('core/listener.ts'); + const { Uniwind } = await load('core/config/config.native.ts'); + const { captures } = await import('../capture'); + (globalThis as Record).IS_REACT_ACT_ENVIRONMENT = true; + const warning = vi.spyOn(console, 'error').mockImplementation(() => {}); + try { + for (const Component of [original[name], optimized[name]]) { + Uniwind.setTheme('light'); + let renderer: ReturnType; + await act(() => { + renderer = create(React.createElement(Component, { className: 'themed' })); + }); + expect(normalize({ style: captures.at(-1)!.props.style }).style.backgroundColor).toBe( + name === 'NativeActivityIndicator' ? undefined : 'red' + ); + captures.length = 0; + await act(() => { + Uniwind.setTheme('dark'); + }); + expect(normalize({ style: captures[0].props.style }).style.backgroundColor).toBe('blue'); + await act(() => { + renderer!.update(React.createElement(Component, { 'className': 'selected', 'data-selected': true })); + }); + const selected = captures.slice(name === 'NativeActivityIndicator' ? -2 : -1)[0]; + expect(normalize({ style: selected.props.style }).style.opacity).toBe(0.5); + captures.length = 0; + await act(() => { + renderer!.update(React.createElement(Component, { 'className': 'selected', 'data-selected': false })); + }); + expect(normalize({ style: captures[0].props.style }).style.opacity).toBeUndefined(); + await act(() => { + renderer!.unmount(); + }); + captures.length = 0; + await act(() => { + UniwindListener.notifyAll(); + }); + expect(captures).toEqual([]); + } + } finally { + Uniwind.setTheme('light'); + const messages = warning.mock.calls.map(([message]) => String(message)); + warning.mockRestore(); + expect( + messages.every( + (message) => + message.startsWith('react-test-renderer is deprecated') || + message.startsWith('Detected multiple renderers concurrently rendering the same context provider') + ) + ).toBe(true); + } + } +); + +it('routes animation hooks missing from Uniwind through the native runtime', async () => { + const { code } = await compile( + '' + ); + if (reactNativeVersion.minor >= 85) { + expect(code).toMatch(/import[^;]*useAnimatedValueXY[^;]*from "react-native-boost\/uniwind"/); + expect(code).toMatch(/import[^;]*useAnimatedColor[^;]*from "react-native-boost\/uniwind"/); + expect(code).not.toContain('new Animated.'); + } else { + expect(code).toContain('new Animated.ValueXY'); + expect(code).toContain('new Animated.Color'); + } +}); + +it('retains classes when removing an eligible Animated wrapper', async () => { + const { code, Component } = await compile('', 'ios', { + optimizations: { 'animated-wrapper-removal': 'on' }, + }); + expect(code).toContain('NativeView'); + expect(code).not.toContain('_jsx(Animated.View'); + expect(capture(Component, {})[0].props.style.width).toBe(40); +}); + +it('combines platform and stylesheet transforms with class styles', async () => { + const { code, Component } = await compile( + '' + ); + expect(code).not.toContain('Platform.OS'); + expect(code).not.toContain('StyleSheet.compose('); + expect(capture(Component, {})[0].props).toMatchObject({ testID: 'ios', style: { width: 40, padding: 2, margin: 3 } }); +}); + +it('keeps the original Uniwind components on web', async () => { + const { code } = await compile( + 'hello', + 'web' + ); + expect(code).not.toContain('react-native-boost/'); + expect(code).toContain('className: "text"'); +}); diff --git a/packages/react-native-boost/src/plugin/__tests__/parity/uniwind/vitest.config.mts b/packages/react-native-boost/src/plugin/__tests__/parity/uniwind/vitest.config.mts new file mode 100644 index 00000000..eb167c47 --- /dev/null +++ b/packages/react-native-boost/src/plugin/__tests__/parity/uniwind/vitest.config.mts @@ -0,0 +1,44 @@ +import base from '../vitest.config.parity.mjs'; +import { createRequire } from 'node:module'; +import { dirname, join } from 'node:path'; +import { fileURLToPath, URL } from 'node:url'; +import { transformSync } from '@babel/core'; + +const require = createRequire(import.meta.url); +const root = dirname(require.resolve('uniwind/package.json')); +const local = (filename: string) => fileURLToPath(new URL(filename, import.meta.url)); +import { defineConfig } from 'vitest/config'; +export default defineConfig({ + ...base, + plugins: [ + { + name: 'uniwind-parity', + enforce: 'pre' as const, + resolveId(source: string) { + if (source === 'react-native-boost/uniwind') return local('../../../../runtime/uniwind.ts'); + if (source.startsWith('react-native-boost/uniwind/')) + return join(root, 'src/components/native', source.split('/').at(-1) + '.ts'); + if (source.startsWith('@babel/runtime/')) + return createRequire(require.resolve('react-native/package.json')).resolve(source); + }, + transform(code: string, id: string) { + if (!id.startsWith(root + '/') || !/\.tsx?$/.test(id)) return; + const result = transformSync(code, { + filename: id, + babelrc: false, + configFile: false, + presets: [[require.resolve('@react-native/babel-preset'), { disableImportExportTransform: true }]], + }); + return result?.code ? { code: result.code, map: result.map } : null; + }, + }, + ...base.plugins!, + ], + resolve: { alias: [{ find: /^react-native$/, replacement: local('./react-native.ts') }] }, + test: { + ...base.test, + name: 'uniwind-parity', + include: [local('./uniwind.test.tsx')], + server: { deps: { inline: [/react-native/, /uniwind/] } }, + }, +}); diff --git a/packages/react-native-boost/src/plugin/__tests__/parity/vitest.config.parity.mts b/packages/react-native-boost/src/plugin/__tests__/parity/vitest.config.parity.mts index cf13fd5f..8b0eff0d 100644 --- a/packages/react-native-boost/src/plugin/__tests__/parity/vitest.config.parity.mts +++ b/packages/react-native-boost/src/plugin/__tests__/parity/vitest.config.parity.mts @@ -1,6 +1,6 @@ import { createRequire } from 'node:module'; import { dirname, join } from 'node:path'; -import { fileURLToPath } from 'node:url'; +import { fileURLToPath, URL } from 'node:url'; import { transformSync } from '@babel/core'; import { defineConfig } from 'vitest/config'; @@ -40,15 +40,20 @@ const basenameRedirects: Array<[RegExp, string]> = [ [/(^|[./])ViewNativeComponent$/, u('./mocks/ViewNativeComponent.ts')], [/(^|[./])TextNativeComponent$/, u('./mocks/TextNativeComponent.ts')], [/(^|[./])ImageViewNativeComponent$/, u('./mocks/ImageViewNativeComponent.ts')], + [/(^|[./])ActivityIndicatorViewNativeComponent(?:\.js)?$/, u('./mocks/ActivityIndicatorViewNativeComponent.ts')], + [/(^|[./])ProgressBarAndroidNativeComponent(?:\.js)?$/, u('./mocks/ProgressBarAndroidNativeComponent.ts')], [/(^|[./])TextInlineImageNativeComponent$/, u('./mocks/TextInlineImageNativeComponent.ts')], [/(^|[./])NativeImageLoader(Android|IOS)$/, u('./mocks/NativeImageLoader.ts')], + [/(^|[./])NativeAnimatedHelper$/, u('./mocks/NativeAnimatedHelper.ts')], + [/(^|[./])RendererProxy$/, u('./mocks/RendererProxy.ts')], + [/(^|[./])ReactNativePrivateInterface$/, u('./mocks/ReactNativePrivateInterface.ts')], [/(^|[./])StyleSheet$/, u('./mocks/StyleSheet.ts')], [/(^|[./])resolveAssetSource$/, u('./mocks/resolveAssetSource.ts')], [/(^|[./])Platform$/, u('./mocks/Platform.ts')], + [/(^|[./])PixelRatio$/, u('./mocks/PixelRatio.ts')], [/(^|[./])usePressability$/, u('./mocks/usePressability.ts')], [/(^|[./])PressabilityDebug$/, u('./mocks/PressabilityDebug.ts')], [/(^|[./])ReactNativeFeatureFlags$/, u('./mocks/ReactNativeFeatureFlags.ts')], - [/(^|[./])processColor$/, u('./mocks/processColor.ts')], ]; export default defineConfig({ @@ -67,6 +72,57 @@ export default defineConfig({ }, transform(code, id) { if (!RN_SRC.test(id)) return null; + // Vite cannot intercept these CommonJS requires, so expose them to its resolver as imports. + if (id.endsWith('/StyleSheet/StyleSheetExports.js')) { + code = code + .replace( + /const ReactNativeStyleAttributes =\s*require\('\.\.\/Components\/View\/ReactNativeStyleAttributes'\)\.default;/, + "import ReactNativeStyleAttributes from '../Components/View/ReactNativeStyleAttributes';" + ) + .replace( + /^const PixelRatio = require\('\.\.\/Utilities\/PixelRatio'\)\.default;/m, + "import PixelRatio from '../Utilities/PixelRatio';" + ); + } + if (id.endsWith('/StyleSheet/processColor.js')) { + code = code + .replace( + "const Platform = require('../Utilities/Platform').default;", + "import Platform from '../Utilities/Platform';" + ) + .replace( + "const normalizeColor = require('./normalizeColor').default;", + "import normalizeColor from './normalizeColor';" + ); + } + if (id.endsWith('/StyleSheet/processBackgroundImage.js')) { + code = code.replace( + "const processColor = require('./processColor').default;", + "import processColor from './processColor';" + ); + } + if (id.endsWith('/StyleSheet/processTransform.js')) { + code = code.replace( + "const stringifySafe = require('../Utilities/stringifySafe').default;", + "import stringifySafe from '../Utilities/stringifySafe';" + ); + } + if (id.endsWith('/ActivityIndicator/ActivityIndicator.js')) { + // The real wrapper binds its platform host at module load. The parity suite changes platforms + // in one process, so select at render time while leaving the wrapper's prop logic unchanged. + code = code.replace( + `const PlatformActivityIndicator = + Platform.OS === 'android' + ? require('../ProgressBarAndroid/ProgressBarAndroid').default + : require('./ActivityIndicatorViewNativeComponent').default;`, + `import ProgressBarAndroid from '../ProgressBarAndroid/ProgressBarAndroid.android'; +import ActivityIndicatorView from './ActivityIndicatorViewNativeComponent'; +const PlatformActivityIndicator = props => + Platform.OS === 'android' + ? React.createElement(ProgressBarAndroid, props) + : React.createElement(ActivityIndicatorView, props);` + ); + } const out = transformSync(code, { configFile: false, babelrc: false, @@ -88,7 +144,13 @@ export default defineConfig({ environment: 'node', setupFiles: [u('./setup.ts')], // fibers.collect.ts is a benchmark collector that no-ops unless BENCH_FIBERS_OUT is set. - include: [u('./parity.test.ts'), u('./fibers.collect.ts'), u('./fuzz/fuzz.test.ts')], + include: [ + u('./parity.test.ts'), + u('./text-raw.test.ts'), + u('./image-callbacks.test.ts'), + u('./fibers.collect.ts'), + u('./fuzz/fuzz.test.ts'), + ], server: { deps: { inline: [/react-native/] } }, // force RN source through the transform pipeline }, }); diff --git a/packages/react-native-boost/src/plugin/__tests__/parity/wrapper.ts b/packages/react-native-boost/src/plugin/__tests__/parity/wrapper.ts index 18dcecae..25ff957f 100644 --- a/packages/react-native-boost/src/plugin/__tests__/parity/wrapper.ts +++ b/packages/react-native-boost/src/plugin/__tests__/parity/wrapper.ts @@ -18,10 +18,18 @@ async function compileWrapperCase(os: PlatformOS, jsxBody: string, preamble = '' os === 'ios' ? 'react-native/Libraries/Image/Image.ios' : 'react-native/Libraries/Image/Image.android' }';\n` : ''; + const activityIndicatorImport = jsxBody.includes(' { + const plugins: PluginItem[] = compilerFirst ? [compiler, boostPlugin] : [boostPlugin, compiler]; + + it('opens literal spreads before hoisting but never retries rejected ancestors', () => { + const code = transform( + `import {View, Text, Image, ActivityIndicator} from 'react-native'; + import Parent from './unknown'; + export function Screen() { return + label + {}} /> + + label + ; }`, + plugins + ); + expect(code).toContain('react/compiler-runtime'); + for (const host of ['NativeText', 'NativeImage', 'NativeActivityIndicator']) expect(code).toContain(host); + expect(code).toMatch(/ { + const code = transform( + ` + import {View, Text} from 'react-native'; + export function Screen({value}) { + return {value}; + } + `, + plugins + ); + expect(code).toContain('react/compiler-runtime'); + expect(code).toContain('<_NativeViewWithContext>'); + expect(code).toContain('<_NativeView testID="inner">'); + }); + + it('resolves local forwarding components before lowering', () => { + const code = transform( + ` + import {View, Text} from 'react-native'; + function Box({children}) { return {children}; } + export function Screen({value}) { + return {value}; + } + `, + plugins + ); + expect(code).toContain('react/compiler-runtime'); + expect(code).toContain('<_NativeView testID="inner">'); + }); + + it('does not retry children of an inspecting parent after hoisting', () => { + const code = transform( + ` + import React from 'react'; + import {View} from 'react-native'; + function Inspect({children}) { return React.cloneElement(children, {testID: children.type.name}); } + export function Screen() { return ; } + `, + plugins + ); + expect(code).toContain('react/compiler-runtime'); + expect(code).toContain(' { + const code = transform( + ` + import {View, Text} from 'react-native'; + export function Screen(props) { + return + label + + {/* @boost-ignore */} + ; + } + `, + plugins + ); + expect(code).toContain('<_NativeViewWithContext testID="reset">'); + expect(code).toContain(' { + const macro: PluginObj = { + visitor: { + Program(path) { + path.traverse({ + JSXIdentifier(child) { + if (child.node.name === 'View' && child.parentPath.isJSXOpeningElement()) child.node.name = 'Unknown'; + if (child.node.name === 'View' && child.parentPath.isJSXClosingElement()) child.node.name = 'Unknown'; + }, + }); + }, + }, + }; + const code = transform( + ` + import {View, Text} from 'react-native'; + export function Screen() { return label; } + `, + [macro, ...plugins] + ); + expect(code).not.toContain('react-native-boost/runtime'); + }); + }); +} + +it('preserves an existing visitor wrapper', () => { + const visited = vi.fn(); + transformSync(`import {View} from 'react-native'; export const Screen = () => ;`, { + filename: 'screen.jsx', + configFile: false, + babelrc: false, + plugins: ['@babel/plugin-syntax-jsx', compiler, boostPlugin], + caller: { name: 'test', platform: 'android' } as TransformCaller, + wrapPluginVisitorMethod(key, phase, visitor) { + return function (this: unknown, path, state) { + if (key === 'react-forget' && phase === 'enter' && path.isProgram()) visited(); + return visitor.call(this, path, state); + }; + }, + }); + expect(visited).toHaveBeenCalledOnce(); +}); diff --git a/packages/react-native-boost/src/plugin/__tests__/static-accessibility.test.ts b/packages/react-native-boost/src/plugin/__tests__/static-accessibility.test.ts new file mode 100644 index 00000000..d59cd107 --- /dev/null +++ b/packages/react-native-boost/src/plugin/__tests__/static-accessibility.test.ts @@ -0,0 +1,232 @@ +import { transformSync, transformFromAstSync, traverse, types as t, type TransformCaller } from '@babel/core'; +import { generateTestPlugin } from '../utils/generate-test-plugin'; +import { nativeTextOptimizer } from '../optimizers/native-text'; +import { nativeViewOptimizer } from '../optimizers/native-view'; +import { nativeImageOptimizer } from '../optimizers/native-image'; +import { buildPropertiesFromAttributes } from '../utils/common'; +import compiler from 'babel-plugin-react-compiler'; +import { describe, expect, it } from 'vitest'; +import boost from '../index'; +import type { PluginOptions } from '../types'; + +function emittedProps( + component: 'Text' | 'View' | 'Image', + props: string, + platform: 'ios' | 'android', + minor?: number +) { + const optimizer = { Text: nativeTextOptimizer, View: nativeViewOptimizer, Image: nativeImageOptimizer }[component]; + const output = transformSync( + `import {${component}} from 'react-native'; <${component} ${component === 'Image' ? 'src="logo.png"' : ''} ${props} />`, + { + configFile: false, + babelrc: false, + ast: true, + parserOpts: { plugins: ['jsx'] }, + plugins: [generateTestPlugin(optimizer, {}, platform, minor)], + } + )!; + let bag: t.ObjectExpression | undefined; + traverse(output.ast!, { + JSXOpeningElement(path) { + if (!t.isJSXIdentifier(path.node.name) || !path.node.name.name.includes('Native' + component)) return; + expect(path.node.attributes.some((attribute) => t.isJSXSpreadAttribute(attribute))).toBe(false); + bag = buildPropertiesFromAttributes( + path.node.attributes.filter( + (attribute) => + t.isJSXAttribute(attribute) && + t.isJSXIdentifier(attribute.name) && + /^(accessib|aria-|alt$|role$|disabled$|importantForAccessibility$)/.test(attribute.name.name) + ) + ); + }, + }); + expect(bag).toBeDefined(); + const code = transformFromAstSync(t.file(t.program([t.expressionStatement(bag!)])), '', { + configFile: false, + babelrc: false, + })!.code!; + return new Function(`return ${code}`) as () => Record; +} + +for (const minor of [83, 84, 85, 86, 87, 88]) + for (const platform of ['ios', 'android'] as const) { + it(`emits release-specific nulls and own keys on ${platform} RN ${minor}`, () => { + const text = emittedProps( + 'Text', + 'aria-hidden={null} accessibilityElementsHidden={true} disabled={false} accessibilityState={{disabled:true,extra:"kept"}}', + platform, + minor + ); + const first = text(); + expect(first.accessibilityElementsHidden).toBe(minor < 85 ? true : null); + expect(Object.hasOwn(first, 'accessibilityLabel')).toBe(minor < 85); + expect(Object.hasOwn(first, 'accessibilityRole')).toBe(minor === 84); + expect(Object.hasOwn(first, 'role')).toBe(minor === 84); + const roles = emittedProps( + 'Text', + 'aria-busy={true} accessibilityRole={null} role={undefined}', + platform, + minor + )(); + expect(Object.hasOwn(roles, 'accessibilityRole')).toBe(minor < 85); + expect(roles.accessibilityRole).toBe(minor === 83 ? null : undefined); + expect(Object.hasOwn(first, 'importantForAccessibility')).toBe(minor < 85); + expect(first.accessibilityState).toStrictEqual( + minor >= 88 + ? { busy: undefined, checked: undefined, disabled: false, expanded: undefined, selected: undefined } + : { disabled: false, extra: 'kept' } + ); + expect(first.accessibilityState).not.toBe(text().accessibilityState); + const image = emittedProps('Image', 'alt={null} accessible={false} accessibilityState={null}', platform, minor)(); + expect(image.accessible).toBe((platform === 'ios' && minor < 88) || minor < 85); + expect(Object.hasOwn(image, 'accessibilityLabel')).toBe((platform === 'ios' && minor < 88) || minor < 85); + expect(Object.hasOwn(image, 'accessibilityState')).toBe((platform === 'ios' && minor < 88) || minor < 85); + if (platform === 'ios' && minor < 88) expect(image.accessibilityState).toBeNull(); + const view = emittedProps( + 'View', + 'aria-busy={null} aria-disabled={false} accessibilityState={{busy:true, extra:"removed"}} aria-valuenow={0} accessibilityValue={{text:null}}', + platform, + minor + ); + expect(view().accessibilityState).toStrictEqual({ + busy: true, + checked: undefined, + disabled: false, + expanded: undefined, + selected: undefined, + }); + expect(view().accessibilityValue).toStrictEqual({ max: undefined, min: undefined, now: 0, text: null }); + expect(view().accessibilityState).not.toBe(view().accessibilityState); + }); + } + +function transform( + component: string, + props: string, + platform = 'ios', + compile = false, + preamble = '', + options: PluginOptions = {}, + ignore = false +) { + return transformSync( + `import {Text,View,Image} from 'react-native'; ${preamble} + export default function Case(value) {return ${ignore ? '{/* @boost-ignore */}' : ''}<${component} ${component === 'Image' ? 'src="logo.png"' : ''} ${props} />}`, + { + configFile: false, + babelrc: false, + filename: 'static-accessibility.jsx', + caller: { name: 'test', platform } as TransformCaller, + parserOpts: { plugins: ['jsx'] }, + plugins: [...(compile ? [compiler] : []), [boost, { logLevel: 'silent', ...options }]], + } + )!.code!; +} + +for (const platform of ['ios', 'android']) + for (const compile of [false, true]) { + describe(`static accessibility ${platform} compiler=${compile}`, () => { + for (const component of ['Text', 'View', 'Image']) { + it.each([ + 'aria-busy={true} aria-selected={false}', + 'aria-busy={null} accessibilityState={{busy:false, disabled:true}}', + 'aria-busy={undefined} accessibilityState={null}', + '{...{"aria-busy":true, accessibilityState:{selected:false}}}', + ])(`folds ${component} %s`, (props) => { + const output = transform(component, props, platform, compile); + expect(output).toContain(`Native${component}`); + expect(output).not.toContain(`<${component} `); + expect(output).not.toContain(`process${component}AccessibilityProps`); + }); + it.each([ + 'aria-busy={value.busy} accessibilityState={{disabled:false}}', + 'aria-busy={true} accessibilityState={value.state}', + 'aria-busy={true} accessibilityState={{get disabled(){return false}}}', + 'aria-busy={true} accessibilityState={{["disabled"]:false}}', + 'aria-busy={true} accessibilityState={{disabled:false,disabled:true}}', + 'aria-busy={true} accessibilityState={{__proto__:null,disabled:false}}', + 'aria-busy={true} accessibilityState={{...value.state}}', + 'aria-busy={true} accessibilityState={{disabled:effect()}}', + 'aria-busy={}', + 'aria-busy=', + 'aria-busy={true} aria-busy={false}', + 'aria-busy={true} {...value}', + ])(`does not fold ${component} %s`, (props) => { + const output = transform(component, props, platform, compile); + expect(output.includes(`<${component}`) || output.includes(`process${component}AccessibilityProps`)).toBe( + true + ); + }); + } + it('keeps dynamic groups, shadowed undefined, and integration boundaries', () => { + for (const component of ['Text', 'View', 'Image']) { + const dynamic = transform( + component, + 'aria-busy={busy} accessibilityState={{disabled:false}}', + platform, + compile, + 'const busy=globalThis.busy;' + ); + expect(dynamic).toContain(`process${component}AccessibilityProps`); + const shadowed = transform( + component, + 'aria-busy={undefined}', + platform, + compile, + 'const undefined=globalThis.busy;' + ); + expect(shadowed).toContain(`process${component}AccessibilityProps`); + for (const target of ['web', 'unknown']) { + const fallback = transform(component, 'aria-busy={true}', target, compile); + expect( + fallback.includes(`<${component}`) || fallback.includes(`process${component}AccessibilityProps`) + ).toBe(true); + } + const off = transform(component, 'aria-busy={true}', platform, compile, '', { + optimizations: { [`native-${component.toLowerCase()}`]: 'off' }, + }); + expect(off).toContain(`<${component}`); + const ignored = transform(component, 'aria-busy={true}', platform, compile, '', {}, true); + expect(ignored).toContain(`<${component}`); + const uniwind = transform(component, 'aria-busy={true}', platform, compile, '', { + integrations: { uniwind: 'on' }, + }); + expect(uniwind).toContain('react-native-boost/uniwind'); + expect(uniwind).toContain('aria-busy'); + const unistyles = transform(component, 'aria-busy={true}', platform, compile, '', { + integrations: { unistyles: 'on' }, + }); + expect(unistyles).not.toContain(`process${component}AccessibilityProps`); + } + }); + it('folds Image string-array labels without a helper', () => { + const output = transform( + 'Image', + 'aria-labelledby={null} accessibilityLabelledBy={["first","second"]}', + platform, + compile + ); + expect(output).toContain('NativeImage'); + expect(output).not.toContain('processImageAccessibilityProps'); + }); + it('folds View values and Text disabled reconciliation', () => { + expect( + transform('View', 'aria-valuenow={0} accessibilityValue={{min:0,max:10,text:null}}', platform, compile) + ).not.toContain('processViewAccessibilityProps'); + const text = transform('Text', 'disabled={false} accessibilityState={{disabled:true}}', platform, compile); + expect(text).toContain('NativeText'); + expect(text).not.toContain('processTextAccessibilityProps'); + }); + it('does not inspect escaping const state or move its mutation', () => { + const output = transform( + 'Text', + 'disabled={false} accessibilityState={state}', + platform, + compile, + 'const state={disabled:true}; escape(state);' + ); + expect(output).not.toContain('NativeText'); + }); + }); + } diff --git a/packages/react-native-boost/src/plugin/index.ts b/packages/react-native-boost/src/plugin/index.ts index 8c51c11f..e3562142 100644 --- a/packages/react-native-boost/src/plugin/index.ts +++ b/packages/react-native-boost/src/plugin/index.ts @@ -1,57 +1,270 @@ +import path from 'node:path'; +import { traverse, type BabelFile, type PluginObj } from '@babel/core'; import { declare } from '@babel/helper-plugin-utils'; -import { textOptimizer } from './optimizers/text'; -import { imageOptimizer } from './optimizers/image'; -import { PluginOptions, TargetPlatform } from './types'; +import { nativeTextOptimizer } from './optimizers/native-text'; +import { nativeImageOptimizer } from './optimizers/native-image'; +import type { + HubFile, + OptimizationName, + Optimizer, + OptimizerContext, + OptimizerState, + PluginOptions, + TargetPlatform, +} from './types'; import { createLogger } from './utils/logger'; -import { viewOptimizer } from './optimizers/view'; -import { isIgnoredFile } from './utils/common'; +import { nativeViewOptimizer } from './optimizers/native-view'; +import { analyzeAncestorModule, createFileIgnoreMatcher } from './utils/common'; import { isUnistylesInstalled } from './utils/unistyles'; +import { nativeActivityIndicatorOptimizer } from './optimizers/native-activity-indicator'; +import { animatedValueInitializationOptimizer } from './optimizers/animated-value-initialization'; +import { animatedWrapperRemovalOptimizer } from './optimizers/animated-wrapper-removal'; +import { stylesheetOperationsOptimizer } from './optimizers/stylesheet-operations'; +import { platformFoldingOptimizer } from './optimizers/platform-folding'; +import { validatePluginOptions } from './utils/options'; +import PluginError from './utils/plugin-error'; +import { readModuleImports } from './utils/ancestor-snapshot'; +import { + getReactNativeMinor, + loadReactNativeColorNormalizer, + resolveReactNativeTarget, + type ReactNativeTargetResolution, +} from './utils/react-native-target'; -export type { PluginOptimizationOptions, PluginOptions } from './types'; +export type { + IntegrationState, + LogLevel, + OptimizationSetting, + OptimizationState, + PluginAssumptions, + PluginIntegrationOptions, + PluginOptimizationOptions, + PluginOptions, + ReactNativeTargetOption, +} from './types'; + +const warnings = new Set(); + +const ancestorOptimizers: Optimizer[] = [ + animatedWrapperRemovalOptimizer, + nativeTextOptimizer, + nativeViewOptimizer, + nativeImageOptimizer, + nativeActivityIndicatorOptimizer, +]; +const ancestorVisitor = traverse.visitors.merge(ancestorOptimizers.map((optimizer) => optimizer.visitor)); +const optimizers: Optimizer[] = [ + platformFoldingOptimizer, + animatedValueInitializationOptimizer, + ...ancestorOptimizers, + stylesheetOperationsOptimizer, +]; export default declare((api, rawOptions, dirname?: string) => { api.assertVersion(7); - const options = (rawOptions ?? {}) as PluginOptions; - const logger = createLogger({ - verbose: options.verbose === true, - silent: options.silent === true, - }); + const injectionId = readInternalString(rawOptions, '__reactNativeBoost'); + const snapshotPath = readInternalString(rawOptions, '__reactNativeBoostSnapshot'); + const projectRoot = readInternalString(rawOptions, '__reactNativeBoostProjectRoot'); + const callerData = api.caller((caller) => + JSON.stringify({ + bundler: (caller as { bundler?: unknown } | undefined)?.bundler, + platform: (caller as { platform?: unknown } | undefined)?.platform, + }) + ) as string; + const caller = JSON.parse(callerData) as { bundler?: unknown; platform?: unknown }; + if (!injectionId && caller.bundler === 'metro') { + warnOnce( + createLogger('warn'), + 'React Native Boost 2 must be configured through Metro. Move your options to withBoostConfig in metro.config.js and remove react-native-boost/plugin from babel.config.js.' + ); + return { name: 'react-native-boost', visitor: {} }; + } - // Target platform, resolved at build time. Metro sets this on the Babel caller per platform bundle, - // letting optimizers inline platform-specific defaults instead of deferring them to the runtime. - const platform = api.caller((caller) => - normalizeTargetPlatform((caller as { platform?: string } | undefined)?.platform) - ); + const options = validatePluginOptions(stripInternalOptions(rawOptions ?? {})); + const logger = createLogger(options.logLevel ?? 'info'); + const isIgnoredFile = createFileIgnoreMatcher(options.ignores ?? []); + const platform = normalizeTargetPlatform(caller.platform); + const configuredTarget = options.target?.reactNative; + let targetResolution: ReactNativeTargetResolution | undefined = configuredTarget + ? resolveReactNativeTarget(configuredTarget, getRequireOrigins(dirname)) + : injectionId + ? { versions: [] } + : undefined; + if (targetResolution?.target?.packageJson) { + (api as typeof api & { addExternalDependency?: (file: string) => void }).addExternalDependency?.( + targetResolution.target.packageJson + ); + } - // Resolve "Unistyles mode" once per plugin instance. An explicit `unistyles` flag always wins; - // otherwise auto-detect an installed `react-native-unistyles` and, when found, enable the mode but - // hint to set the flag explicitly β€” a detected package does not prove its Babel plugin is active. - const autoDetectedUnistyles = options.unistyles === undefined && isUnistylesInstalled(dirname); - const unistylesEnabled = options.unistyles === true || autoDetectedUnistyles; + const unistylesMode = options.integrations?.unistyles ?? 'auto'; + const autoDetectedUnistyles = unistylesMode === 'auto' && isUnistylesInstalled(dirname); + const unistylesEnabled = unistylesMode === 'on' || autoDetectedUnistyles; if (autoDetectedUnistyles) { logger.warning({ message: 'react-native-unistyles was detected, so Unistyles mode was enabled automatically. Set ' + - '`unistyles: true` in the react-native-boost plugin options to make this explicit, or ' + - '`unistyles: false` to opt out.', + "`integrations: { unistyles: 'on' }` to make this explicit, or set it to `off` to opt out.", }); } - return { + let colorNormalizerResolved = false; + let normalizeColor: OptimizerContext['normalizeColor']; + + const resolveReactNativeMinor = (file: HubFile): number | undefined => { + if (!targetResolution) { + const fileOptions = file.opts as HubFile['opts'] & { cwd?: string }; + targetResolution = resolveReactNativeTarget(undefined, [ + fileOptions.filename, + fileOptions.cwd ? path.join(fileOptions.cwd, 'package.json') : undefined, + ...getRequireOrigins(dirname), + ]); + const versions = targetResolution.versions.join(', '); + if (targetResolution.target && !injectionId) { + warnOnce( + logger, + `Using best-effort React Native ${targetResolution.target.version} detection${versions.includes(',') ? ` from versions ${versions}` : ''}.` + ); + } + } + + if (!targetResolution.target && !injectionId) { + warnOnce(logger, 'React Native could not be detected. Version-dependent optimizations will be skipped.'); + } + + return getReactNativeMinor(targetResolution.target?.version); + }; + + const plugin: PluginObj = { name: 'react-native-boost', - visitor: { - JSXOpeningElement(path) { - if (isIgnoredFile(path, options.ignores ?? [])) return; - if (options.optimizations?.text !== false) textOptimizer(path, logger, options, platform, unistylesEnabled); - if (options.optimizations?.view !== false) viewOptimizer(path, logger, options, platform, unistylesEnabled); - if (options.optimizations?.image !== false) imageOptimizer(path, logger, options, platform, unistylesEnabled); - }, + pre(file) { + if (injectionId) (file.metadata as Record).reactNativeBoost = { injectionId }; + + const hubFile = file as unknown as HubFile; + const ignored = isIgnoredFile(hubFile); + if (snapshotPath && !ignored) { + const imports = readModuleImports(snapshotPath, projectRoot, hubFile.opts.filename, platform); + hubFile.__ancestorImports = imports?.ancestors; + hubFile.__spreadImports = imports?.spreads; + hubFile.__ancestorReferences = new Map(); + hubFile.__spreadReferences = new Map(); + } + const reactNativeMinor = ignored ? undefined : resolveReactNativeMinor(hubFile); + if (!ignored && !colorNormalizerResolved) { + normalizeColor = loadReactNativeColorNormalizer(targetResolution?.target?.packageJson); + colorNormalizerResolved = true; + } + this.optimizerContext = { logger, options, platform, unistylesEnabled, reactNativeMinor, normalizeColor }; + this.enabledOptimizations = ignored ? new Set() : getEnabledOptimizations(options, this.optimizerContext); + + configureReactCompilerPass(file, this, snapshotPath !== undefined); }, + visitor: traverse.visitors.merge([ + ...optimizers.map((optimizer) => optimizer.visitor), + { + Program: { + enter(path) { + if (!snapshotPath) return; + const file = (path.hub as unknown as { file: HubFile }).file; + file.__ancestorAnalysis ??= analyzeAncestorModule(path); + }, + exit(path, state) { + if (!snapshotPath) return; + const file = (path.hub as unknown as { file: HubFile & { metadata: unknown } }).file; + const metadata = file.metadata as Record; + const boostMetadata = metadata.reactNativeBoost as Record; + boostMetadata.analysis = { + ...file.__ancestorAnalysis, + ...(state.ancestorSources && { + sources: Object.fromEntries( + [...state.ancestorSources].map(([source, statement]) => [source, statement.source!.value]) + ), + }), + references: [...(file.__ancestorReferences?.values() ?? [])], + spreadReferences: [...(file.__spreadReferences?.values() ?? [])], + }; + }, + }, + }, + ]), }; + + return plugin as unknown as PluginObj; }); -function normalizeTargetPlatform(platform?: string): TargetPlatform | undefined { +function configureReactCompilerPass(file: BabelFile, state: OptimizerState, analyzeExports: boolean): void { + const hubFile = file as unknown as HubFile; + const wrapVisitor = file.opts.wrapPluginVisitorMethod; + file.opts.wrapPluginVisitorMethod = (key, phase, visitor) => { + const wrapped = wrapVisitor ? wrapVisitor(key, phase, visitor) : visitor; + if (key !== 'react-forget' || phase !== 'enter') return wrapped; + return function (this: unknown, path, visitorState) { + if (path.isProgram()) { + path.scope.crawl(); + if (analyzeExports) { + hubFile.__ancestorAnalysis = analyzeAncestorModule(path); + state.ancestorSources = new Map(); + for (const statement of path.node.body) { + if ('source' in statement && statement.source) { + state.ancestorSources.set(statement.source.value, statement); + } + } + } + // Decide before compiler hoisting hides parents; never retry failed JSX after lowering. + path.traverse(ancestorVisitor, state); + for (const optimizer of ancestorOptimizers) state.enabledOptimizations.delete(optimizer.name); + } + return wrapped.call(this, path, visitorState); + }; + }; +} + +function getEnabledOptimizations(options: PluginOptions, context: OptimizerContext): Set { + return new Set( + optimizers + .filter((optimizer) => { + const defaultState = + typeof optimizer.defaultState === 'function' + ? optimizer.defaultState(context) + : (optimizer.defaultState ?? 'on'); + const setting = options.optimizations?.[optimizer.name]; + return ((Array.isArray(setting) ? setting[0] : setting) ?? defaultState) === 'on'; + }) + .map((optimizer) => optimizer.name) + ); +} + +function normalizeTargetPlatform(platform: unknown): TargetPlatform | undefined { return platform === 'ios' || platform === 'android' || platform === 'web' ? platform : undefined; } + +function getRequireOrigins(dirname?: string): Array { + return [dirname ? path.join(dirname, 'package.json') : undefined, path.join(process.cwd(), 'package.json')]; +} + +function readInternalString(rawOptions: unknown, key: string): string | undefined { + const value = + typeof rawOptions === 'object' && rawOptions !== null && !Array.isArray(rawOptions) + ? (rawOptions as Record)[key] + : undefined; + if (value === undefined) return; + if (typeof value !== 'string' || value.length === 0) throw new PluginError(`The internal ${key} option is invalid.`); + return value; +} + +function stripInternalOptions(rawOptions: unknown): unknown { + if (typeof rawOptions !== 'object' || rawOptions === null || Array.isArray(rawOptions)) return rawOptions; + const { __reactNativeBoost, __reactNativeBoostProjectRoot, __reactNativeBoostSnapshot, ...options } = + rawOptions as Record; + void __reactNativeBoost; + void __reactNativeBoostProjectRoot; + void __reactNativeBoostSnapshot; + return options; +} + +function warnOnce(logger: ReturnType, message: string): void { + if (warnings.has(message)) return; + warnings.add(message); + logger.warning({ message }); +} diff --git a/packages/react-native-boost/src/plugin/optimizers/animated-value-initialization/__tests__/fixtures/bailouts/code.js b/packages/react-native-boost/src/plugin/optimizers/animated-value-initialization/__tests__/fixtures/bailouts/code.js new file mode 100644 index 00000000..2ed2e3e8 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/animated-value-initialization/__tests__/fixtures/bailouts/code.js @@ -0,0 +1,16 @@ +import { useRef, useState } from 'react'; +import { Animated } from 'react-native'; +import { Animated as OtherAnimated } from 'other'; + +const initialValue = 0; +const current = useRef(new Animated.Value(initialValue)).current; +const ref = useRef(new Animated.Value(0)); +useRef(new Animated.Value(0)).current = current; +const [state] = useState(new Animated.Value(initialValue)); +const configured = useRef(new Animated.Value(0, getConfig())).current; +const other = useRef(new OtherAnimated.Value(0)).current; +const localUseRef = (value) => ({ current: value }); +const local = localUseRef(new Animated.Value(0)).current; + +/* @boost-ignore */ +const ignored = useRef(new Animated.Value(0)).current; diff --git a/packages/react-native-boost/src/plugin/optimizers/animated-value-initialization/__tests__/fixtures/bailouts/output.js b/packages/react-native-boost/src/plugin/optimizers/animated-value-initialization/__tests__/fixtures/bailouts/output.js new file mode 100644 index 00000000..4104c3b3 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/animated-value-initialization/__tests__/fixtures/bailouts/output.js @@ -0,0 +1,17 @@ +import { useRef, useState } from 'react'; +import { Animated } from 'react-native'; +import { Animated as OtherAnimated } from 'other'; +const initialValue = 0; +const current = useRef(new Animated.Value(initialValue)).current; +const ref = useRef(new Animated.Value(0)); +useRef(new Animated.Value(0)).current = current; +const [state] = useState(new Animated.Value(initialValue)); +const configured = useRef(new Animated.Value(0, getConfig())).current; +const other = useRef(new OtherAnimated.Value(0)).current; +const localUseRef = (value) => ({ + current: value, +}); +const local = localUseRef(new Animated.Value(0)).current; + +/* @boost-ignore */ +const ignored = useRef(new Animated.Value(0)).current; diff --git a/packages/react-native-boost/src/plugin/optimizers/animated-value-initialization/__tests__/fixtures/hooks/code.js b/packages/react-native-boost/src/plugin/optimizers/animated-value-initialization/__tests__/fixtures/hooks/code.js new file mode 100644 index 00000000..ce244f39 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/animated-value-initialization/__tests__/fixtures/hooks/code.js @@ -0,0 +1,11 @@ +import React, { useRef as keep, useState } from 'react'; +import * as RN from 'react-native'; +import { Animated as Motion } from 'react-native'; + +const value = keep(new Motion.Value(-1, { useNativeDriver: false })).current; +const namespacedValue = React.useRef(new RN.Animated.Value(2)).current; +const position = keep(new Motion.ValueXY({ x: 0, y: 1 })).current; +const color = React.useRef(new Motion.Color('#fff')).current; +const [stateValue] = useState(new Motion.Value(3)); +const [statePosition] = React.useState(new Motion.ValueXY()); +const [stateColor] = useState(new Motion.Color({ r: 1, g: 0, b: 0, a: 1 })); diff --git a/packages/react-native-boost/src/plugin/optimizers/animated-value-initialization/__tests__/fixtures/hooks/output.js b/packages/react-native-boost/src/plugin/optimizers/animated-value-initialization/__tests__/fixtures/hooks/output.js new file mode 100644 index 00000000..ab31cfab --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/animated-value-initialization/__tests__/fixtures/hooks/output.js @@ -0,0 +1,28 @@ +import { + useAnimatedValue as _useAnimatedValue, + useAnimatedValueXY as _useAnimatedValueXY, + useAnimatedColor as _useAnimatedColor, +} from 'react-native'; +import React, { useRef as keep, useState } from 'react'; +import * as RN from 'react-native'; +import { Animated as Motion } from 'react-native'; +const value = _useAnimatedValue(-1, { + useNativeDriver: false, +}); +const namespacedValue = _useAnimatedValue(2); +const position = _useAnimatedValueXY({ + x: 0, + y: 1, +}); +const color = _useAnimatedColor('#fff'); +const [stateValue] = useState(() => new Motion.Value(3)); +const [statePosition] = React.useState(() => new Motion.ValueXY()); +const [stateColor] = useState( + () => + new Motion.Color({ + r: 1, + g: 0, + b: 0, + a: 1, + }) +); diff --git a/packages/react-native-boost/src/plugin/optimizers/animated-value-initialization/__tests__/index.test.ts b/packages/react-native-boost/src/plugin/optimizers/animated-value-initialization/__tests__/index.test.ts new file mode 100644 index 00000000..5b4c4279 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/animated-value-initialization/__tests__/index.test.ts @@ -0,0 +1,93 @@ +import { createRequire } from 'node:module'; +import path from 'node:path'; +import { transformSync, type TransformCaller } from '@babel/core'; +import { pluginTester } from 'babel-plugin-tester'; +import { describe, expect, it } from 'vitest'; +import boostPlugin from '../../../index'; +import { formatTestResult } from '../../../utils/format-test-result'; +import { generateTestPlugin } from '../../../utils/generate-test-plugin'; +import { animatedValueInitializationOptimizer } from '..'; + +const reactNativePackageJson = createRequire(import.meta.url).resolve('react-native/package.json'); + +pluginTester({ + plugin: generateTestPlugin(animatedValueInitializationOptimizer, {}, 'ios', 87), + title: 'animated value initialization', + fixtures: path.resolve(import.meta.dirname, 'fixtures'), + formatResult: formatTestResult, +}); + +function transformWithOptimizer( + source: string, + platform: 'ios' | 'web', + reactNativeMinor: number, + uniwind: 'on' | 'off' = 'off' +): string { + return transformSync(source, { + configFile: false, + babelrc: false, + plugins: [ + generateTestPlugin( + animatedValueInitializationOptimizer, + { integrations: { uniwind } }, + platform, + reactNativeMinor + ), + ], + })!.code!; +} + +function transformWithBoost(source: string, enabled = true): string { + return transformSync(source, { + configFile: false, + babelrc: false, + caller: { name: 'test', platform: 'ios' } as TransformCaller, + plugins: [ + [ + boostPlugin, + { + logLevel: 'silent', + target: { reactNative: { packageJson: reactNativePackageJson } }, + optimizations: enabled ? undefined : { 'animated-value-initialization': 'off' }, + }, + ], + ], + })!.code!; +} + +const source = ` + import { useRef, useState } from 'react'; + import { Animated } from 'react-native'; + const value = useRef(new Animated.Value(0)).current; + const position = useRef(new Animated.ValueXY({ x: 0, y: 0 })).current; + const color = useRef(new Animated.Color('red')).current; + const [statePosition] = useState(new Animated.ValueXY()); +`; + +describe('animated value initialization integration', () => { + it.each(['on', 'off'] as const)('uses only available hooks with Uniwind %s', (uniwind) => { + const output = transformWithOptimizer(source, 'ios', 84, uniwind); + + expect(output).toContain('_useAnimatedValue(0)'); + expect(output).toContain('useRef(new Animated.ValueXY({'); + expect(output).toContain("useRef(new Animated.Color('red')).current"); + expect(output).not.toContain('useAnimatedValueXY'); + expect(output).not.toContain('useAnimatedColor'); + expect(output).toContain('useState(() => new Animated.ValueXY())'); + }); + + it('does not change web builds', () => { + const output = transformWithOptimizer(source, 'web', 87); + + expect(output).toContain('useRef(new Animated.Value(0)).current'); + expect(output).toContain('useState(new Animated.ValueXY())'); + }); + + it('runs by default and respects its optimization setting', () => { + expect(transformWithBoost(source)).toContain('_useAnimatedValue(0)'); + + const disabledOutput = transformWithBoost(source, false); + expect(disabledOutput).toContain('useRef(new Animated.Value(0)).current'); + expect(disabledOutput).toContain('useState(new Animated.ValueXY())'); + }); +}); diff --git a/packages/react-native-boost/src/plugin/optimizers/animated-value-initialization/index.ts b/packages/react-native-boost/src/plugin/optimizers/animated-value-initialization/index.ts new file mode 100644 index 00000000..f20c5ae7 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/animated-value-initialization/index.ts @@ -0,0 +1,189 @@ +import { NodePath, types as t } from '@babel/core'; +import type { HubFile, Optimizer } from '../../types'; +import PluginError from '../../utils/plugin-error'; +import { addFileImportHint, isIgnoredLine, isModuleImportBinding, isStaticLiteralTree } from '../../utils/common'; + +type AnimatedConstructor = 'Color' | 'Value' | 'ValueXY'; +type ReactHook = 'useRef' | 'useState'; + +const HOOK_BY_CONSTRUCTOR: Record = { + Color: 'useAnimatedColor', + Value: 'useAnimatedValue', + ValueXY: 'useAnimatedValueXY', +}; + +export const animatedValueInitializationOptimizer: Optimizer = { + name: 'animated-value-initialization', + defaultState: ({ reactNativeMinor }) => (reactNativeMinor !== undefined && reactNativeMinor >= 83 ? 'on' : 'off'), + visitor: { + MemberExpression(path, state) { + if (!state.enabledOptimizations.has('animated-value-initialization')) return; + if (path.node.computed || !t.isIdentifier(path.node.property, { name: 'current' }) || !path.isReferenced()) + return; + + const call = path.node.object; + if (!t.isCallExpression(call) || !isReactHook(path, call.callee, 'useRef') || call.arguments.length !== 1) return; + + const constructor = getAnimatedConstructor(path, call.arguments[0]); + if (!constructor) return; + + const { logger, platform, reactNativeMinor } = state.optimizerContext; + const target = `Animated.${constructor.name}`; + if (platform !== 'ios' && platform !== 'android') { + logger.skipped({ target, path, reason: 'target platform is unknown' }); + return; + } + if (!supportsHook(constructor.name, reactNativeMinor)) { + logger.skipped({ target, path, reason: 'React Native version does not export the matching hook' }); + return; + } + if (isIgnoredLine(path)) return; + + const file = (path.hub as unknown as { file?: HubFile }).file; + if (!file) throw new PluginError('No file found in Babel hub'); + + const hookName = HOOK_BY_CONSTRUCTOR[constructor.name]; + const hook = addFileImportHint({ + file, + nameHint: hookName, + path, + importName: hookName, + moduleName: + state.optimizerContext.options?.integrations?.uniwind === 'on' + ? 'react-native-boost/uniwind' + : 'react-native', + }); + const replacement = t.callExpression( + hook, + constructor.expression.arguments.map((argument) => t.cloneNode(argument, true)) + ); + t.inheritsComments(replacement, path.node); + path.replaceWith(replacement); + logger.optimized({ target, path }); + }, + CallExpression(path, state) { + if (!state.enabledOptimizations.has('animated-value-initialization')) return; + if (!isReactHook(path, path.node.callee, 'useState') || path.node.arguments.length !== 1) return; + + const constructor = getAnimatedConstructor(path, path.node.arguments[0]); + if (!constructor || isIgnoredLine(path)) return; + + const { logger, platform } = state.optimizerContext; + const target = `Animated.${constructor.name}`; + if (platform !== 'ios' && platform !== 'android') { + logger.skipped({ target, path, reason: 'target platform is unknown' }); + return; + } + + const initializer = t.arrowFunctionExpression([], t.cloneNode(constructor.expression, true)); + t.inheritsComments(initializer, constructor.expression); + path.node.arguments[0] = initializer; + logger.optimized({ target, path }); + }, + }, +}; + +function getAnimatedConstructor( + path: NodePath, + node: t.Node | null | undefined +): { expression: t.NewExpression; name: AnimatedConstructor } | undefined { + if ( + !t.isNewExpression(node) || + !t.isMemberExpression(node.callee) || + node.callee.computed || + !t.isIdentifier(node.callee.property) || + !isAnimatedReference(path, node.callee.object) + ) { + return; + } + + const name = node.callee.property.name; + if (!isAnimatedConstructor(name) || !hasStaticArguments(node, name)) return; + return { expression: node, name }; +} + +function isAnimatedConstructor(name: string): name is AnimatedConstructor { + return name === 'Value' || name === 'ValueXY' || name === 'Color'; +} + +function hasStaticArguments(expression: t.NewExpression, constructor: AnimatedConstructor): boolean { + const arguments_ = expression.arguments; + if (arguments_.length > 2 || arguments_.some((argument) => !t.isExpression(argument))) return false; + if (arguments_[1] && !isStaticConfig(arguments_[1] as t.Expression)) return false; + + const initialValue = arguments_[0] as t.Expression | undefined; + if (constructor === 'Value') return initialValue !== undefined && isStaticNumber(initialValue); + if (constructor === 'ValueXY') return initialValue === undefined || isStaticValueXY(initialValue); + return initialValue === undefined || isStaticLiteralTree(initialValue); +} + +function isStaticConfig(expression: t.Expression): boolean { + return t.isNullLiteral(expression) || (t.isObjectExpression(expression) && isStaticLiteralTree(expression)); +} + +function isStaticNumber(expression: t.Expression): boolean { + return ( + t.isNumericLiteral(expression) || + (t.isUnaryExpression(expression, { operator: '-' }) && t.isNumericLiteral(expression.argument)) + ); +} + +function isStaticValueXY(expression: t.Expression): boolean { + if (!t.isObjectExpression(expression) || !isStaticLiteralTree(expression)) return false; + + let x: t.Expression | undefined; + let y: t.Expression | undefined; + for (const property of expression.properties) { + if (!t.isObjectProperty(property) || !t.isExpression(property.value)) return false; + const name = t.isIdentifier(property.key) ? property.key.name : (property.key as t.StringLiteral).value; + if (name === 'x') x = property.value; + if (name === 'y') y = property.value; + } + return x !== undefined && y !== undefined && isStaticNumber(x) && isStaticNumber(y); +} + +function supportsHook(constructor: AnimatedConstructor, reactNativeMinor?: number): boolean { + return reactNativeMinor !== undefined && reactNativeMinor >= (constructor === 'Value' ? 83 : 85); +} + +function isReactHook(path: NodePath, expression: t.Node, hook: ReactHook): boolean { + if (t.isIdentifier(expression)) { + const binding = path.scope.getBinding(expression.name); + return isModuleImportBinding(binding, 'react', t.isImportSpecifier, hook); + } + + if ( + !t.isMemberExpression(expression) || + expression.computed || + !t.isIdentifier(expression.object) || + !t.isIdentifier(expression.property, { name: hook }) + ) { + return false; + } + + const binding = path.scope.getBinding(expression.object.name); + return isModuleImportBinding( + binding, + 'react', + (node) => t.isImportDefaultSpecifier(node) || t.isImportNamespaceSpecifier(node) + ); +} + +function isAnimatedReference(path: NodePath, expression: t.Node): boolean { + if (t.isIdentifier(expression)) { + const binding = path.scope.getBinding(expression.name); + return isModuleImportBinding(binding, 'react-native', t.isImportSpecifier, 'Animated'); + } + + if ( + !t.isMemberExpression(expression) || + expression.computed || + !t.isIdentifier(expression.object) || + !t.isIdentifier(expression.property, { name: 'Animated' }) + ) { + return false; + } + + const binding = path.scope.getBinding(expression.object.name); + return isModuleImportBinding(binding, 'react-native', t.isImportNamespaceSpecifier); +} diff --git a/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures-web/unsupported/code.js b/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures-web/unsupported/code.js new file mode 100644 index 00000000..10386108 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures-web/unsupported/code.js @@ -0,0 +1,7 @@ +import { Animated } from 'react-native'; + +; +<> + {/* @boost-force */} + +; diff --git a/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures-web/unsupported/output.js b/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures-web/unsupported/output.js new file mode 100644 index 00000000..019e51a9 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures-web/unsupported/output.js @@ -0,0 +1,6 @@ +import { Animated } from 'react-native'; +; +<> + {/* @boost-force */} + +; diff --git a/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/aliases-and-static-values/code.js b/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/aliases-and-static-values/code.js new file mode 100644 index 00000000..17a249e7 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/aliases-and-static-values/code.js @@ -0,0 +1,7 @@ +import { Animated as Motion } from 'react-native'; + +function onLayout() {} + + + +; diff --git a/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/aliases-and-static-values/output.js b/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/aliases-and-static-values/output.js new file mode 100644 index 00000000..41d25752 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/aliases-and-static-values/output.js @@ -0,0 +1,11 @@ +import { Animated as Motion, View as _AnimatedWrapperRemovalView } from 'react-native'; +function onLayout() {} +<_AnimatedWrapperRemovalView + accessibilityState={{ + disabled: false, + }} + onLayout={onLayout} + collapsable={false} + style={undefined}> + +; diff --git a/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/bailout-animated-event/code.js b/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/bailout-animated-event/code.js new file mode 100644 index 00000000..bace3bda --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/bailout-animated-event/code.js @@ -0,0 +1,3 @@ +import { Animated } from 'react-native'; + +; diff --git a/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/bailout-animated-event/output.js b/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/bailout-animated-event/output.js new file mode 100644 index 00000000..537a22bc --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/bailout-animated-event/output.js @@ -0,0 +1,6 @@ +import { Animated } from 'react-native'; +; diff --git a/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/bailout-dynamic-child/code.js b/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/bailout-dynamic-child/code.js new file mode 100644 index 00000000..39964281 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/bailout-dynamic-child/code.js @@ -0,0 +1,3 @@ +import { Animated } from 'react-native'; + +{value}; diff --git a/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/bailout-dynamic-child/output.js b/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/bailout-dynamic-child/output.js new file mode 100644 index 00000000..2ad73e31 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/bailout-dynamic-child/output.js @@ -0,0 +1,2 @@ +import { Animated } from 'react-native'; +{value}; diff --git a/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/bailout-dynamic-style/code.js b/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/bailout-dynamic-style/code.js new file mode 100644 index 00000000..5bfb45ce --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/bailout-dynamic-style/code.js @@ -0,0 +1,3 @@ +import { Animated } from 'react-native'; + +; diff --git a/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/bailout-dynamic-style/output.js b/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/bailout-dynamic-style/output.js new file mode 100644 index 00000000..d1f56ea7 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/bailout-dynamic-style/output.js @@ -0,0 +1,6 @@ +import { Animated } from 'react-native'; +; diff --git a/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/bailout-passthrough-values/code.js b/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/bailout-passthrough-values/code.js new file mode 100644 index 00000000..8a0ff6b8 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/bailout-passthrough-values/code.js @@ -0,0 +1,3 @@ +import { Animated } from 'react-native'; + +; diff --git a/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/bailout-passthrough-values/output.js b/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/bailout-passthrough-values/output.js new file mode 100644 index 00000000..0bbec317 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/bailout-passthrough-values/output.js @@ -0,0 +1,8 @@ +import { Animated } from 'react-native'; +; diff --git a/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/bailout-ref/code.js b/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/bailout-ref/code.js new file mode 100644 index 00000000..30dafaf2 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/bailout-ref/code.js @@ -0,0 +1,3 @@ +import { Animated } from 'react-native'; + +; diff --git a/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/bailout-ref/output.js b/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/bailout-ref/output.js new file mode 100644 index 00000000..a447bb7f --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/bailout-ref/output.js @@ -0,0 +1,2 @@ +import { Animated } from 'react-native'; +; diff --git a/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/bailout-refresh-control/code.js b/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/bailout-refresh-control/code.js new file mode 100644 index 00000000..accea302 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/bailout-refresh-control/code.js @@ -0,0 +1,3 @@ +import { Animated } from 'react-native'; + +} />; diff --git a/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/bailout-refresh-control/output.js b/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/bailout-refresh-control/output.js new file mode 100644 index 00000000..5ac3fc6f --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/bailout-refresh-control/output.js @@ -0,0 +1,2 @@ +import { Animated } from 'react-native'; +} />; diff --git a/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/bailout-spread/code.js b/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/bailout-spread/code.js new file mode 100644 index 00000000..13780457 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/bailout-spread/code.js @@ -0,0 +1,3 @@ +import { Animated } from 'react-native'; + +; diff --git a/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/bailout-spread/output.js b/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/bailout-spread/output.js new file mode 100644 index 00000000..6026e7d0 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/bailout-spread/output.js @@ -0,0 +1,2 @@ +import { Animated } from 'react-native'; +; diff --git a/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/bindings/code.js b/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/bindings/code.js new file mode 100644 index 00000000..d9a193a4 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/bindings/code.js @@ -0,0 +1,6 @@ +import DeepAnimated from 'react-native/Libraries/Animated/Animated'; + +const Animated = library; + +; +; diff --git a/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/bindings/output.js b/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/bindings/output.js new file mode 100644 index 00000000..a7545a51 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/bindings/output.js @@ -0,0 +1,4 @@ +import DeepAnimated from 'react-native/Libraries/Animated/Animated'; +const Animated = library; +; +; diff --git a/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/defaults/code.js b/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/defaults/code.js new file mode 100644 index 00000000..e98fe481 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/defaults/code.js @@ -0,0 +1,8 @@ +import { Animated } from 'react-native'; + +<> + + x + + +; diff --git a/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/defaults/output.js b/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/defaults/output.js new file mode 100644 index 00000000..cb8437f4 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/defaults/output.js @@ -0,0 +1,14 @@ +import { + Animated, + View as _AnimatedWrapperRemovalView, + Text as _AnimatedWrapperRemovalText, + ScrollView as _AnimatedWrapperRemovalScrollView, +} from 'react-native'; +<> + <_AnimatedWrapperRemovalView collapsable={false} style={undefined} /> + <_AnimatedWrapperRemovalText collapsable={false} style={undefined}> + x + + <_AnimatedWrapperRemovalScrollView scrollEventThrottle={0.0001} collapsable={false} style={undefined} /> + <_AnimatedWrapperRemovalScrollView scrollEventThrottle={16} collapsable={false} style={undefined} /> +; diff --git a/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/forced-animated-event/code.js b/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/forced-animated-event/code.js new file mode 100644 index 00000000..95bdbaba --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/forced-animated-event/code.js @@ -0,0 +1,6 @@ +import { Animated } from 'react-native'; + +<> + {/* @boost-force */} + +; diff --git a/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/forced-animated-event/output.js b/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/forced-animated-event/output.js new file mode 100644 index 00000000..34fc30cf --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/forced-animated-event/output.js @@ -0,0 +1,11 @@ +import { Animated, ScrollView as _AnimatedWrapperRemovalScrollView } from 'react-native'; +<> + {/* @boost-force */} + <_AnimatedWrapperRemovalScrollView + onScroll={Animated.event([], { + useNativeDriver: true, + })} + scrollEventThrottle={0.0001} + collapsable={false} + /> +; diff --git a/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/forced-dynamic-child/code.js b/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/forced-dynamic-child/code.js new file mode 100644 index 00000000..391f0ca4 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/forced-dynamic-child/code.js @@ -0,0 +1,6 @@ +import { Animated } from 'react-native'; + +<> + {/* @boost-force */} + {value} +; diff --git a/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/forced-dynamic-child/output.js b/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/forced-dynamic-child/output.js new file mode 100644 index 00000000..47b671ab --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/forced-dynamic-child/output.js @@ -0,0 +1,5 @@ +import { Animated, Text as _AnimatedWrapperRemovalText } from 'react-native'; +<> + {/* @boost-force */} + <_AnimatedWrapperRemovalText collapsable={false}>{value} +; diff --git a/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/forced-dynamic-style/code.js b/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/forced-dynamic-style/code.js new file mode 100644 index 00000000..8ed2c216 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/forced-dynamic-style/code.js @@ -0,0 +1,6 @@ +import { Animated } from 'react-native'; + +<> + {/* @boost-force */} + +; diff --git a/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/forced-dynamic-style/output.js b/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/forced-dynamic-style/output.js new file mode 100644 index 00000000..b797a0cc --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/forced-dynamic-style/output.js @@ -0,0 +1,10 @@ +import { Animated, View as _AnimatedWrapperRemovalView } from 'react-native'; +<> + {/* @boost-force */} + <_AnimatedWrapperRemovalView + style={{ + opacity: value, + }} + collapsable={false} + /> +; diff --git a/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/forced-passthrough-values/code.js b/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/forced-passthrough-values/code.js new file mode 100644 index 00000000..098174a5 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/forced-passthrough-values/code.js @@ -0,0 +1,6 @@ +import { Animated } from 'react-native'; + +<> + {/* @boost-force */} + +; diff --git a/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/forced-passthrough-values/output.js b/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/forced-passthrough-values/output.js new file mode 100644 index 00000000..0fcffdfe --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/forced-passthrough-values/output.js @@ -0,0 +1,12 @@ +import { Animated, View as _AnimatedWrapperRemovalView } from 'react-native'; +<> + {/* @boost-force */} + <_AnimatedWrapperRemovalView + passthroughAnimatedPropExplicitValues={{ + style: { + opacity: 1, + }, + }} + collapsable={false} + /> +; diff --git a/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/forced-ref/code.js b/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/forced-ref/code.js new file mode 100644 index 00000000..6735477b --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/forced-ref/code.js @@ -0,0 +1,6 @@ +import { Animated } from 'react-native'; + +<> + {/* @boost-force */} + +; diff --git a/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/forced-ref/output.js b/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/forced-ref/output.js new file mode 100644 index 00000000..8fc92c0a --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/forced-ref/output.js @@ -0,0 +1,5 @@ +import { Animated, View as _AnimatedWrapperRemovalView } from 'react-native'; +<> + {/* @boost-force */} + <_AnimatedWrapperRemovalView ref={ref} collapsable={false} /> +; diff --git a/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/forced-refresh-control/code.js b/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/forced-refresh-control/code.js new file mode 100644 index 00000000..21a935f3 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/forced-refresh-control/code.js @@ -0,0 +1,6 @@ +import { Animated } from 'react-native'; + +<> + {/* @boost-force */} + } /> +; diff --git a/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/forced-refresh-control/output.js b/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/forced-refresh-control/output.js new file mode 100644 index 00000000..4f14671d --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/forced-refresh-control/output.js @@ -0,0 +1,9 @@ +import { Animated, ScrollView as _AnimatedWrapperRemovalScrollView } from 'react-native'; +<> + {/* @boost-force */} + <_AnimatedWrapperRemovalScrollView + refreshControl={} + scrollEventThrottle={0.0001} + collapsable={false} + /> +; diff --git a/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/forced-spread/code.js b/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/forced-spread/code.js new file mode 100644 index 00000000..5d34e69e --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/forced-spread/code.js @@ -0,0 +1,6 @@ +import { Animated } from 'react-native'; + +<> + {/* @boost-force */} + +; diff --git a/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/forced-spread/output.js b/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/forced-spread/output.js new file mode 100644 index 00000000..abfde790 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/forced-spread/output.js @@ -0,0 +1,5 @@ +import { Animated, View as _AnimatedWrapperRemovalView } from 'react-native'; +<> + {/* @boost-force */} + <_AnimatedWrapperRemovalView {...props} collapsable={false} /> +; diff --git a/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/ignore/code.js b/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/ignore/code.js new file mode 100644 index 00000000..82638583 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/ignore/code.js @@ -0,0 +1,6 @@ +import { Animated } from 'react-native'; + +<> + {/* @boost-ignore */} + +; diff --git a/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/ignore/output.js b/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/ignore/output.js new file mode 100644 index 00000000..fc70c38a --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/ignore/output.js @@ -0,0 +1,5 @@ +import { Animated } from 'react-native'; +<> + {/* @boost-ignore */} + +; diff --git a/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/view-style/code.js b/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/view-style/code.js new file mode 100644 index 00000000..0557ee62 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/view-style/code.js @@ -0,0 +1,3 @@ +import { Animated } from 'react-native'; + +; diff --git a/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/view-style/output.js b/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/view-style/output.js new file mode 100644 index 00000000..496ff68b --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/fixtures/view-style/output.js @@ -0,0 +1,9 @@ +import { Animated, View as _AnimatedWrapperRemovalView } from 'react-native'; +<_AnimatedWrapperRemovalView + testID="card" + collapsable={false} + style={{ + width: 12, + opacity: 0.5, + }} +/>; diff --git a/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/index.test.ts b/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/index.test.ts new file mode 100644 index 00000000..256bf2ce --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/animated-wrapper-removal/__tests__/index.test.ts @@ -0,0 +1,108 @@ +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { transformSync, type TransformCaller } from '@babel/core'; +import { pluginTester } from 'babel-plugin-tester'; +import { afterAll, describe, expect, it } from 'vitest'; +import boostPlugin from '../../../index'; +import { generateTestPlugin } from '../../../utils/generate-test-plugin'; +import { formatTestResult } from '../../../utils/format-test-result'; +import { animatedWrapperRemovalOptimizer } from '..'; + +const babelOptions = { plugins: ['@babel/plugin-syntax-jsx'] }; + +pluginTester({ + plugin: generateTestPlugin(animatedWrapperRemovalOptimizer, {}, 'ios'), + title: 'animated wrapper removal', + fixtures: path.resolve(import.meta.dirname, 'fixtures'), + babelOptions, + formatResult: formatTestResult, +}); + +pluginTester({ + plugin: generateTestPlugin(animatedWrapperRemovalOptimizer, {}, 'web'), + title: 'animated wrapper removal web', + fixtures: path.resolve(import.meta.dirname, 'fixtures-web'), + babelOptions, + formatResult: formatTestResult, +}); + +const targetDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'react-native-boost-animated-')); +afterAll(() => fs.rmSync(targetDirectory, { recursive: true, force: true })); + +function targetPackageJson(minor: number): string { + const packageJson = path.join(targetDirectory, `${minor}.json`); + fs.writeFileSync(packageJson, JSON.stringify({ name: 'react-native', version: `0.${minor}.0` })); + return packageJson; +} + +function transformWithBoost(source: string, reactNativeMinor = 86, animatedWrapperRemoval?: 'on' | 'off'): string { + return transformSync(source, { + configFile: false, + babelrc: false, + caller: { name: 'test', platform: 'ios' } as TransformCaller, + plugins: [ + '@babel/plugin-syntax-jsx', + [ + boostPlugin, + { + logLevel: 'silent', + target: { reactNative: { packageJson: targetPackageJson(reactNativeMinor) } }, + assumptions: { unknownAncestorsDoNotRenderText: true }, + optimizations: animatedWrapperRemoval ? { 'animated-wrapper-removal': animatedWrapperRemoval } : undefined, + }, + ], + ], + })!.code!; +} + +const source = (element: string) => `import { Animated } from 'react-native';\n${element};`; + +describe('animated wrapper removal integration', () => { + it('feeds lowered components into the native host optimizers', () => { + const output = transformWithBoost(` + import { Animated } from 'react-native'; + function Case() { + return ( + + Ready + + + ); + } + `); + + expect(output).toContain('<_NativeView'); + expect(output).toContain('<_NativeText'); + expect(output).toContain('<_NativeImage'); + expect(output).not.toContain(' { + const output = transformWithBoost(` + import { Animated, Text } from 'react-native'; + function Case() { + return nested; + } + `); + + expect(output).not.toContain('NativeText'); + expect(output).not.toContain('NativeVirtualText'); + expect(output).toContain('<_AnimatedWrapperRemovalText collapsable={false} style={undefined}>'); + expect(output).toContain('nested'); + }); + + it('defaults on only for RN 0.83 through 0.86 and honors overrides', () => { + const input = source(''); + const forcedInput = source('<>{/* @boost-force */}'); + + expect(transformWithBoost(input, 82)).toContain('(['Image', 'ScrollView', 'Text', 'View']); +const REF_PROPS = new Set(['innerViewRef', 'ref', 'scrollViewRef']); + +const optimizeAnimatedWrapperRemoval: JSXOptimizer = (path, { logger, platform }) => { + const component = getAnimatedComponent(path); + if (!component) return; + + const target = `Animated.${component}`; + if (platform !== 'ios' && platform !== 'android') { + logger.skipped({ target, path, reason: 'target platform is unknown' }); + return; + } + + const forced = isForcedLine(path); + const style = buildStaticStyle(path); + const bailoutChecks = [ + { + reason: 'has spread props', + shouldBail: () => path.node.attributes.some((attribute) => t.isJSXSpreadAttribute(attribute)), + }, + { + reason: 'has props that require the Animated wrapper', + shouldBail: () => hasUnsupportedAttribute(path, component), + }, + { + reason: 'has children that may contain an Animated value', + shouldBail: () => !hasStaticChildren(path.parent as t.JSXElement, path), + }, + { reason: 'has a dynamic style', shouldBail: () => style === undefined }, + ]; + + const overriddenReason = forced ? getFirstBailoutReason(bailoutChecks) : undefined; + if (forced) { + if (overriddenReason) logger.forced({ target, path, reason: overriddenReason }); + } else { + const skipReason = getFirstBailoutReason([ + { reason: 'line is marked with @boost-ignore', shouldBail: () => isIgnoredLine(path) }, + ...bailoutChecks, + ]); + if (skipReason) { + logger.skipped({ target, path, reason: skipReason }); + return; + } + } + + const file = (path.hub as unknown as { file?: HubFile }).file; + if (!file) throw new PluginError('No file found in Babel hub'); + + const replacement = addFileImportHint({ + file, + nameHint: `AnimatedWrapperRemoval${component}`, + path, + importName: component, + moduleName: 'react-native', + }); + + const preserveAuthoredStyle = overriddenReason !== undefined; + const attributes = path.node.attributes.filter( + (attribute) => + !t.isJSXAttribute(attribute) || + !t.isJSXIdentifier(attribute.name) || + (attribute.name.name !== 'collapsable' && (preserveAuthoredStyle || attribute.name.name !== 'style')) + ); + + if ( + component === 'ScrollView' && + !attributes.some( + (attribute) => t.isJSXAttribute(attribute) && t.isJSXIdentifier(attribute.name, { name: 'scrollEventThrottle' }) + ) + ) { + attributes.push(makeAttribute('scrollEventThrottle', t.numericLiteral(0.0001))); + } + + // Native Animated cannot target flattened views, so its wrapper always forces this value. + attributes.push(makeAttribute('collapsable', t.booleanLiteral(false))); + if (!preserveAuthoredStyle && style) attributes.push(makeAttribute('style', style)); + path.node.attributes = attributes; + path.node.name = t.jsxIdentifier(replacement.name); + + const parent = path.parent as t.JSXElement; + if (parent.closingElement) parent.closingElement.name = t.jsxIdentifier(replacement.name); + + markReactNativeComponent(path.node, component); + logger.optimized({ target, path }); +}; + +export const animatedWrapperRemovalOptimizer = createJSXOptimizer( + 'animated-wrapper-removal', + optimizeAnimatedWrapperRemoval, + ({ reactNativeMinor }) => + reactNativeMinor !== undefined && reactNativeMinor >= 83 && reactNativeMinor <= 86 ? 'on' : 'off' +); + +function getAnimatedComponent(path: NodePath): AnimatedComponent | undefined { + const name = path.node.name; + if ( + !t.isJSXMemberExpression(name) || + !t.isJSXIdentifier(name.object) || + !t.isJSXIdentifier(name.property) || + !COMPONENTS.has(name.property.name as AnimatedComponent) + ) { + return; + } + + const binding = path.scope.getBinding(name.object.name); + if ( + binding?.kind !== 'module' || + !t.isImportSpecifier(binding.path.node) || + !t.isIdentifier(binding.path.node.imported, { name: 'Animated' }) || + !t.isImportDeclaration(binding.path.parent) || + binding.path.parent.source.value !== 'react-native' + ) { + return; + } + + return name.property.name as AnimatedComponent; +} + +function hasUnsupportedAttribute(path: NodePath, component: AnimatedComponent): boolean { + const seen = new Set(); + + for (const attribute of path.node.attributes) { + if (!t.isJSXAttribute(attribute) || !t.isJSXIdentifier(attribute.name)) return true; + const name = attribute.name.name; + if (seen.has(name)) return true; + seen.add(name); + + if (REF_PROPS.has(name) || name === 'passthroughAnimatedPropExplicitValues') return true; + if (component === 'ScrollView' && name === 'refreshControl') return true; + if (name === 'key' || name === 'style') continue; + if (name === 'collapsable') { + if (!isStaticAttribute(attribute)) return true; + continue; + } + if (!isStaticAttribute(attribute) && !isFunctionAttribute(path, attribute)) return true; + } + + return false; +} + +function isStaticAttribute(attribute: t.JSXAttribute): boolean { + if (!attribute.value || t.isStringLiteral(attribute.value)) return true; + if (!t.isJSXExpressionContainer(attribute.value)) return false; + const expression = attribute.value.expression; + return t.isJSXEmptyExpression(expression) || (t.isExpression(expression) && isStaticLiteralTree(expression)); +} + +function isFunctionAttribute(path: NodePath, attribute: t.JSXAttribute): boolean { + if (!t.isJSXExpressionContainer(attribute.value) || !t.isExpression(attribute.value.expression)) return false; + const expression = attribute.value.expression; + if (t.isArrowFunctionExpression(expression) || t.isFunctionExpression(expression)) return true; + return t.isIdentifier(expression) && isFunctionIdentifier(path, expression.name); +} + +function hasStaticChildren(parent: t.JSXElement, path: NodePath): boolean { + return parent.children.every((child) => { + if (t.isJSXText(child) || t.isJSXElement(child) || t.isJSXFragment(child)) return true; + if (!t.isJSXExpressionContainer(child)) return false; + if (t.isJSXEmptyExpression(child.expression)) return true; + if (t.isJSXElement(child.expression) || t.isJSXFragment(child.expression)) return true; + return ( + t.isExpression(child.expression) && + (isStaticLiteralTree(child.expression) || + t.isArrowFunctionExpression(child.expression) || + t.isFunctionExpression(child.expression) || + (t.isIdentifier(child.expression) && isFunctionIdentifier(path, child.expression.name))) + ); + }); +} + +function isFunctionIdentifier(path: NodePath, name: string): boolean { + const binding = path.scope.getBinding(name); + if (binding?.path.isFunctionDeclaration()) return true; + return ( + binding?.constant === true && + binding.path.isVariableDeclarator() && + (t.isArrowFunctionExpression(binding.path.node.init) || t.isFunctionExpression(binding.path.node.init)) + ); +} + +function buildStaticStyle(path: NodePath): t.Expression | undefined { + const attribute = path.node.attributes.find( + (candidate): candidate is t.JSXAttribute => + t.isJSXAttribute(candidate) && t.isJSXIdentifier(candidate.name, { name: 'style' }) + ); + if (!attribute?.value || t.isStringLiteral(attribute.value)) return t.identifier('undefined'); + if (!t.isJSXExpressionContainer(attribute.value) || t.isJSXEmptyExpression(attribute.value.expression)) { + return t.identifier('undefined'); + } + + const expression = attribute.value.expression; + if (t.isObjectExpression(expression) || t.isArrayExpression(expression)) { + const flattened = tryFlattenStaticStyle(expression, false); + if (!flattened) return undefined; + return t.objectExpression( + [...flattened].map(([name, value]) => + t.objectProperty(t.isValidIdentifier(name, false) ? t.identifier(name) : t.stringLiteral(name), value) + ) + ); + } + if (isStaticLiteralTree(expression)) return t.identifier('undefined'); + if (t.isIdentifier(expression, { name: 'undefined' }) && !path.scope.getBinding('undefined')) { + return t.identifier('undefined'); + } + return undefined; +} diff --git a/packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/accessibility-spread-bails/code.js b/packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/accessibility-spread-bails/code.js deleted file mode 100644 index 99785991..00000000 --- a/packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/accessibility-spread-bails/code.js +++ /dev/null @@ -1,5 +0,0 @@ -import { Image } from 'react-native'; - -const props = { 'aria-label': 'Logo' }; - -; diff --git a/packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/force-dynamic-runtime/code.js b/packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/force-dynamic-runtime/code.js deleted file mode 100644 index 4ed42b35..00000000 --- a/packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/force-dynamic-runtime/code.js +++ /dev/null @@ -1,8 +0,0 @@ -import { Image } from 'react-native'; - -<> - {/* @boost-force */} - - {/* @boost-force */} - -; diff --git a/packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/force-text-ancestor/code.js b/packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/force-text-ancestor/code.js deleted file mode 100644 index 61c0d188..00000000 --- a/packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/force-text-ancestor/code.js +++ /dev/null @@ -1,7 +0,0 @@ -import { Image, Text } from 'react-native'; - - - - {/* @boost-force */} - -; diff --git a/packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/force-text-ancestor/output.js b/packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/force-text-ancestor/output.js deleted file mode 100644 index a65d4cf2..00000000 --- a/packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/force-text-ancestor/output.js +++ /dev/null @@ -1,31 +0,0 @@ -import { NativeImage as _NativeImage } from 'react-native-boost/runtime'; -import { Image, Text } from 'react-native'; - - - {/* @boost-force */} - <_NativeImage - style={[ - { - width: 16, - height: 16, - }, - { - overflow: 'hidden', - }, - ]} - source={[ - { - uri: 'forced.png', - width: 16, - height: 16, - }, - ]} - resizeMode="cover" - /> -; diff --git a/packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/source-array/output.js b/packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/source-array/output.js deleted file mode 100644 index 06638335..00000000 --- a/packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/source-array/output.js +++ /dev/null @@ -1,30 +0,0 @@ -import { NativeImage as _NativeImage } from 'react-native-boost/runtime'; -import { Image } from 'react-native'; -<_NativeImage - width={16} - height={16} - style={[ - {}, - { - overflow: 'hidden', - }, - { - opacity: 0.9, - }, - ]} - source={[ - { - uri: 'logo.png', - width: 16, - height: 16, - scale: 1, - }, - { - uri: 'logo@2x.png', - width: 32, - height: 32, - scale: 2, - }, - ]} - resizeMode="cover" -/>; diff --git a/packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/src-precedence/output.js b/packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/src-precedence/output.js deleted file mode 100644 index f4e6d232..00000000 --- a/packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/src-precedence/output.js +++ /dev/null @@ -1,18 +0,0 @@ -import { NativeImage as _NativeImage } from 'react-native-boost/runtime'; -import { Image } from 'react-native'; -<_NativeImage - style={[ - {}, - { - overflow: 'hidden', - }, - ]} - source={[ - { - uri: 'https://example.com/src.png', - headers: {}, - width: 20, - }, - ]} - resizeMode="cover" -/>; diff --git a/packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/text-ancestor-bails/code.js b/packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/text-ancestor-bails/code.js deleted file mode 100644 index c83c008f..00000000 --- a/packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/text-ancestor-bails/code.js +++ /dev/null @@ -1,5 +0,0 @@ -import { Image, Text } from 'react-native'; - - - -; diff --git a/packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/text-ancestor-bails/output.js b/packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/text-ancestor-bails/output.js deleted file mode 100644 index 43075b4d..00000000 --- a/packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/text-ancestor-bails/output.js +++ /dev/null @@ -1,10 +0,0 @@ -import { Image, Text } from 'react-native'; - - -; diff --git a/packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/unknown-imported-ancestor/code.js b/packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/unknown-imported-ancestor/code.js deleted file mode 100644 index 6a6241f4..00000000 --- a/packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/unknown-imported-ancestor/code.js +++ /dev/null @@ -1,9 +0,0 @@ -import { Image } from 'react-native'; -import { ExternalWrapper } from './ExternalWrapper'; - -<> - - - - -; diff --git a/packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/unknown-imported-ancestor/dangerous-output.js b/packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/unknown-imported-ancestor/dangerous-output.js deleted file mode 100644 index e90e7799..00000000 --- a/packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/unknown-imported-ancestor/dangerous-output.js +++ /dev/null @@ -1,45 +0,0 @@ -import { NativeImage as _NativeImage } from 'react-native-boost/runtime'; -import { Image } from 'react-native'; -import { ExternalWrapper } from './ExternalWrapper'; -<> - <_NativeImage - style={[ - { - width: 16, - height: 16, - }, - { - overflow: 'hidden', - }, - ]} - source={[ - { - uri: 'safe.png', - width: 16, - height: 16, - }, - ]} - resizeMode="cover" - /> - - <_NativeImage - style={[ - { - width: 16, - height: 16, - }, - { - overflow: 'hidden', - }, - ]} - source={[ - { - uri: 'unknown.png', - width: 16, - height: 16, - }, - ]} - resizeMode="cover" - /> - -; diff --git a/packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/unknown-imported-ancestor/output.js b/packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/unknown-imported-ancestor/output.js deleted file mode 100644 index fe727fba..00000000 --- a/packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/unknown-imported-ancestor/output.js +++ /dev/null @@ -1,33 +0,0 @@ -import { NativeImage as _NativeImage } from 'react-native-boost/runtime'; -import { Image } from 'react-native'; -import { ExternalWrapper } from './ExternalWrapper'; -<> - <_NativeImage - style={[ - { - width: 16, - height: 16, - }, - { - overflow: 'hidden', - }, - ]} - source={[ - { - uri: 'safe.png', - width: 16, - height: 16, - }, - ]} - resizeMode="cover" - /> - - - -; diff --git a/packages/react-native-boost/src/plugin/optimizers/image/__tests__/index.test.ts b/packages/react-native-boost/src/plugin/optimizers/image/__tests__/index.test.ts deleted file mode 100644 index 4a3192eb..00000000 --- a/packages/react-native-boost/src/plugin/optimizers/image/__tests__/index.test.ts +++ /dev/null @@ -1,503 +0,0 @@ -import path from 'node:path'; -import { parseSync, transformSync, traverse, types as t, type PluginObj, type TransformCaller } from '@babel/core'; -import { pluginTester } from 'babel-plugin-tester'; -import { describe, expect, it } from 'vitest'; -import { generateTestPlugin } from '../../../utils/generate-test-plugin'; -import { formatTestResult } from '../../../utils/format-test-result'; -import { createLogger } from '../../../utils/logger'; -import type { TargetPlatform } from '../../../types'; -import boostPlugin from '../../../index'; -import { imageOptimizer } from '..'; - -const transformImage = async ( - source: string, - platform: TargetPlatform, - { - dangerouslyOptimizeImageWithUnknownAncestors = false, - unistylesEnabled = false, - }: { - dangerouslyOptimizeImageWithUnknownAncestors?: boolean; - unistylesEnabled?: boolean; - } = {} -): Promise => { - const logger = createLogger({ silent: true, verbose: false }); - const plugin = (): PluginObj => ({ - name: `${platform}-image-optimizer-test`, - visitor: { - JSXOpeningElement(path) { - imageOptimizer(path, logger, { dangerouslyOptimizeImageWithUnknownAncestors }, platform, unistylesEnabled); - }, - }, - }); - - return formatTestResult( - transformSync(source, { - configFile: false, - babelrc: false, - plugins: ['@babel/plugin-syntax-jsx', plugin], - })!.code! - ); -}; - -const getNativeImageAttributes = (source: string): t.JSXAttribute[][] => { - const ast = parseSync(source, { - configFile: false, - babelrc: false, - parserOpts: { sourceType: 'module', plugins: ['jsx'] }, - }); - const images: t.JSXAttribute[][] = []; - - traverse(ast!, { - JSXOpeningElement(path) { - if (!t.isJSXIdentifier(path.node.name, { name: '_NativeImage' })) return; - images.push(path.node.attributes.filter((attribute): attribute is t.JSXAttribute => t.isJSXAttribute(attribute))); - }, - }); - - return images; -}; - -const getAttributeExpression = (attributes: t.JSXAttribute[], name: string): t.Expression | undefined => { - const attribute = attributes.find((item) => t.isJSXIdentifier(item.name, { name })); - if (!attribute?.value) return undefined; - if (t.isStringLiteral(attribute.value)) return attribute.value; - if (t.isJSXExpressionContainer(attribute.value) && t.isExpression(attribute.value.expression)) { - return attribute.value.expression; - } - return undefined; -}; - -const getAttributeNames = (attributes: t.JSXAttribute[]): Set => - new Set( - attributes.map((attribute) => - t.isJSXIdentifier(attribute.name) - ? attribute.name.name - : `${attribute.name.namespace.name}:${attribute.name.name.name}` - ) - ); - -const getObjectExpressionProperty = (object: t.ObjectExpression, name: string): t.Expression | undefined => { - const property = object.properties.find( - (item): item is t.ObjectProperty => - t.isObjectProperty(item) && - ((t.isIdentifier(item.key) && item.key.name === name) || - (t.isStringLiteral(item.key) && item.key.value === name)) && - t.isExpression(item.value) - ); - - return property?.value as t.Expression | undefined; -}; - -const getStringPropertyValue = (object: t.ObjectExpression, name: string): string | undefined => { - const value = getObjectExpressionProperty(object, name); - return t.isStringLiteral(value) ? value.value : undefined; -}; - -/** - * Asserts an expression is a call to the named runtime gate (the emitted identifier is uid-prefixed) - * and returns its single argument β€” the value the gate resolves at runtime. - */ -const unwrapRuntimeGate = (expression: t.Expression | undefined, helperName: string): t.Expression | undefined => { - expect(t.isCallExpression(expression)).toBe(true); - const call = expression as t.CallExpression; - expect(t.isIdentifier(call.callee)).toBe(true); - expect((call.callee as t.Identifier).name).toContain(helperName); - expect(call.arguments).toHaveLength(1); - return call.arguments[0] as t.Expression; -}; - -pluginTester({ - plugin: generateTestPlugin(imageOptimizer, {}, 'ios'), - title: 'image', - fixtures: path.resolve(import.meta.dirname, 'fixtures'), - babelOptions: { - plugins: ['@babel/plugin-syntax-jsx'], - }, - formatResult: formatTestResult, -}); - -pluginTester({ - plugin: generateTestPlugin(imageOptimizer, { dangerouslyOptimizeImageWithUnknownAncestors: true }, 'ios'), - title: 'image dangerous unknown ancestors', - babelOptions: { - plugins: ['@babel/plugin-syntax-jsx'], - }, - formatResult: formatTestResult, - tests: [ - { - title: 'optimizes Image inside unresolved ancestor when enabled', - fixture: path.resolve(import.meta.dirname, 'fixtures/unknown-imported-ancestor/code.js'), - outputFixture: path.resolve(import.meta.dirname, 'fixtures/unknown-imported-ancestor/dangerous-output.js'), - }, - ], -}); - -describe('image android output', () => { - it('emits Android top-level empty headers for src sources', async () => { - const output = await transformImage( - ` - import { Image } from 'react-native'; - ; - `, - 'android' - ); - - const images = getNativeImageAttributes(output); - expect(images).toHaveLength(1); - const headers = getAttributeExpression(images[0]!, 'headers'); - - expect(t.isObjectExpression(headers)).toBe(true); - expect((headers as t.ObjectExpression).properties).toHaveLength(0); - }); - - it('emits Android source without the legacy src duplicate, plus top-level headers for request header props', async () => { - const output = await transformImage( - ` - import { Image } from 'react-native'; - ; - `, - 'android' - ); - - const images = getNativeImageAttributes(output); - expect(images).toHaveLength(1); - const image = images[0]!; - const source = getAttributeExpression(image, 'source'); - const headers = getAttributeExpression(image, 'headers'); - - expect(getAttributeNames(image).has('src')).toBe(false); - expect(t.isArrayExpression(source)).toBe(true); - expect(t.isObjectExpression(headers)).toBe(true); - expect(getStringPropertyValue(headers as t.ObjectExpression, 'Access-Control-Allow-Credentials')).toBe('true'); - expect(getStringPropertyValue(headers as t.ObjectExpression, 'Referrer-Policy')).toBe('origin'); - }); - - it('routes object-source headers through the RN version gate and lifts array-source headers directly', async () => { - const output = await transformImage( - ` - import { Image } from 'react-native'; - ; - ; - `, - 'android' - ); - - const images = getNativeImageAttributes(output); - expect(images).toHaveLength(2); - const objectSourceImage = images[0]!; - const arraySourceImage = images[1]!; - const arrayHeaders = getAttributeExpression(arraySourceImage, 'headers'); - - // Only some RN versions lift a plain OBJECT source's inline headers onto the top-level prop, so - // the value is resolved by the runtime gate instead of being baked in either way. - const gatedHeaders = unwrapRuntimeGate( - getAttributeExpression(objectSourceImage, 'headers'), - 'processImageObjectSourceHeaders' - ); - expect(t.isObjectExpression(gatedHeaders)).toBe(true); - expect(getStringPropertyValue(gatedHeaders as t.ObjectExpression, 'Authorization')).toBe('Bearer object'); - - // Every supported version lifts an ARRAY source's `source[0].headers`, so that stays static. - expect(t.isObjectExpression(arrayHeaders)).toBe(true); - expect(getStringPropertyValue(arrayHeaders as t.ObjectExpression, 'Authorization')).toBe('Bearer first'); - - const objectSource = getAttributeExpression(objectSourceImage, 'source'); - const arraySource = getAttributeExpression(arraySourceImage, 'source'); - expect(t.isArrayExpression(objectSource)).toBe(true); - expect(t.isArrayExpression(arraySource)).toBe(true); - - const objectSourceEntry = (objectSource as t.ArrayExpression).elements[0]; - const arraySourceEntry = (arraySource as t.ArrayExpression).elements[0]; - expect(t.isObjectExpression(objectSourceEntry)).toBe(true); - expect(t.isObjectExpression(arraySourceEntry)).toBe(true); - - const objectSourceHeaders = getObjectExpressionProperty(objectSourceEntry as t.ObjectExpression, 'headers'); - const arraySourceHeaders = getObjectExpressionProperty(arraySourceEntry as t.ObjectExpression, 'headers'); - expect(t.isObjectExpression(objectSourceHeaders)).toBe(true); - expect(t.isObjectExpression(arraySourceHeaders)).toBe(true); - expect(getStringPropertyValue(objectSourceHeaders as t.ObjectExpression, 'Authorization')).toBe('Bearer object'); - expect(getStringPropertyValue(arraySourceHeaders as t.ObjectExpression, 'Authorization')).toBe('Bearer first'); - }); - - it('emits single-entry array source dimensions through the RN version gate on Android only', async () => { - const source = ` - import { Image } from 'react-native'; - ; - ; - ; - `; - - const getStyleDimensionEntries = (image: t.JSXAttribute[], gated: boolean): t.ObjectExpression => { - const style = getAttributeExpression(image, 'style'); - expect(t.isArrayExpression(style)).toBe(true); - const first = (style as t.ArrayExpression).elements[0] as t.Expression; - const dimensions = gated ? unwrapRuntimeGate(first, 'processImageArraySourceDimensions') : first; - expect(t.isObjectExpression(dimensions)).toBe(true); - return dimensions as t.ObjectExpression; - }; - - const androidImages = getNativeImageAttributes(await transformImage(source, 'android')); - expect(androidImages).toHaveLength(3); - for (const image of androidImages.slice(0, 2)) { - const dimensions = getStyleDimensionEntries(image, true); - expect(getObjectExpressionProperty(dimensions, 'width')).toMatchObject({ value: 16 }); - expect(getObjectExpressionProperty(dimensions, 'height')).toMatchObject({ value: 8 }); - } - // Multi-entry array: no dimension propagation on any version, mirroring the wrapper's - // `source_.length === 1` gate β€” so there is nothing to resolve at runtime either. - expect(getStyleDimensionEntries(androidImages[2]!, false).properties).toHaveLength(0); - - // iOS never propagates array-source dimensions into style, on any version. - const iosImages = getNativeImageAttributes(await transformImage(source, 'ios')); - expect(iosImages).toHaveLength(3); - for (const image of iosImages) { - expect(getStyleDimensionEntries(image, false).properties).toHaveLength(0); - } - }); - - it('defers a src source with dynamic dimensions to the runtime helper on Android', async () => { - const source = ` - import { Image } from 'react-native'; - ; - `; - - const androidOutput = await transformImage(source, 'android'); - expect(androidOutput).toContain('processImageSourceProps'); - - // iOS emits the dimensions only once (in the source entry), so it can stay static. - const iosOutput = await transformImage(source, 'ios'); - expect(iosOutput).not.toContain('processImageSourceProps'); - expect(iosOutput).toContain('<_NativeImage'); - }); - - it('does not hoist style tintColor on Android', async () => { - const output = await transformImage( - ` - import { Image } from 'react-native'; - ; - `, - 'android' - ); - - const images = getNativeImageAttributes(output); - expect(images).toHaveLength(1); - expect(getAttributeNames(images[0]!).has('tintColor')).toBe(false); - }); -}); - -describe('image unistyles', () => { - it('bails on a Unistyles style because there is no lean Image host', async () => { - const output = await transformImage( - ` - import { Image } from 'react-native'; - import { StyleSheet } from 'react-native-unistyles'; - const styles = StyleSheet.create({ image: { width: 16 } }); - ; - `, - 'ios', - { unistylesEnabled: true } - ); - - expect(output).not.toContain('NativeImage'); - expect(output).toContain(' { - const output = await transformImage( - ` - import { Image } from 'react-native'; - import { StyleSheet } from 'react-native-unistyles'; - const styles = StyleSheet.create({ image: { width: 16 } }); - <> - {/* @boost-force */} - - ; - `, - 'ios', - { unistylesEnabled: true } - ); - - expect(output).not.toContain('NativeImage'); - expect(output).toContain(' { - const output = await transformImage( - ` - import { Image } from 'react-native'; - ; - `, - 'ios', - { unistylesEnabled: true } - ); - - expect(output).not.toContain('NativeImage'); - expect(output).toContain(' { - const output = await transformImage( - ` - import { Image } from 'react-native'; - <> - {/* @boost-force */} - - ; - `, - 'ios', - { unistylesEnabled: true } - ); - - expect(output).toContain('NativeImage'); - }); - - it('optimizes an Image without a style in Unistyles mode', async () => { - const output = await transformImage( - ` - import { Image } from 'react-native'; - ; - `, - 'ios', - { unistylesEnabled: true } - ); - - expect(output).toContain('NativeImage'); - }); - - it('optimizes an Image with a plain literal style in Unistyles mode', async () => { - const output = await transformImage( - ` - import { Image } from 'react-native'; - ; - `, - 'ios', - { unistylesEnabled: true } - ); - - expect(output).toContain('NativeImage'); - }); - - it('optimizes an Image with a React Native StyleSheet style in Unistyles mode', async () => { - const output = await transformImage( - ` - import { Image, StyleSheet } from 'react-native'; - const styles = StyleSheet.create({ image: { width: 16 } }); - ; - `, - 'ios', - { unistylesEnabled: true } - ); - - expect(output).toContain('NativeImage'); - }); -}); - -describe('image unknown platform output', () => { - it('bails because the native Image host prop contract is platform-specific', async () => { - const logger = createLogger({ silent: true, verbose: false }); - const plugin = (): PluginObj => ({ - name: 'unknown-platform-image-optimizer-test', - visitor: { - JSXOpeningElement(path) { - imageOptimizer(path, logger, {}, undefined); - }, - }, - }); - - const output = await formatTestResult( - transformSync( - ` - import { Image } from 'react-native'; - ; - `, - { - configFile: false, - babelrc: false, - plugins: ['@babel/plugin-syntax-jsx', plugin], - } - )!.code! - ); - - expect(output).not.toContain('NativeImage'); - expect(output).toContain(' { - const source = (value: string) => ` - import { Image } from 'react-native'; - ; - `; - - it.each(['maybeAccessible', 'null'])( - 'routes accessible={%s} through the runtime helper on Android', - async (value) => { - const output = await transformImage(source(value), 'android'); - - expect(output).toContain('processImageAccessibilityProps'); - expect(getAttributeNames(getNativeImageAttributes(output)[0]).has('accessible')).toBe(false); - } - ); - - it('keeps a provably non-nullish accessible inline on Android', async () => { - const output = await transformImage(source('false'), 'android'); - - expect(output).not.toContain('processImageAccessibilityProps'); - expect(getAttributeNames(getNativeImageAttributes(output)[0]).has('accessible')).toBe(true); - }); - - it('passes accessible straight through on iOS, which forwards it unchanged', async () => { - const output = await transformImage(source('maybeAccessible'), 'ios'); - - expect(output).not.toContain('processImageAccessibilityProps'); - expect(getAttributeNames(getNativeImageAttributes(output)[0]).has('accessible')).toBe(true); - }); -}); - -describe('image consumed props', () => { - it.each(['ios', 'android'] as const)( - 'removes wrapper-consumed size and request props from optimized %s output', - async (platform) => { - const output = await transformImage( - ` - import { Image } from 'react-native'; - ; - ; - `, - platform - ); - - const images = getNativeImageAttributes(output); - expect(images).toHaveLength(2); - - for (const image of images) { - const names = getAttributeNames(image); - expect(names.has('width')).toBe(false); - expect(names.has('height')).toBe(false); - expect(names.has('crossOrigin')).toBe(false); - expect(names.has('referrerPolicy')).toBe(false); - } - } - ); -}); diff --git a/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures-android/custom-size-and-style/code.js b/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures-android/custom-size-and-style/code.js new file mode 100644 index 00000000..6f685d30 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures-android/custom-size-and-style/code.js @@ -0,0 +1,3 @@ +import { ActivityIndicator } from 'react-native'; + +; diff --git a/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures-android/custom-size-and-style/output.js b/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures-android/custom-size-and-style/output.js new file mode 100644 index 00000000..fb0e0fb0 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures-android/custom-size-and-style/output.js @@ -0,0 +1,25 @@ +import { + NativeView as _NativeView, + activityIndicatorStyles as _activityIndicatorStyles, + NativeActivityIndicator as _NativeActivityIndicator, +} from 'react-native-boost/runtime'; +import { ActivityIndicator } from 'react-native'; +<_NativeView + style={[ + _activityIndicatorStyles.container, + { + margin: 4, + }, + ]}> + <_NativeActivityIndicator + animating={true} + color={null} + hidesWhenStopped={true} + style={{ + height: 24, + width: 24, + }} + styleAttr="Normal" + indeterminate={true} + /> +; diff --git a/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures-android/defaults/code.js b/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures-android/defaults/code.js new file mode 100644 index 00000000..a7df315f --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures-android/defaults/code.js @@ -0,0 +1,3 @@ +import { ActivityIndicator } from 'react-native'; + +; diff --git a/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures-android/defaults/output.js b/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures-android/defaults/output.js new file mode 100644 index 00000000..6de74aa0 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures-android/defaults/output.js @@ -0,0 +1,17 @@ +import { + NativeView as _NativeView, + activityIndicatorStyles as _activityIndicatorStyles, + NativeActivityIndicator as _NativeActivityIndicator, +} from 'react-native-boost/runtime'; +import { ActivityIndicator } from 'react-native'; +<_NativeView style={_activityIndicatorStyles.container}> + <_NativeActivityIndicator + animating={true} + color={null} + hidesWhenStopped={true} + style={_activityIndicatorStyles.small} + size="small" + styleAttr="Normal" + indeterminate={true} + /> +; diff --git a/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures-unistyles/bailout/code.js b/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures-unistyles/bailout/code.js new file mode 100644 index 00000000..2941a8ad --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures-unistyles/bailout/code.js @@ -0,0 +1,6 @@ +import { ActivityIndicator } from 'react-native'; +import { StyleSheet } from 'react-native-unistyles'; + +const styles = StyleSheet.create({ spinner: { margin: 4 } }); + +; diff --git a/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures-unistyles/bailout/output.js b/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures-unistyles/bailout/output.js new file mode 100644 index 00000000..51b0c784 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures-unistyles/bailout/output.js @@ -0,0 +1,8 @@ +import { ActivityIndicator } from 'react-native'; +import { StyleSheet } from 'react-native-unistyles'; +const styles = StyleSheet.create({ + spinner: { + margin: 4, + }, +}); +; diff --git a/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures-unistyles/forced/code.js b/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures-unistyles/forced/code.js new file mode 100644 index 00000000..6cb96742 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures-unistyles/forced/code.js @@ -0,0 +1,9 @@ +import { ActivityIndicator } from 'react-native'; +import { StyleSheet } from 'react-native-unistyles'; + +const styles = StyleSheet.create({ spinner: { margin: 4 } }); + +<> + {/* @boost-force */} + +; diff --git a/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures-unistyles/forced/output.js b/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures-unistyles/forced/output.js new file mode 100644 index 00000000..c242abd3 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures-unistyles/forced/output.js @@ -0,0 +1,25 @@ +import { + NativeView as _NativeView, + activityIndicatorStyles as _activityIndicatorStyles, + NativeActivityIndicator as _NativeActivityIndicator, + processActivityIndicatorStyle as _processActivityIndicatorStyle, +} from 'react-native-boost/runtime'; +import { ActivityIndicator } from 'react-native'; +import { StyleSheet } from 'react-native-unistyles'; +const styles = StyleSheet.create({ + spinner: { + margin: 4, + }, +}); +<> + {/* @boost-force */} + <_NativeView style={_processActivityIndicatorStyle(styles.spinner)}> + <_NativeActivityIndicator + animating={true} + color="#999999" + hidesWhenStopped={true} + style={_activityIndicatorStyles.small} + size="small" + /> + +; diff --git a/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures-unknown-ancestor/basic/code.js b/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures-unknown-ancestor/basic/code.js new file mode 100644 index 00000000..2ae77901 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures-unknown-ancestor/basic/code.js @@ -0,0 +1,6 @@ +import { ActivityIndicator } from 'react-native'; +import { Wrapper } from './wrapper'; + + + +; diff --git a/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures-unknown-ancestor/basic/output.js b/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures-unknown-ancestor/basic/output.js new file mode 100644 index 00000000..f9779042 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures-unknown-ancestor/basic/output.js @@ -0,0 +1,18 @@ +import { + NativeView as _NativeView, + activityIndicatorStyles as _activityIndicatorStyles, + NativeActivityIndicator as _NativeActivityIndicator, +} from 'react-native-boost/runtime'; +import { ActivityIndicator } from 'react-native'; +import { Wrapper } from './wrapper'; + + <_NativeView style={_activityIndicatorStyles.container}> + <_NativeActivityIndicator + animating={true} + color="#999999" + hidesWhenStopped={true} + style={_activityIndicatorStyles.small} + size="small" + /> + +; diff --git a/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures-unknown-platform/basic/code.js b/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures-unknown-platform/basic/code.js new file mode 100644 index 00000000..7b608779 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures-unknown-platform/basic/code.js @@ -0,0 +1,7 @@ +import { ActivityIndicator } from 'react-native'; + +; +<> + {/* @boost-force */} + +; diff --git a/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures-unknown-platform/basic/output.js b/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures-unknown-platform/basic/output.js new file mode 100644 index 00000000..d0029912 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures-unknown-platform/basic/output.js @@ -0,0 +1,6 @@ +import { ActivityIndicator } from 'react-native'; +; +<> + {/* @boost-force */} + +; diff --git a/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures-web/basic/code.js b/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures-web/basic/code.js new file mode 100644 index 00000000..7b608779 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures-web/basic/code.js @@ -0,0 +1,7 @@ +import { ActivityIndicator } from 'react-native'; + +; +<> + {/* @boost-force */} + +; diff --git a/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures-web/basic/output.js b/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures-web/basic/output.js new file mode 100644 index 00000000..d0029912 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures-web/basic/output.js @@ -0,0 +1,6 @@ +import { ActivityIndicator } from 'react-native'; +; +<> + {/* @boost-force */} + +; diff --git a/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures/bailout-children/code.js b/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures/bailout-children/code.js new file mode 100644 index 00000000..3a718b17 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures/bailout-children/code.js @@ -0,0 +1,5 @@ +import { ActivityIndicator, View } from 'react-native'; + + + +; diff --git a/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures/bailout-children/output.js b/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures/bailout-children/output.js new file mode 100644 index 00000000..033bfeb3 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures/bailout-children/output.js @@ -0,0 +1,4 @@ +import { ActivityIndicator, View } from 'react-native'; + + +; diff --git a/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures/bailout-impure-prop/code.js b/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures/bailout-impure-prop/code.js new file mode 100644 index 00000000..ef79f130 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures/bailout-impure-prop/code.js @@ -0,0 +1,3 @@ +import { ActivityIndicator } from 'react-native'; + +; diff --git a/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures/bailout-impure-prop/output.js b/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures/bailout-impure-prop/output.js new file mode 100644 index 00000000..b6602a98 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures/bailout-impure-prop/output.js @@ -0,0 +1,2 @@ +import { ActivityIndicator } from 'react-native'; +; diff --git a/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures/bailout-spread/code.js b/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures/bailout-spread/code.js new file mode 100644 index 00000000..4325b91d --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures/bailout-spread/code.js @@ -0,0 +1,3 @@ +import { ActivityIndicator } from 'react-native'; + +; diff --git a/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures/bailout-spread/output.js b/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures/bailout-spread/output.js new file mode 100644 index 00000000..2fc61a08 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures/bailout-spread/output.js @@ -0,0 +1,2 @@ +import { ActivityIndicator } from 'react-native'; +; diff --git a/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures/dynamic-props/code.js b/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures/dynamic-props/code.js new file mode 100644 index 00000000..19aa5674 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures/dynamic-props/code.js @@ -0,0 +1,9 @@ +import { ActivityIndicator } from 'react-native'; + +const animating = true; +const color = 'red'; +const hides = false; +const size = 24; +const style = { margin: 4 }; + +; diff --git a/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures/dynamic-props/output.js b/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures/dynamic-props/output.js new file mode 100644 index 00000000..79375ddc --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures/dynamic-props/output.js @@ -0,0 +1,24 @@ +import { + NativeView as _NativeView, + activityIndicatorStyles as _activityIndicatorStyles, + NativeActivityIndicator as _NativeActivityIndicator, + resolveActivityIndicatorDefault as _resolveActivityIndicatorDefault, + processActivityIndicatorSize as _processActivityIndicatorSize, + processActivityIndicatorStyle as _processActivityIndicatorStyle, +} from 'react-native-boost/runtime'; +import { ActivityIndicator } from 'react-native'; +const animating = true; +const color = 'red'; +const hides = false; +const size = 24; +const style = { + margin: 4, +}; +<_NativeView style={_processActivityIndicatorStyle(style)}> + <_NativeActivityIndicator + animating={_resolveActivityIndicatorDefault(animating, true)} + color={_resolveActivityIndicatorDefault(color, '#999999')} + hidesWhenStopped={_resolveActivityIndicatorDefault(hides, true)} + {..._processActivityIndicatorSize(size)} + /> +; diff --git a/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures/forced-children/code.js b/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures/forced-children/code.js new file mode 100644 index 00000000..8072da6f --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures/forced-children/code.js @@ -0,0 +1,8 @@ +import { ActivityIndicator, View } from 'react-native'; + +<> + {/* @boost-force */} + + + +; diff --git a/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures/forced-children/output.js b/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures/forced-children/output.js new file mode 100644 index 00000000..b36686f2 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures/forced-children/output.js @@ -0,0 +1,18 @@ +import { + NativeView as _NativeView, + activityIndicatorStyles as _activityIndicatorStyles, + NativeActivityIndicator as _NativeActivityIndicator, +} from 'react-native-boost/runtime'; +import { ActivityIndicator, View } from 'react-native'; +<> + {/* @boost-force */} + <_NativeView style={_activityIndicatorStyles.container}> + <_NativeActivityIndicator + animating={true} + color="#999999" + hidesWhenStopped={true} + style={_activityIndicatorStyles.small} + size="small" + /> + +; diff --git a/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures/forced-impure-prop/code.js b/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures/forced-impure-prop/code.js new file mode 100644 index 00000000..7ce15529 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures/forced-impure-prop/code.js @@ -0,0 +1,6 @@ +import { ActivityIndicator } from 'react-native'; + +<> + {/* @boost-force */} + +; diff --git a/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures/forced-impure-prop/output.js b/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures/forced-impure-prop/output.js new file mode 100644 index 00000000..e5cee816 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures/forced-impure-prop/output.js @@ -0,0 +1,19 @@ +import { + NativeView as _NativeView, + activityIndicatorStyles as _activityIndicatorStyles, + NativeActivityIndicator as _NativeActivityIndicator, + resolveActivityIndicatorDefault as _resolveActivityIndicatorDefault, +} from 'react-native-boost/runtime'; +import { ActivityIndicator } from 'react-native'; +<> + {/* @boost-force */} + <_NativeView style={_activityIndicatorStyles.container}> + <_NativeActivityIndicator + animating={true} + color={_resolveActivityIndicatorDefault(getColor(), '#999999')} + hidesWhenStopped={true} + style={_activityIndicatorStyles.small} + size="small" + /> + +; diff --git a/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures/forced-spread/code.js b/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures/forced-spread/code.js new file mode 100644 index 00000000..3878f552 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures/forced-spread/code.js @@ -0,0 +1,6 @@ +import { ActivityIndicator } from 'react-native'; + +<> + {/* @boost-force */} + +; diff --git a/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures/forced-spread/output.js b/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures/forced-spread/output.js new file mode 100644 index 00000000..c5f746f4 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures/forced-spread/output.js @@ -0,0 +1,19 @@ +import { + NativeView as _NativeView, + activityIndicatorStyles as _activityIndicatorStyles, + NativeActivityIndicator as _NativeActivityIndicator, +} from 'react-native-boost/runtime'; +import { ActivityIndicator } from 'react-native'; +<> + {/* @boost-force */} + <_NativeView style={_activityIndicatorStyles.container}> + <_NativeActivityIndicator + animating={true} + color="#999999" + hidesWhenStopped={true} + {...props} + style={_activityIndicatorStyles.small} + size="small" + /> + +; diff --git a/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures/static-props/code.js b/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures/static-props/code.js new file mode 100644 index 00000000..fd2bd991 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures/static-props/code.js @@ -0,0 +1,3 @@ +import { ActivityIndicator } from 'react-native'; + +; diff --git a/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures/static-props/output.js b/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures/static-props/output.js new file mode 100644 index 00000000..5fbe1133 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures/static-props/output.js @@ -0,0 +1,16 @@ +import { + NativeView as _NativeView, + activityIndicatorStyles as _activityIndicatorStyles, + NativeActivityIndicator as _NativeActivityIndicator, +} from 'react-native-boost/runtime'; +import { ActivityIndicator } from 'react-native'; +<_NativeView style={_activityIndicatorStyles.container}> + <_NativeActivityIndicator + animating={false} + color="red" + hidesWhenStopped={true} + testID="spinner" + style={_activityIndicatorStyles.large} + size="large" + /> +; diff --git a/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures/text-ancestor/code.js b/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures/text-ancestor/code.js new file mode 100644 index 00000000..779e1da7 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures/text-ancestor/code.js @@ -0,0 +1,5 @@ +import { ActivityIndicator, Text } from 'react-native'; + + + +; diff --git a/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures/text-ancestor/output.js b/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures/text-ancestor/output.js new file mode 100644 index 00000000..318c9b93 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/fixtures/text-ancestor/output.js @@ -0,0 +1,17 @@ +import { + NativeView as _NativeView, + activityIndicatorStyles as _activityIndicatorStyles, + NativeActivityIndicator as _NativeActivityIndicator, +} from 'react-native-boost/runtime'; +import { ActivityIndicator, Text } from 'react-native'; + + <_NativeView style={_activityIndicatorStyles.container}> + <_NativeActivityIndicator + animating={true} + color="#999999" + hidesWhenStopped={true} + style={_activityIndicatorStyles.small} + size="small" + /> + +; diff --git a/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/index.test.ts b/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/index.test.ts new file mode 100644 index 00000000..5f665980 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/__tests__/index.test.ts @@ -0,0 +1,55 @@ +import path from 'node:path'; +import { pluginTester } from 'babel-plugin-tester'; +import { nativeActivityIndicatorOptimizer } from '..'; +import { generateTestPlugin } from '../../../utils/generate-test-plugin'; +import { formatTestResult } from '../../../utils/format-test-result'; + +const babelOptions = { plugins: ['@babel/plugin-syntax-jsx'] }; + +pluginTester({ + plugin: generateTestPlugin(nativeActivityIndicatorOptimizer, {}, 'ios'), + title: 'activity indicator ios', + fixtures: path.resolve(import.meta.dirname, 'fixtures'), + babelOptions, + formatResult: formatTestResult, +}); + +pluginTester({ + plugin: generateTestPlugin(nativeActivityIndicatorOptimizer, {}, 'android'), + title: 'activity indicator android', + fixtures: path.resolve(import.meta.dirname, 'fixtures-android'), + babelOptions, + formatResult: formatTestResult, +}); + +pluginTester({ + plugin: generateTestPlugin(nativeActivityIndicatorOptimizer), + title: 'activity indicator unknown platform', + fixtures: path.resolve(import.meta.dirname, 'fixtures-unknown-platform'), + babelOptions, + formatResult: formatTestResult, +}); + +pluginTester({ + plugin: generateTestPlugin(nativeActivityIndicatorOptimizer, {}, 'web'), + title: 'activity indicator web', + fixtures: path.resolve(import.meta.dirname, 'fixtures-web'), + babelOptions, + formatResult: formatTestResult, +}); + +pluginTester({ + plugin: generateTestPlugin(nativeActivityIndicatorOptimizer, { integrations: { unistyles: 'on' } }, 'ios'), + title: 'activity indicator unistyles', + fixtures: path.resolve(import.meta.dirname, 'fixtures-unistyles'), + babelOptions, + formatResult: formatTestResult, +}); + +pluginTester({ + plugin: generateTestPlugin(nativeActivityIndicatorOptimizer, {}, 'ios'), + title: 'activity indicator unknown ancestor', + fixtures: path.resolve(import.meta.dirname, 'fixtures-unknown-ancestor'), + babelOptions, + formatResult: formatTestResult, +}); diff --git a/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/index.ts b/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/index.ts new file mode 100644 index 00000000..57fc7591 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-activity-indicator/index.ts @@ -0,0 +1,289 @@ +import { NodePath, types as t } from '@babel/core'; +import type { HubFile, JSXOptimizer } from '../../types'; +import PluginError from '../../utils/plugin-error'; +import { BailoutCheck, getFirstBailoutReason } from '../../utils/helpers'; +import { + addFileImportHint, + createStyleOriginResolver, + isForcedLine, + isIgnoredLine, + isReactNativeComponent, + isStaticLiteralTree, + makeAttribute, + replaceWithNativeComponent, +} from '../../utils/common'; +import { RUNTIME_MODULE_NAME } from '../../utils/constants'; +import { createJSXOptimizer } from '../../utils/optimizer'; + +const CONSUMED_PROPS = new Set([ + 'animating', + 'children', + 'color', + 'hidesWhenStopped', + 'key', + 'onLayout', + 'ref', + 'size', + 'style', +]); + +const optimizeNativeActivityIndicator: JSXOptimizer = (path, { logger, platform, unistylesEnabled, options }) => { + if (platform === 'web') return; + if (!isReactNativeComponent(path, 'ActivityIndicator')) return; + + if (platform !== 'ios' && platform !== 'android') { + logger.skipped({ target: 'ActivityIndicator', path, reason: 'target platform is unknown' }); + return; + } + + const parent = path.parent as t.JSXElement; + const forced = isForcedLine(path); + const getStyleOrigin = createStyleOriginResolver(path, unistylesEnabled); + + const bailoutChecks: BailoutCheck[] = [ + { + reason: 'has a Unistyles style that cannot be moved to the generated outer host', + shouldBail: () => getStyleOrigin() === 'unistyles', + }, + { + reason: 'has spread props', + shouldBail: () => path.node.attributes.some((attribute) => t.isJSXSpreadAttribute(attribute)), + }, + { + reason: 'contains children', + shouldBail: () => + findAttribute(path.node.attributes, 'children') !== undefined || + parent.children.some((child) => !t.isJSXText(child) || child.value.trim() !== ''), + }, + { + reason: 'contains an impure prop expression that cannot be safely reordered', + shouldBail: () => hasImpureAttributeValue(path), + }, + { + reason: 'has an unresolved style source that may be a Unistyles style', + shouldBail: () => getStyleOrigin() === 'unknown', + }, + ]; + + if (forced) { + const overriddenReason = getFirstBailoutReason(bailoutChecks); + if (overriddenReason) logger.forced({ target: 'ActivityIndicator', path, reason: overriddenReason }); + } else { + const skipReason = getFirstBailoutReason([ + { + reason: 'line is marked with @boost-ignore', + shouldBail: () => isIgnoredLine(path), + }, + ...bailoutChecks, + ]); + if (skipReason) { + logger.skipped({ target: 'ActivityIndicator', path, reason: skipReason }); + return; + } + } + + const hub = path.hub as unknown; + const file = typeof hub === 'object' && hub !== null && 'file' in hub ? (hub.file as HubFile) : undefined; + if (!file) throw new PluginError('No file found in Babel hub'); + + if (options?.integrations?.uniwind === 'on') { + replaceWithNativeComponent(path, parent, file, 'NativeActivityIndicator', { + moduleName: 'react-native-boost/uniwind', + }); + logger.optimized({ target: 'ActivityIndicator', path }); + return; + } + + const view = addRuntimeImport(path, file, 'NativeView'); + const styles = addRuntimeImport(path, file, 'activityIndicatorStyles'); + const host = addRuntimeImport(path, file, 'NativeActivityIndicator'); + const attributes = path.node.attributes; + const animating = buildDefaultedAttribute(path, file, attributes, 'animating', t.booleanLiteral(true)); + const color = buildDefaultedAttribute( + path, + file, + attributes, + 'color', + platform === 'ios' ? t.stringLiteral('#999999') : t.nullLiteral() + ); + const hidesWhenStopped = buildDefaultedAttribute(path, file, attributes, 'hidesWhenStopped', t.booleanLiteral(true)); + const sizeAttributes = buildSizeAttributes(path, file, attributes, styles); + const outerStyle = buildOuterStyle(path, file, attributes, styles); + + const remaining = attributes + .filter( + (attribute) => + !t.isJSXAttribute(attribute) || !t.isJSXIdentifier(attribute.name) || !CONSUMED_PROPS.has(attribute.name.name) + ) + .map((attribute) => t.cloneNode(attribute, true)); + + const ref = findAttribute(attributes, 'ref'); + const onLayout = findAttribute(attributes, 'onLayout'); + const key = findAttribute(attributes, 'key'); + + const innerAttributes: Array = [ + animating, + color, + hidesWhenStopped, + ...remaining, + ref ? t.cloneNode(ref, true) : undefined, + ...sizeAttributes, + platform === 'android' ? makeAttribute('styleAttr', t.stringLiteral('Normal')) : undefined, + platform === 'android' ? makeAttribute('indeterminate', t.booleanLiteral(true)) : undefined, + ].filter((attribute): attribute is t.JSXAttribute | t.JSXSpreadAttribute => attribute !== undefined); + + const inner = t.jsxElement(t.jsxOpeningElement(t.jsxIdentifier(host.name), innerAttributes, true), null, [], true); + const outerAttributes = [ + key ? t.cloneNode(key, true) : undefined, + onLayout ? t.cloneNode(onLayout, true) : undefined, + makeAttribute('style', outerStyle), + ].filter((attribute): attribute is t.JSXAttribute => attribute !== undefined); + const outer = t.jsxElement( + t.jsxOpeningElement(t.jsxIdentifier(view.name), outerAttributes, false), + t.jsxClosingElement(t.jsxIdentifier(view.name)), + [inner], + false + ); + + t.inheritsComments(outer, parent); + logger.optimized({ target: 'ActivityIndicator', path }); + path.parentPath.replaceWith(outer); +}; + +export const nativeActivityIndicatorOptimizer = createJSXOptimizer( + 'native-activity-indicator', + optimizeNativeActivityIndicator +); + +function addRuntimeImport(path: NodePath, file: HubFile, importName: string): t.Identifier { + return addFileImportHint({ + file, + nameHint: importName, + path, + importName, + moduleName: RUNTIME_MODULE_NAME, + }); +} + +function buildDefaultedAttribute( + path: NodePath, + file: HubFile, + attributes: Array, + name: string, + fallback: t.Expression +): t.JSXAttribute { + const attribute = findAttribute(attributes, name); + if (!attribute) return makeAttribute(name, fallback); + + const value = getAttributeValueExpression(attribute); + if (isDefinitelyDefined(value)) return makeAttribute(name, t.cloneNode(value, true)); + + const resolver = addRuntimeImport(path, file, 'resolveActivityIndicatorDefault'); + return makeAttribute( + name, + t.callExpression(t.identifier(resolver.name), [t.cloneNode(value, true), t.cloneNode(fallback, true)]) + ); +} + +function buildSizeAttributes( + path: NodePath, + file: HubFile, + attributes: Array, + styles: t.Identifier +): Array { + const attribute = findAttribute(attributes, 'size'); + if (!attribute) return staticSizeAttributes('small', styles); + + const value = getAttributeValueExpression(attribute); + if (isStaticLiteralTree(value)) { + if (t.isStringLiteral(value) && (value.value === 'small' || value.value === 'large')) { + return staticSizeAttributes(value.value, styles); + } + return [ + makeAttribute( + 'style', + t.objectExpression([ + t.objectProperty(t.identifier('height'), t.cloneNode(value, true)), + t.objectProperty(t.identifier('width'), t.cloneNode(value, true)), + ]) + ), + ]; + } + + const resolver = addRuntimeImport(path, file, 'processActivityIndicatorSize'); + return [t.jsxSpreadAttribute(t.callExpression(t.identifier(resolver.name), [t.cloneNode(value, true)]))]; +} + +function staticSizeAttributes(size: 'small' | 'large', styles: t.Identifier): t.JSXAttribute[] { + return [ + makeAttribute('style', t.memberExpression(t.identifier(styles.name), t.identifier(size))), + makeAttribute('size', t.stringLiteral(size)), + ]; +} + +function buildOuterStyle( + path: NodePath, + file: HubFile, + attributes: Array, + styles: t.Identifier +): t.Expression { + const container = t.memberExpression(t.identifier(styles.name), t.identifier('container')); + const attribute = findAttribute(attributes, 'style'); + if (!attribute) return container; + + const value = getAttributeValueExpression(attribute); + const truthiness = getStaticTruthiness(value); + if (truthiness === false) return container; + if (truthiness === true) return t.arrayExpression([container, t.cloneNode(value, true)]); + + const resolver = addRuntimeImport(path, file, 'processActivityIndicatorStyle'); + return t.callExpression(t.identifier(resolver.name), [t.cloneNode(value, true)]); +} + +function findAttribute( + attributes: Array, + name: string +): t.JSXAttribute | undefined { + return attributes.findLast( + (attribute): attribute is t.JSXAttribute => + t.isJSXAttribute(attribute) && t.isJSXIdentifier(attribute.name, { name }) + ); +} + +function getAttributeValueExpression(attribute: t.JSXAttribute): t.Expression { + if (!attribute.value) return t.booleanLiteral(true); + if (t.isStringLiteral(attribute.value)) return attribute.value; + if (t.isJSXExpressionContainer(attribute.value)) { + return t.isJSXEmptyExpression(attribute.value.expression) ? t.booleanLiteral(true) : attribute.value.expression; + } + return t.nullLiteral(); +} + +function isDefinitelyDefined(expression: t.Expression): boolean { + return ( + isStaticLiteralTree(expression) || + t.isArrowFunctionExpression(expression) || + t.isFunctionExpression(expression) || + t.isTemplateLiteral(expression) + ); +} + +function getStaticTruthiness(expression: t.Expression): boolean | undefined { + if (t.isNullLiteral(expression)) return false; + if (t.isBooleanLiteral(expression)) return expression.value; + if (t.isStringLiteral(expression)) return expression.value.length > 0; + if (t.isNumericLiteral(expression)) return expression.value !== 0 && !Number.isNaN(expression.value); + if (t.isUnaryExpression(expression) && expression.operator === '-' && t.isNumericLiteral(expression.argument)) { + return expression.argument.value !== 0; + } + if (t.isObjectExpression(expression) || t.isArrayExpression(expression)) return true; + return undefined; +} + +function hasImpureAttributeValue(path: NodePath): boolean { + return path.node.attributes.some((attribute) => { + if (!t.isJSXAttribute(attribute) || !t.isJSXExpressionContainer(attribute.value)) return false; + if (t.isJSXEmptyExpression(attribute.value.expression)) return false; + return !path.scope.isPure(attribute.value.expression); + }); +} diff --git a/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-rn-86/forced-children/code.js b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-rn-86/forced-children/code.js new file mode 100644 index 00000000..781cb238 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-rn-86/forced-children/code.js @@ -0,0 +1,8 @@ +import { Image } from 'react-native'; + +<> + {/* @boost-force */} + + + +; diff --git a/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-rn-86/forced-children/output.js b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-rn-86/forced-children/output.js new file mode 100644 index 00000000..ec3877d7 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-rn-86/forced-children/output.js @@ -0,0 +1,21 @@ +import { NativeImage as _NativeImage } from 'react-native-boost/runtime'; +import { Image } from 'react-native'; +const _imageSource = [ + { + uri: 'logo.png', + }, +]; +<> + {/* @boost-force */} + <_NativeImage + style={[ + {}, + { + overflow: 'hidden', + }, + ]} + source={_imageSource} + resizeMode="cover"> + + +; diff --git a/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-rn-86/forced-dynamic-src-set-style/code.js b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-rn-86/forced-dynamic-src-set-style/code.js new file mode 100644 index 00000000..3a1f7b62 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-rn-86/forced-dynamic-src-set-style/code.js @@ -0,0 +1,6 @@ +import { Image } from 'react-native'; + +<> + {/* @boost-force */} + +; diff --git a/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-rn-86/forced-dynamic-src-set-style/output.js b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-rn-86/forced-dynamic-src-set-style/output.js new file mode 100644 index 00000000..e8bd2389 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-rn-86/forced-dynamic-src-set-style/output.js @@ -0,0 +1,14 @@ +import { + processImageSourceProps as _processImageSourceProps, + NativeImage as _NativeImage, +} from 'react-native-boost/runtime'; +import { Image } from 'react-native'; +<> + {/* @boost-force */} + <_NativeImage + srcSet="logo.png 1x" + {..._processImageSourceProps({ + style: styles.image, + })} + /> +; diff --git a/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-rn-86/forced-dynamic-src-set/code.js b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-rn-86/forced-dynamic-src-set/code.js new file mode 100644 index 00000000..6b4e30df --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-rn-86/forced-dynamic-src-set/code.js @@ -0,0 +1,6 @@ +import { Image } from 'react-native'; + +<> + {/* @boost-force */} + +; diff --git a/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-rn-86/forced-dynamic-src-set/output.js b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-rn-86/forced-dynamic-src-set/output.js new file mode 100644 index 00000000..01be4b7e --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-rn-86/forced-dynamic-src-set/output.js @@ -0,0 +1,6 @@ +import { NativeImage as _NativeImage } from 'react-native-boost/runtime'; +import { Image } from 'react-native'; +<> + {/* @boost-force */} + <_NativeImage srcSet={sources} /> +; diff --git a/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-rn-86/forced-missing-source/code.js b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-rn-86/forced-missing-source/code.js new file mode 100644 index 00000000..99521240 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-rn-86/forced-missing-source/code.js @@ -0,0 +1,6 @@ +import { Image } from 'react-native'; + +<> + {/* @boost-force */} + +; diff --git a/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-rn-86/forced-missing-source/output.js b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-rn-86/forced-missing-source/output.js new file mode 100644 index 00000000..c552e0b2 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-rn-86/forced-missing-source/output.js @@ -0,0 +1,6 @@ +import { NativeImage as _NativeImage } from 'react-native-boost/runtime'; +import { Image } from 'react-native'; +<> + {/* @boost-force */} + <_NativeImage /> +; diff --git a/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-rn-86/forced-spread/code.js b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-rn-86/forced-spread/code.js new file mode 100644 index 00000000..ed52d3cc --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-rn-86/forced-spread/code.js @@ -0,0 +1,6 @@ +import { Image } from 'react-native'; + +<> + {/* @boost-force */} + +; diff --git a/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-rn-86/forced-spread/output.js b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-rn-86/forced-spread/output.js new file mode 100644 index 00000000..39df1f5e --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-rn-86/forced-spread/output.js @@ -0,0 +1,21 @@ +import { NativeImage as _NativeImage } from 'react-native-boost/runtime'; +import { Image } from 'react-native'; +const _imageSource = [ + { + uri: 'logo.png', + }, +]; +<> + {/* @boost-force */} + <_NativeImage + {...props} + style={[ + {}, + { + overflow: 'hidden', + }, + ]} + source={_imageSource} + resizeMode="cover" + /> +; diff --git a/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-rn-86/forced-unsupported-prop/code.js b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-rn-86/forced-unsupported-prop/code.js new file mode 100644 index 00000000..62a38788 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-rn-86/forced-unsupported-prop/code.js @@ -0,0 +1,6 @@ +import { Image } from 'react-native'; + +<> + {/* @boost-force */} + +; diff --git a/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-rn-86/forced-unsupported-prop/output.js b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-rn-86/forced-unsupported-prop/output.js new file mode 100644 index 00000000..f9686db6 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-rn-86/forced-unsupported-prop/output.js @@ -0,0 +1,21 @@ +import { NativeImage as _NativeImage } from 'react-native-boost/runtime'; +import { Image } from 'react-native'; +const _imageSource = [ + { + uri: 'logo.png', + }, +]; +<> + {/* @boost-force */} + <_NativeImage + onLoad={handleLoad} + style={[ + {}, + { + overflow: 'hidden', + }, + ]} + source={_imageSource} + resizeMode="cover" + /> +; diff --git a/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-rn-86/src-set-bailout-dynamic-dimension/code.js b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-rn-86/src-set-bailout-dynamic-dimension/code.js new file mode 100644 index 00000000..49dba2f5 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-rn-86/src-set-bailout-dynamic-dimension/code.js @@ -0,0 +1,3 @@ +import { Image } from 'react-native'; + +; diff --git a/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-rn-86/src-set-bailout-dynamic-dimension/output.js b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-rn-86/src-set-bailout-dynamic-dimension/output.js new file mode 100644 index 00000000..687ed4b7 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-rn-86/src-set-bailout-dynamic-dimension/output.js @@ -0,0 +1,2 @@ +import { Image } from 'react-native'; +; diff --git a/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-rn-86/src-set-bailout-dynamic-style/code.js b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-rn-86/src-set-bailout-dynamic-style/code.js new file mode 100644 index 00000000..c34a413e --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-rn-86/src-set-bailout-dynamic-style/code.js @@ -0,0 +1,3 @@ +import { Image } from 'react-native'; + +; diff --git a/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-rn-86/src-set-bailout-dynamic-style/output.js b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-rn-86/src-set-bailout-dynamic-style/output.js new file mode 100644 index 00000000..24383dbe --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-rn-86/src-set-bailout-dynamic-style/output.js @@ -0,0 +1,2 @@ +import { Image } from 'react-native'; +; diff --git a/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-rn-86/src-set-bailout-dynamic/code.js b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-rn-86/src-set-bailout-dynamic/code.js new file mode 100644 index 00000000..73318344 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-rn-86/src-set-bailout-dynamic/code.js @@ -0,0 +1,3 @@ +import { Image } from 'react-native'; + +; diff --git a/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-rn-86/src-set-bailout-dynamic/output.js b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-rn-86/src-set-bailout-dynamic/output.js new file mode 100644 index 00000000..15033b19 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-rn-86/src-set-bailout-dynamic/output.js @@ -0,0 +1,2 @@ +import { Image } from 'react-native'; +; diff --git a/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-rn-86/src-set-bailout-malformed/code.js b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-rn-86/src-set-bailout-malformed/code.js new file mode 100644 index 00000000..f0f339fd --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-rn-86/src-set-bailout-malformed/code.js @@ -0,0 +1,3 @@ +import { Image } from 'react-native'; + +; diff --git a/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-rn-86/src-set-bailout-malformed/output.js b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-rn-86/src-set-bailout-malformed/output.js new file mode 100644 index 00000000..645b5829 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-rn-86/src-set-bailout-malformed/output.js @@ -0,0 +1,2 @@ +import { Image } from 'react-native'; +; diff --git a/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-rn-86/src-set-bailout-source-prop/code.js b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-rn-86/src-set-bailout-source-prop/code.js new file mode 100644 index 00000000..385ed308 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-rn-86/src-set-bailout-source-prop/code.js @@ -0,0 +1,3 @@ +import { Image } from 'react-native'; + +; diff --git a/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-rn-86/src-set-bailout-source-prop/output.js b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-rn-86/src-set-bailout-source-prop/output.js new file mode 100644 index 00000000..f5021469 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-rn-86/src-set-bailout-source-prop/output.js @@ -0,0 +1,7 @@ +import { Image } from 'react-native'; +; diff --git a/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-rn-86/src-set-bailout-unsupported-descriptor/code.js b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-rn-86/src-set-bailout-unsupported-descriptor/code.js new file mode 100644 index 00000000..eca121ba --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-rn-86/src-set-bailout-unsupported-descriptor/code.js @@ -0,0 +1,3 @@ +import { Image } from 'react-native'; + +; diff --git a/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-rn-86/src-set-bailout-unsupported-descriptor/output.js b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-rn-86/src-set-bailout-unsupported-descriptor/output.js new file mode 100644 index 00000000..29b0a835 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-rn-86/src-set-bailout-unsupported-descriptor/output.js @@ -0,0 +1,2 @@ +import { Image } from 'react-native'; +; diff --git a/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-rn-86/src-set-bailout-unsupported-separator/code.js b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-rn-86/src-set-bailout-unsupported-separator/code.js new file mode 100644 index 00000000..248ab9f6 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-rn-86/src-set-bailout-unsupported-separator/code.js @@ -0,0 +1,3 @@ +import { Image } from 'react-native'; + +; diff --git a/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-rn-86/src-set-bailout-unsupported-separator/output.js b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-rn-86/src-set-bailout-unsupported-separator/output.js new file mode 100644 index 00000000..c8cda9b2 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-rn-86/src-set-bailout-unsupported-separator/output.js @@ -0,0 +1,2 @@ +import { Image } from 'react-native'; +; diff --git a/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-rn-86/src-set-literal-spread/code.js b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-rn-86/src-set-literal-spread/code.js new file mode 100644 index 00000000..f391f17f --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-rn-86/src-set-literal-spread/code.js @@ -0,0 +1,3 @@ +import { Image } from 'react-native'; + +; diff --git a/packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/src-prop/output.js b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-rn-86/src-set-literal-spread/output.js similarity index 62% rename from packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/src-prop/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-rn-86/src-set-literal-spread/output.js index c90a0880..e90496b2 100644 --- a/packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/src-prop/output.js +++ b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-rn-86/src-set-literal-spread/output.js @@ -1,19 +1,20 @@ import { NativeImage as _NativeImage } from 'react-native-boost/runtime'; import { Image } from 'react-native'; +const _imageSource = [ + { + headers: {}, + scale: 1, + uri: 'logo.png', + }, +]; <_NativeImage + testID={'logo'} style={[ {}, { overflow: 'hidden', }, ]} - source={[ - { - uri: 'https://example.com/a.png', - headers: {}, - width: 10, - height: 20, - }, - ]} + source={_imageSource} resizeMode="cover" />; diff --git a/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-rn-86/static-accessibility/code.js b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-rn-86/static-accessibility/code.js new file mode 100644 index 00000000..04c55b40 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-rn-86/static-accessibility/code.js @@ -0,0 +1,12 @@ +import { Image } from 'react-native'; + +Fallback; +{null}; +Fallback; diff --git a/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-rn-86/static-accessibility/output.js b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-rn-86/static-accessibility/output.js new file mode 100644 index 00000000..05f941de --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-rn-86/static-accessibility/output.js @@ -0,0 +1,69 @@ +import { + NativeImage as _NativeImage, + processImageAccessibilityProps as _processImageAccessibilityProps, +} from 'react-native-boost/runtime'; +import { Image } from 'react-native'; +const _imageSource = [ + { + uri: 'logo.png', + headers: {}, + }, + ], + _imageSource2 = [ + { + uri: 'logo.png', + headers: {}, + }, + ], + _imageSource3 = [ + { + uri: 'logo.png', + headers: {}, + }, + ]; +<_NativeImage + alt="Fallback" + aria-label="Winner" + accessibilityState={{ + disabled: false, + }} + accessibilityLabel="Winner" + accessible={false} + style={[ + {}, + { + overflow: 'hidden', + }, + ]} + source={_imageSource} + resizeMode="cover" +/>; +<_NativeImage + alt={null} + accessible={true} + accessibilityState={void 0} + accessibilityLabel={null} + style={[ + {}, + { + overflow: 'hidden', + }, + ]} + source={_imageSource2} + resizeMode="cover" +/>; +<_NativeImage + {..._processImageAccessibilityProps({ + 'alt': 'Fallback', + 'aria-label': label, + 'aria-busy': true, + })} + style={[ + {}, + { + overflow: 'hidden', + }, + ]} + source={_imageSource3} + resizeMode="cover" +/>; diff --git a/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-unistyles/forced-style/code.js b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-unistyles/forced-style/code.js new file mode 100644 index 00000000..24fcd379 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-unistyles/forced-style/code.js @@ -0,0 +1,9 @@ +import { Image } from 'react-native'; +import { StyleSheet } from 'react-native-unistyles'; + +const styles = StyleSheet.create({ image: { width: 16 } }); + +<> + {/* @boost-force */} + +; diff --git a/packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/force-dynamic-runtime/output.js b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-unistyles/forced-style/output.js similarity index 65% rename from packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/force-dynamic-runtime/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-unistyles/forced-style/output.js index 7a6a02ca..5f4b81a2 100644 --- a/packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/force-dynamic-runtime/output.js +++ b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-unistyles/forced-style/output.js @@ -3,22 +3,20 @@ import { NativeImage as _NativeImage, } from 'react-native-boost/runtime'; import { Image } from 'react-native'; +import { StyleSheet } from 'react-native-unistyles'; +const styles = StyleSheet.create({ + image: { + width: 16, + }, +}); <> - {/* @boost-force */} - <_NativeImage - {..._processImageSourceProps({ - source: source, - })} - /> {/* @boost-force */} <_NativeImage {..._processImageSourceProps({ source: { uri: 'logo.png', - width: 16, - height: 16, }, - style: style, + style: styles.image, })} /> ; diff --git a/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-unistyles/forced-unresolved-style/code.js b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-unistyles/forced-unresolved-style/code.js new file mode 100644 index 00000000..64f09fcb --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-unistyles/forced-unresolved-style/code.js @@ -0,0 +1,6 @@ +import { Image } from 'react-native'; + +<> + {/* @boost-force */} + +; diff --git a/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-unistyles/forced-unresolved-style/output.js b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-unistyles/forced-unresolved-style/output.js new file mode 100644 index 00000000..f4e7638e --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-unistyles/forced-unresolved-style/output.js @@ -0,0 +1,16 @@ +import { + processImageSourceProps as _processImageSourceProps, + NativeImage as _NativeImage, +} from 'react-native-boost/runtime'; +import { Image } from 'react-native'; +<> + {/* @boost-force */} + <_NativeImage + {..._processImageSourceProps({ + source: { + uri: 'logo.png', + }, + style: props.style, + })} + /> +; diff --git a/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-unistyles/react-native-style/code.js b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-unistyles/react-native-style/code.js new file mode 100644 index 00000000..70b5959e --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-unistyles/react-native-style/code.js @@ -0,0 +1,5 @@ +import { Image, StyleSheet } from 'react-native'; + +const styles = StyleSheet.create({ image: { width: 16 } }); + +; diff --git a/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-unistyles/react-native-style/output.js b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-unistyles/react-native-style/output.js new file mode 100644 index 00000000..a90f7eb6 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-unistyles/react-native-style/output.js @@ -0,0 +1,18 @@ +import { + processImageSourceProps as _processImageSourceProps, + NativeImage as _NativeImage, +} from 'react-native-boost/runtime'; +import { Image, StyleSheet } from 'react-native'; +const styles = StyleSheet.create({ + image: { + width: 16, + }, +}); +<_NativeImage + {..._processImageSourceProps({ + source: { + uri: 'logo.png', + }, + style: styles.image, + })} +/>; diff --git a/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-unistyles/style-bailout/code.js b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-unistyles/style-bailout/code.js new file mode 100644 index 00000000..916268f4 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-unistyles/style-bailout/code.js @@ -0,0 +1,6 @@ +import { Image } from 'react-native'; +import { StyleSheet } from 'react-native-unistyles'; + +const styles = StyleSheet.create({ image: { width: 16 } }); + +; diff --git a/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-unistyles/style-bailout/output.js b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-unistyles/style-bailout/output.js new file mode 100644 index 00000000..26c974bd --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-unistyles/style-bailout/output.js @@ -0,0 +1,13 @@ +import { Image } from 'react-native'; +import { StyleSheet } from 'react-native-unistyles'; +const styles = StyleSheet.create({ + image: { + width: 16, + }, +}); +; diff --git a/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-unistyles/unresolved-style-bailout/code.js b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-unistyles/unresolved-style-bailout/code.js new file mode 100644 index 00000000..f681760d --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-unistyles/unresolved-style-bailout/code.js @@ -0,0 +1,3 @@ +import { Image } from 'react-native'; + +; diff --git a/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-unistyles/unresolved-style-bailout/output.js b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-unistyles/unresolved-style-bailout/output.js new file mode 100644 index 00000000..69e1757c --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-unistyles/unresolved-style-bailout/output.js @@ -0,0 +1,7 @@ +import { Image } from 'react-native'; +; diff --git a/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-unknown-platform/basic/code.js b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-unknown-platform/basic/code.js new file mode 100644 index 00000000..87d4ee7b --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-unknown-platform/basic/code.js @@ -0,0 +1,7 @@ +import { Image } from 'react-native'; + +; +<> + {/* @boost-force */} + +; diff --git a/packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/accessibility-spread-bails/output.js b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-unknown-platform/basic/output.js similarity index 57% rename from packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/accessibility-spread-bails/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-unknown-platform/basic/output.js index e15bac3c..23b1d350 100644 --- a/packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/accessibility-spread-bails/output.js +++ b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures-unknown-platform/basic/output.js @@ -1,12 +1,16 @@ import { Image } from 'react-native'; -const props = { - 'aria-label': 'Logo', -}; ; +<> + {/* @boost-force */} + +; diff --git a/packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/accessibility-props/code.js b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures/accessibility-props/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/accessibility-props/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures/accessibility-props/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/accessibility-props/output.js b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures/accessibility-props/output.js similarity index 89% rename from packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/accessibility-props/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures/accessibility-props/output.js index 215dff38..1b6f3609 100644 --- a/packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/accessibility-props/output.js +++ b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures/accessibility-props/output.js @@ -3,6 +3,34 @@ import { NativeImage as _NativeImage, } from 'react-native-boost/runtime'; import { Image } from 'react-native'; +const _imageSource = [ + { + uri: 'logo.png', + width: 16, + height: 16, + }, + ], + _imageSource2 = [ + { + uri: 'logo.png', + width: 16, + height: 16, + }, + ], + _imageSource3 = [ + { + uri: 'logo.png', + width: 16, + height: 16, + }, + ], + _imageSource4 = [ + { + uri: 'logo.png', + width: 16, + height: 16, + }, + ]; const label = getLabel(); const labelledBy = getLabelledBy(); <_NativeImage @@ -20,13 +48,7 @@ const labelledBy = getLabelledBy(); overflow: 'hidden', }, ]} - source={[ - { - uri: 'logo.png', - width: 16, - height: 16, - }, - ]} + source={_imageSource} resizeMode="cover" />; <_NativeImage @@ -44,13 +66,7 @@ const labelledBy = getLabelledBy(); overflow: 'hidden', }, ]} - source={[ - { - uri: 'logo.png', - width: 16, - height: 16, - }, - ]} + source={_imageSource2} resizeMode="cover" />; <_NativeImage @@ -71,13 +87,7 @@ const labelledBy = getLabelledBy(); overflow: 'hidden', }, ]} - source={[ - { - uri: 'logo.png', - width: 16, - height: 16, - }, - ]} + source={_imageSource3} resizeMode="cover" />; <_NativeImage @@ -97,12 +107,6 @@ const labelledBy = getLabelledBy(); overflow: 'hidden', }, ]} - source={[ - { - uri: 'logo.png', - width: 16, - height: 16, - }, - ]} + source={_imageSource4} resizeMode="cover" />; diff --git a/packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/basic/code.js b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures/basic/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/basic/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures/basic/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/basic/output.js b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures/basic/output.js similarity index 70% rename from packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/basic/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures/basic/output.js index cbd58b82..7deb69bb 100644 --- a/packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/basic/output.js +++ b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures/basic/output.js @@ -1,5 +1,12 @@ import { NativeImage as _NativeImage } from 'react-native-boost/runtime'; import { Image } from 'react-native'; +const _imageSource = [ + { + uri: 'logo.png', + width: 24, + height: 16, + }, +]; <_NativeImage testID="logo" style={[ @@ -11,12 +18,6 @@ import { Image } from 'react-native'; overflow: 'hidden', }, ]} - source={[ - { - uri: 'logo.png', - width: 24, - height: 16, - }, - ]} + source={_imageSource} resizeMode="cover" />; diff --git a/packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/dynamic-request-headers-runtime/code.js b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures/dynamic-request-headers-runtime/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/dynamic-request-headers-runtime/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures/dynamic-request-headers-runtime/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/dynamic-request-headers-runtime/output.js b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures/dynamic-request-headers-runtime/output.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/dynamic-request-headers-runtime/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures/dynamic-request-headers-runtime/output.js diff --git a/packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/dynamic-source-runtime/code.js b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures/dynamic-source-runtime/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/dynamic-source-runtime/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures/dynamic-source-runtime/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/dynamic-source-runtime/output.js b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures/dynamic-source-runtime/output.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/dynamic-source-runtime/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures/dynamic-source-runtime/output.js diff --git a/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures/dynamic-style-runtime/code.js b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures/dynamic-style-runtime/code.js new file mode 100644 index 00000000..bc2c4077 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures/dynamic-style-runtime/code.js @@ -0,0 +1,3 @@ +import { Image } from 'react-native'; + +; diff --git a/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures/dynamic-style-runtime/output.js b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures/dynamic-style-runtime/output.js new file mode 100644 index 00000000..f1b79124 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures/dynamic-style-runtime/output.js @@ -0,0 +1,15 @@ +import { + processImageSourceProps as _processImageSourceProps, + NativeImage as _NativeImage, +} from 'react-native-boost/runtime'; +import { Image } from 'react-native'; +<_NativeImage + {..._processImageSourceProps({ + source: { + uri: 'logo.png', + width: 16, + height: 16, + }, + style: style, + })} +/>; diff --git a/packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/event-bails/code.js b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures/event-bails/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/event-bails/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures/event-bails/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/event-bails/output.js b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures/event-bails/output.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/event-bails/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures/event-bails/output.js diff --git a/packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/fallback-semantics/code.js b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures/fallback-semantics/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/fallback-semantics/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures/fallback-semantics/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/fallback-semantics/output.js b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures/fallback-semantics/output.js similarity index 74% rename from packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/fallback-semantics/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures/fallback-semantics/output.js index 6ee1674d..e486a8a4 100644 --- a/packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/fallback-semantics/output.js +++ b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures/fallback-semantics/output.js @@ -1,23 +1,45 @@ -import { NativeImage as _NativeImage } from 'react-native-boost/runtime'; +import { + processImageSourceProps as _processImageSourceProps, + NativeImage as _NativeImage, +} from 'react-native-boost/runtime'; import { Image } from 'react-native'; -<_NativeImage - style={[ +const _imageSource = [ { + uri: 'logo.png', width: 16, - height: 12, + height: 16, }, + ], + _imageSource2 = [ { - overflow: 'hidden', + uri: 'logo.png', + width: 16, + height: 16, }, - ]} - source={[ + ], + _imageSource3 = [ { + uri: 'logo.png', + width: 16, + height: 16, + }, + ], + _imageSource4 = [ + { + uri: 'logo.png', + width: 16, + height: 16, + }, + ]; +<_NativeImage + {..._processImageSourceProps({ + source: { uri: 'logo.png', width: null, height: 12, }, - ]} - resizeMode="cover" + width: 16, + })} />; <_NativeImage style={[ @@ -32,13 +54,7 @@ import { Image } from 'react-native'; resizeMode: 'contain', }, ]} - source={[ - { - uri: 'logo.png', - width: 16, - height: 16, - }, - ]} + source={_imageSource} resizeMode="contain" />; <_NativeImage @@ -54,13 +70,7 @@ import { Image } from 'react-native'; resizeMode: 'contain', }, ]} - source={[ - { - uri: 'logo.png', - width: 16, - height: 16, - }, - ]} + source={_imageSource2} resizeMode="contain" />; <_NativeImage @@ -76,13 +86,7 @@ import { Image } from 'react-native'; objectFit: 'fill', }, ]} - source={[ - { - uri: 'logo.png', - width: 16, - height: 16, - }, - ]} + source={_imageSource3} resizeMode="stretch" />; <_NativeImage @@ -98,13 +102,7 @@ import { Image } from 'react-native'; tintColor: 'red', }, ]} - source={[ - { - uri: 'logo.png', - width: 16, - height: 16, - }, - ]} + source={_imageSource4} resizeMode="cover" tintColor="red" />; diff --git a/packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/native-props/code.js b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures/native-props/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/native-props/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures/native-props/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/native-props/output.js b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures/native-props/output.js similarity index 80% rename from packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/native-props/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures/native-props/output.js index 2030f3b3..7c4d6ce5 100644 --- a/packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/native-props/output.js +++ b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures/native-props/output.js @@ -1,5 +1,12 @@ import { NativeImage as _NativeImage } from 'react-native-boost/runtime'; import { Image } from 'react-native'; +const _imageSource = [ + { + uri: 'logo.png', + width: 16, + height: 16, + }, +]; <_NativeImage blurRadius={2} resizeMethod="resize" @@ -21,12 +28,6 @@ import { Image } from 'react-native'; overflow: 'hidden', }, ]} - source={[ - { - uri: 'logo.png', - width: 16, - height: 16, - }, - ]} + source={_imageSource} resizeMode="cover" />; diff --git a/packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/request-headers/code.js b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures/request-headers/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/request-headers/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures/request-headers/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/request-headers/output.js b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures/request-headers/output.js similarity index 56% rename from packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/request-headers/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures/request-headers/output.js index 29460414..20826ce8 100644 --- a/packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/request-headers/output.js +++ b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures/request-headers/output.js @@ -1,24 +1,34 @@ -import { NativeImage as _NativeImage } from 'react-native-boost/runtime'; +import { + processImageSourceProps as _processImageSourceProps, + NativeImage as _NativeImage, +} from 'react-native-boost/runtime'; import { Image } from 'react-native'; -<_NativeImage - style={[ - {}, +const _imageSource = [ { - overflow: 'hidden', - }, - ]} - source={[ - { - uri: 'https://example.com/logo.png', + uri: 'logo.png', + width: 16, + height: 16, headers: { 'Access-Control-Allow-Credentials': 'true', - 'Referrer-Policy': 'no-referrer', + 'Referrer-Policy': 'origin', }, + }, + ], + _imageSource2 = [ + { + uri: '', width: 16, height: 16, }, - ]} - resizeMode="cover" + ]; +<_NativeImage + {..._processImageSourceProps({ + src: 'https://example.com/logo.png', + width: 16, + height: 16, + crossOrigin: 'use-credentials', + referrerPolicy: 'no-referrer', + })} />; <_NativeImage style={[ @@ -27,17 +37,7 @@ import { Image } from 'react-native'; overflow: 'hidden', }, ]} - source={[ - { - uri: 'logo.png', - width: 16, - height: 16, - headers: { - 'Access-Control-Allow-Credentials': 'true', - 'Referrer-Policy': 'origin', - }, - }, - ]} + source={_imageSource} resizeMode="cover" />; <_NativeImage @@ -50,12 +50,6 @@ import { Image } from 'react-native'; overflow: 'hidden', }, ]} - source={[ - { - uri: '', - width: 16, - height: 16, - }, - ]} + source={_imageSource2} resizeMode="cover" />; diff --git a/packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/require-source/code.js b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures/require-source/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/require-source/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures/require-source/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/require-source/output.js b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures/require-source/output.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/require-source/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures/require-source/output.js diff --git a/packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/source-array/code.js b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures/source-array/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/source-array/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures/source-array/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures/source-array/output.js b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures/source-array/output.js new file mode 100644 index 00000000..1e45188d --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures/source-array/output.js @@ -0,0 +1,28 @@ +import { + processImageSourceProps as _processImageSourceProps, + NativeImage as _NativeImage, +} from 'react-native-boost/runtime'; +import { Image } from 'react-native'; +<_NativeImage + {..._processImageSourceProps({ + source: [ + { + uri: 'logo.png', + width: 16, + height: 16, + scale: 1, + }, + { + uri: 'logo@2x.png', + width: 32, + height: 32, + scale: 2, + }, + ], + width: 16, + height: 16, + style: { + opacity: 0.9, + }, + })} +/>; diff --git a/packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/spread-wrapper-props-bails/code.js b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures/spread-wrapper-props-bails/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/spread-wrapper-props-bails/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures/spread-wrapper-props-bails/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/spread-wrapper-props-bails/output.js b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures/spread-wrapper-props-bails/output.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/spread-wrapper-props-bails/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures/spread-wrapper-props-bails/output.js diff --git a/packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/src-precedence/code.js b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures/src-precedence/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/src-precedence/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures/src-precedence/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures/src-precedence/output.js b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures/src-precedence/output.js new file mode 100644 index 00000000..e7d83b86 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures/src-precedence/output.js @@ -0,0 +1,16 @@ +import { + processImageSourceProps as _processImageSourceProps, + NativeImage as _NativeImage, +} from 'react-native-boost/runtime'; +import { Image } from 'react-native'; +<_NativeImage + {..._processImageSourceProps({ + source: { + uri: 'source.png', + width: 16, + height: 16, + }, + src: 'https://example.com/src.png', + width: 20, + })} +/>; diff --git a/packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/src-prop/code.js b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures/src-prop/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/src-prop/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures/src-prop/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures/src-prop/output.js b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures/src-prop/output.js new file mode 100644 index 00000000..401bb9b0 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures/src-prop/output.js @@ -0,0 +1,12 @@ +import { + processImageSourceProps as _processImageSourceProps, + NativeImage as _NativeImage, +} from 'react-native-boost/runtime'; +import { Image } from 'react-native'; +<_NativeImage + {..._processImageSourceProps({ + src: 'https://example.com/a.png', + width: 10, + height: 20, + })} +/>; diff --git a/packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/static-style-derived-props/code.js b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures/static-style-derived-props/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/static-style-derived-props/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures/static-style-derived-props/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/static-style-derived-props/output.js b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures/static-style-derived-props/output.js similarity index 81% rename from packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/static-style-derived-props/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures/static-style-derived-props/output.js index 6867529a..e17223ed 100644 --- a/packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/static-style-derived-props/output.js +++ b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures/static-style-derived-props/output.js @@ -1,5 +1,10 @@ import { NativeImage as _NativeImage } from 'react-native-boost/runtime'; import { Image } from 'react-native'; +const _imageSource = [ + { + uri: 'hero.png', + }, +]; <_NativeImage style={[ {}, @@ -16,11 +21,7 @@ import { Image } from 'react-native'; }, ], ]} - source={[ - { - uri: 'hero.png', - }, - ]} + source={_imageSource} resizeMode="stretch" tintColor="red" />; diff --git a/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures/unknown-imported-ancestor/code.js b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures/unknown-imported-ancestor/code.js new file mode 100644 index 00000000..7f3a368e --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures/unknown-imported-ancestor/code.js @@ -0,0 +1,6 @@ +import { Image } from 'react-native'; +import { ExternalWrapper } from './ExternalWrapper'; + + + +; diff --git a/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures/unknown-imported-ancestor/output.js b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures/unknown-imported-ancestor/output.js new file mode 100644 index 00000000..c4d609d3 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures/unknown-imported-ancestor/output.js @@ -0,0 +1,11 @@ +import { Image } from 'react-native'; +import { ExternalWrapper } from './ExternalWrapper'; + + +; diff --git a/packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/view-props/code.js b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures/view-props/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/view-props/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures/view-props/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/view-props/output.js b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures/view-props/output.js similarity index 61% rename from packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/view-props/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures/view-props/output.js index 3be9364c..b8ccebe0 100644 --- a/packages/react-native-boost/src/plugin/optimizers/image/__tests__/fixtures/view-props/output.js +++ b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/fixtures/view-props/output.js @@ -1,5 +1,15 @@ -import { NativeImage as _NativeImage } from 'react-native-boost/runtime'; +import { + processImageAccessibilityProps as _processImageAccessibilityProps, + NativeImage as _NativeImage, +} from 'react-native-boost/runtime'; import { Image } from 'react-native'; +const _imageSource = [ + { + uri: 'logo.png', + width: 16, + height: 16, + }, +]; <_NativeImage accessible={true} accessibilityLabel="logo" @@ -8,9 +18,6 @@ import { Image } from 'react-native'; accessibilityValue={{ text: 'loaded', }} - accessibilityState={{ - selected: true, - }} nativeID="logo" pointerEvents="none" collapsable={false} @@ -20,6 +27,11 @@ import { Image } from 'react-native'; borderTopRightRadius={2} borderBottomLeftRadius={3} borderBottomRightRadius={4} + {..._processImageAccessibilityProps({ + accessibilityState: { + selected: true, + }, + })} style={[ { width: 16, @@ -29,12 +41,6 @@ import { Image } from 'react-native'; overflow: 'hidden', }, ]} - source={[ - { - uri: 'logo.png', - width: 16, - height: 16, - }, - ]} + source={_imageSource} resizeMode="cover" />; diff --git a/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/index.test.ts b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/index.test.ts new file mode 100644 index 00000000..bbdf079d --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-image/__tests__/index.test.ts @@ -0,0 +1,784 @@ +import { createRequire } from 'node:module'; +import path from 'node:path'; +import { parseSync, transformSync, traverse, types as t, type TransformCaller } from '@babel/core'; +import { pluginTester } from 'babel-plugin-tester'; +import { describe, expect, it } from 'vitest'; +import { generateTestPlugin } from '../../../utils/generate-test-plugin'; +import { formatTestResult } from '../../../utils/format-test-result'; +import type { TargetPlatform } from '../../../types'; +import boostPlugin from '../../../index'; +import { nativeImageOptimizer } from '..'; + +const transformImage = async ( + source: string, + platform?: TargetPlatform, + { + unknownAncestorsDoNotRenderText = false, + unistylesEnabled = false, + reactNativeMinor = 86, + }: { + unknownAncestorsDoNotRenderText?: boolean; + unistylesEnabled?: boolean; + reactNativeMinor?: number | null; + } = {} +): Promise => { + const plugin = generateTestPlugin( + nativeImageOptimizer, + { + assumptions: { unknownAncestorsDoNotRenderText }, + integrations: { unistyles: unistylesEnabled ? 'on' : 'off' }, + }, + platform, + reactNativeMinor ?? undefined + ); + + return formatTestResult( + transformSync(source, { + configFile: false, + babelrc: false, + plugins: ['@babel/plugin-syntax-jsx', plugin], + })!.code! + ); +}; + +describe('Image load callbacks', () => { + for (const platform of ['ios', 'android'] as const) { + it.each([83, 84, 85, 86, 87])( + `forwards functions and matches notification defaults (${platform}, RN %s)`, + async (minor) => { + const output = await transformImage( + `import {Image, View} from 'react-native'; + const load = () => {}; + {}} onLoadStart={null} onLoadEnd={undefined} />;`, + platform, + { reactNativeMinor: minor } + ); + const [attributes] = getNativeImageAttributes(output); + expect(attributes).toBeDefined(); + const names = getAttributeNames(attributes); + expect(names.has('onLoad')).toBe(true); + expect(names.has('onError')).toBe(true); + expect(names.has('onLoadStart')).toBe(platform === 'ios' || minor <= 84); + expect(names.has('onLoadEnd')).toBe(platform === 'ios' || minor <= 84); + expect(names.has('shouldNotifyLoadEvents')).toBe(platform === 'android'); + if (platform === 'android') + expect(getAttributeExpression(attributes!, 'shouldNotifyLoadEvents')).toMatchObject({ value: true }); + } + ); + + it.each([ + 'handler', + 'props.onLoad', + 'makeHandler()', + 'enabled ? () => {} : null', + 'false', + '0', + '""', + '{}', + 'void 0', + ])('rejects unproved callback %s', async (value) => { + const output = await transformImage( + `import {Image, View} from 'react-native'; + function Case({handler, props, enabled}) { return ; }`, + platform + ); + expect(output).not.toContain('_NativeImage'); + }); + + it.each([ + 'ref={null}', + 'onProgress={() => {}}', + 'onPartialLoad={() => {}}', + '{...props}', + 'shouldNotifyLoadEvents={false}', + 'onLoad={null}', + 'testID={record()}', + ])('retains guards for %s', async (extra) => { + const output = await transformImage( + `import {Image, View} from 'react-native'; + {}} ${extra} />;`, + platform + ); + expect(output).not.toContain('_NativeImage'); + }); + + it.each(['IMAGE', 'inspect(IMAGE)', 'IMAGE'])( + 'rejects inspection or injection at %s even with the Text assumption', + async (body) => { + const image = ' {}} />'; + const output = await transformImage( + `import {Image} from 'react-native'; import Unknown from './unknown'; + function Case(){ return ${body.replace('IMAGE', image)}; }`, + platform, + { unknownAncestorsDoNotRenderText: true } + ); + expect(output).not.toContain('_NativeImage'); + } + ); + } + + it('keeps platform, version, binding, and Unistyles limits', async () => { + const source = `import {Image, View} from 'react-native'; {}} />;`; + for (const platform of ['web', undefined] as const) + expect(await transformImage(source, platform)).not.toContain('_NativeImage'); + expect(await transformImage(source, 'android', { reactNativeMinor: null })).not.toContain('_NativeImage'); + expect(await transformImage(source, 'ios', { unistylesEnabled: true })).toContain('_NativeImage'); + expect( + await transformImage(source.replace('onLoad=', 'style={styles.image} onLoad='), 'ios', { unistylesEnabled: true }) + ).not.toContain('_NativeImage'); + expect( + await transformImage( + `import {Image, View} from 'react-native'; function Case(undefined) { return ; }`, + 'ios' + ) + ).not.toContain('_NativeImage'); + expect( + await transformImage( + `import {Image, View} from 'react-native'; let load = () => {}; load = null; ;`, + 'ios' + ) + ).not.toContain('_NativeImage'); + }); + + it.each([83, 84, 85, 86, 87])('matches Android notification defaults without callbacks (RN %s)', async (minor) => { + const output = await transformImage( + `import {Image, View} from 'react-native'; ;`, + 'android', + { reactNativeMinor: minor } + ); + const [attributes] = getNativeImageAttributes(output); + expect(attributes).toBeDefined(); + expect(getAttributeNames(attributes).has('shouldNotifyLoadEvents')).toBe(minor <= 84); + if (minor <= 84) + expect(getAttributeExpression(attributes!, 'shouldNotifyLoadEvents')).toMatchObject({ value: false }); + }); + + it.each([83, 84, 85, 86, 87])('keeps Android nullish-only notification omission (RN %s)', async (minor) => { + const output = await transformImage( + `import {Image, View} from 'react-native'; + ;`, + 'android', + { reactNativeMinor: minor } + ); + const [attributes] = getNativeImageAttributes(output); + expect(attributes).toBeDefined(); + expect(getAttributeNames(attributes).has('shouldNotifyLoadEvents')).toBe(minor <= 84); + if (minor <= 84) + expect(getAttributeExpression(attributes!, 'shouldNotifyLoadEvents')).toMatchObject({ value: false }); + }); +}); + +const getNativeImageAttributes = (source: string): t.JSXAttribute[][] => { + const ast = parseSync(source, { + configFile: false, + babelrc: false, + parserOpts: { sourceType: 'module', plugins: ['jsx'] }, + }); + const images: t.JSXAttribute[][] = []; + + traverse(ast!, { + JSXOpeningElement(path) { + if (!t.isJSXIdentifier(path.node.name, { name: '_NativeImage' })) return; + images.push(path.node.attributes.filter((attribute): attribute is t.JSXAttribute => t.isJSXAttribute(attribute))); + }, + }); + + return images; +}; + +const getHoistedImageSources = (source: string): t.ArrayExpression[] => { + const ast = parseSync(source, { + configFile: false, + babelrc: false, + parserOpts: { sourceType: 'module', plugins: ['jsx'] }, + }); + const sources: t.ArrayExpression[] = []; + + traverse(ast!, { + VariableDeclarator(path) { + if ( + t.isIdentifier(path.node.id) && + path.node.id.name.startsWith('_imageSource') && + t.isArrayExpression(path.node.init) + ) { + sources.push(path.node.init); + } + }, + }); + + return sources; +}; + +const getSourceObjects = (source: t.ArrayExpression): t.ObjectExpression[] => + source.elements.map((element) => { + if (!t.isObjectExpression(element)) throw new Error('expected an Image source object'); + return element; + }); + +const getAttributeExpression = (attributes: t.JSXAttribute[], name: string): t.Expression | undefined => { + const attribute = attributes.find((item) => t.isJSXIdentifier(item.name, { name })); + if (!attribute?.value) return undefined; + if (t.isStringLiteral(attribute.value)) return attribute.value; + if (t.isJSXExpressionContainer(attribute.value) && t.isExpression(attribute.value.expression)) { + return attribute.value.expression; + } + return undefined; +}; + +const getAttributeNames = (attributes: t.JSXAttribute[]): Set => + new Set( + attributes.map((attribute) => + t.isJSXIdentifier(attribute.name) + ? attribute.name.name + : `${attribute.name.namespace.name}:${attribute.name.name.name}` + ) + ); + +const getObjectExpressionProperty = (object: t.ObjectExpression, name: string): t.Expression | undefined => { + const property = object.properties.find( + (item): item is t.ObjectProperty => + t.isObjectProperty(item) && + ((t.isIdentifier(item.key) && item.key.name === name) || + (t.isStringLiteral(item.key) && item.key.value === name)) && + t.isExpression(item.value) + ); + + return property?.value as t.Expression | undefined; +}; + +const getStringPropertyValue = (object: t.ObjectExpression, name: string): string | undefined => { + const value = getObjectExpressionProperty(object, name); + return t.isStringLiteral(value) ? value.value : undefined; +}; + +/** + * Asserts an expression is a call to the named runtime gate (the emitted identifier is uid-prefixed) + * and returns its single argument β€” the value the gate resolves at runtime. + */ +const unwrapRuntimeGate = (expression: t.Expression | undefined, helperName: string): t.Expression | undefined => { + expect(t.isCallExpression(expression)).toBe(true); + const call = expression as t.CallExpression; + expect(t.isIdentifier(call.callee)).toBe(true); + expect((call.callee as t.Identifier).name).toContain(helperName); + expect(call.arguments).toHaveLength(1); + return call.arguments[0] as t.Expression; +}; + +pluginTester({ + plugin: generateTestPlugin(nativeImageOptimizer, {}, 'ios'), + title: 'image', + fixtures: path.resolve(import.meta.dirname, 'fixtures'), + babelOptions: { + plugins: ['@babel/plugin-syntax-jsx'], + }, + formatResult: formatTestResult, +}); + +pluginTester({ + plugin: generateTestPlugin( + nativeImageOptimizer, + { assumptions: { unknownAncestorsDoNotRenderText: true } }, + 'android', + 84 + ), + title: 'legacy Android Image unknown ancestor assumption', + babelOptions: { + plugins: ['@babel/plugin-syntax-jsx'], + }, + formatResult: formatTestResult, + tests: [ + { + title: 'does not treat the Text assumption as proof against injected Image props', + fixture: path.resolve(import.meta.dirname, 'fixtures/unknown-imported-ancestor/code.js'), + outputFixture: path.resolve(import.meta.dirname, 'fixtures/unknown-imported-ancestor/output.js'), + }, + ], +}); + +describe('Image Text ancestry', () => { + const source = ` + import { Image, Text } from 'react-native'; + ; + `; + + it.each([ + ['ios', 83, true], + ['android', 84, false], + ['android', 85, true], + ] as const)('handles Text ancestry on %s with React Native 0.%i', async (platform, minor, optimizes) => { + const output = await transformImage(source, platform, { reactNativeMinor: minor }); + expect(output.includes('_NativeImage')).toBe(optimizes); + }); + + it('lets force override the legacy Android ancestry check', async () => { + const forcedSource = ` + import { Image, Text } from 'react-native'; + + {/* @boost-force */} + + ; + `; + expect(await transformImage(forcedSource, 'android', { reactNativeMinor: 84 })).toContain('_NativeImage'); + }); + + it.each([ + [84, false], + [85, true], + ] as const)('handles an unresolved runtime parent on Android 0.%i', async (minor, optimizes) => { + const rootSource = ` + import { Image } from 'react-native'; + const Avatar = () => ; + `; + const output = await transformImage(rootSource, 'android', { reactNativeMinor: minor }); + expect(output.includes('_NativeImage')).toBe(optimizes); + }); +}); + +pluginTester({ + plugin: generateTestPlugin(nativeImageOptimizer, {}, 'ios', 86), + title: 'image RN 0.86', + fixtures: path.resolve(import.meta.dirname, 'fixtures-rn-86'), + babelOptions: { + plugins: ['@babel/plugin-syntax-jsx'], + }, + formatResult: formatTestResult, +}); + +pluginTester({ + plugin: generateTestPlugin(nativeImageOptimizer, { integrations: { unistyles: 'on' } }, 'ios', 86), + title: 'image unistyles', + fixtures: path.resolve(import.meta.dirname, 'fixtures-unistyles'), + babelOptions: { + plugins: ['@babel/plugin-syntax-jsx'], + }, + formatResult: formatTestResult, +}); + +pluginTester({ + plugin: generateTestPlugin(nativeImageOptimizer, {}, undefined, 86), + title: 'image unknown platform', + fixtures: path.resolve(import.meta.dirname, 'fixtures-unknown-platform'), + babelOptions: { + plugins: ['@babel/plugin-syntax-jsx'], + }, + formatResult: formatTestResult, +}); + +describe('image android output', () => { + it('emits Android top-level empty headers for src sources', async () => { + const output = await transformImage( + ` + import { Image } from 'react-native'; + ; + `, + 'android' + ); + + const images = getNativeImageAttributes(output); + expect(images).toHaveLength(1); + const headers = getAttributeExpression(images[0]!, 'headers'); + + expect(t.isObjectExpression(headers)).toBe(true); + expect((headers as t.ObjectExpression).properties).toHaveLength(0); + }); + + it('emits Android source without the legacy src duplicate, plus top-level headers for request header props', async () => { + const output = await transformImage( + ` + import { Image } from 'react-native'; + ; + `, + 'android' + ); + + const images = getNativeImageAttributes(output); + expect(images).toHaveLength(1); + const image = images[0]!; + const [source] = getHoistedImageSources(output); + const headers = getAttributeExpression(image, 'headers'); + + expect(getAttributeNames(image).has('src')).toBe(false); + expect(getAttributeExpression(image, 'source')).toMatchObject({ name: '_imageSource' }); + expect(source).toBeDefined(); + expect(t.isObjectExpression(headers)).toBe(true); + expect(getStringPropertyValue(headers as t.ObjectExpression, 'Access-Control-Allow-Credentials')).toBe('true'); + expect(getStringPropertyValue(headers as t.ObjectExpression, 'Referrer-Policy')).toBe('origin'); + }); + + it.each([ + [84, true], + [85, false], + [87, true], + ])('resolves object-source header lifting for RN 0.%i at build time', async (reactNativeMinor, liftsHeaders) => { + const output = await transformImage( + ` + import { Image } from 'react-native'; + ; + ; + `, + 'android', + { reactNativeMinor } + ); + + expect(output).not.toContain('processImageObjectSourceHeaders'); + const images = getNativeImageAttributes(output); + const objectHeaders = getAttributeExpression(images[0]!, 'headers'); + if (liftsHeaders) { + expect(t.isObjectExpression(objectHeaders)).toBe(true); + expect(getStringPropertyValue(objectHeaders as t.ObjectExpression, 'Authorization')).toBe('Bearer object'); + } else { + expect(objectHeaders).toBeUndefined(); + } + + const arrayHeaders = getAttributeExpression(images[1]!, 'headers'); + expect(t.isObjectExpression(arrayHeaders)).toBe(true); + expect(getStringPropertyValue(arrayHeaders as t.ObjectExpression, 'Authorization')).toBe('Bearer first'); + }); + + it('keeps the object-source header runtime gate when the RN version is unknown', async () => { + const output = await transformImage( + `import { Image } from 'react-native'; + ;`, + 'android', + { reactNativeMinor: null } + ); + const headers = unwrapRuntimeGate( + getAttributeExpression(getNativeImageAttributes(output)[0]!, 'headers'), + 'processImageObjectSourceHeaders' + ); + + expect(t.isObjectExpression(headers)).toBe(true); + }); + + it('resolves single-entry array source dimensions at build time on Android only', async () => { + const source = ` + import { Image } from 'react-native'; + ; + ; + ; + `; + + const getStyleDimensionEntries = (image: t.JSXAttribute[]): t.ObjectExpression => { + const style = getAttributeExpression(image, 'style'); + expect(t.isArrayExpression(style)).toBe(true); + const dimensions = (style as t.ArrayExpression).elements[0]; + expect(t.isObjectExpression(dimensions)).toBe(true); + return dimensions as t.ObjectExpression; + }; + + const legacyOutput = await transformImage(source, 'android', { reactNativeMinor: 84 }); + expect(legacyOutput).not.toContain('processImageArraySourceDimensions'); + const legacyImages = getNativeImageAttributes(legacyOutput); + for (const image of legacyImages) { + expect(getStyleDimensionEntries(image).properties).toHaveLength(0); + } + + const modernOutput = await transformImage(source, 'android', { reactNativeMinor: 85 }); + expect(modernOutput).not.toContain('processImageArraySourceDimensions'); + const modernImages = getNativeImageAttributes(modernOutput); + for (const image of modernImages.slice(0, 2)) { + const dimensions = getStyleDimensionEntries(image); + expect(getObjectExpressionProperty(dimensions, 'width')).toMatchObject({ value: 16 }); + expect(getObjectExpressionProperty(dimensions, 'height')).toMatchObject({ value: 8 }); + } + expect(getStyleDimensionEntries(modernImages[2]!).properties).toHaveLength(0); + + const unknownOutput = await transformImage(source, 'android', { reactNativeMinor: null }); + expect(unknownOutput).toContain('processImageSourceProps'); + const unknownStyle = getAttributeExpression(getNativeImageAttributes(unknownOutput)[1]!, 'style'); + const unknownDimensions = unwrapRuntimeGate( + (unknownStyle as t.ArrayExpression).elements[0] as t.Expression, + 'processImageArraySourceDimensions' + ); + expect(t.isObjectExpression(unknownDimensions)).toBe(true); + + const iosImages = getNativeImageAttributes(await transformImage(source, 'ios')); + for (const image of iosImages) { + expect(getStyleDimensionEntries(image).properties).toHaveLength(0); + } + }); + + it('defers a src source with dynamic dimensions to the runtime helper on Android', async () => { + const source = ` + import { Image } from 'react-native'; + ; + `; + + const androidOutput = await transformImage(source, 'android'); + expect(androidOutput).toContain('processImageSourceProps'); + + expect(await transformImage(source, 'ios')).toContain('processImageSourceProps'); + const iosOutput = await transformImage(source, 'ios', { reactNativeMinor: 88 }); + expect(iosOutput).not.toContain('processImageSourceProps'); + expect(iosOutput).not.toContain('const _imageSource'); + expect(iosOutput).toContain('<_NativeImage'); + }); + + it('does not hoist style tintColor on Android', async () => { + const output = await transformImage( + ` + import { Image } from 'react-native'; + ; + `, + 'android' + ); + + const images = getNativeImageAttributes(output); + expect(images).toHaveLength(1); + expect(getAttributeNames(images[0]!).has('tintColor')).toBe(false); + }); +}); + +describe('image srcSet output', () => { + it('parses and hoists static density sources', async () => { + const output = await transformImage( + ` + import { Image } from 'react-native'; + ; + `, + 'ios' + ); + + expect(output).toContain('<_NativeImage'); + expect(output).not.toContain('processImageSourceProps'); + + const image = getNativeImageAttributes(output)[0]!; + expect(getAttributeNames(image).has('srcSet')).toBe(false); + expect(getAttributeExpression(image, 'source')).toMatchObject({ name: '_imageSource' }); + + const entries = getSourceObjects(getHoistedImageSources(output)[0]!); + expect(entries).toHaveLength(3); + expect(getStringPropertyValue(entries[0]!, 'uri')).toBe('logo.png'); + expect(getObjectExpressionProperty(entries[0]!, 'scale')).toMatchObject({ value: 1 }); + expect(getStringPropertyValue(entries[1]!, 'uri')).toBe('logo@2x.png'); + expect(getObjectExpressionProperty(entries[1]!, 'scale')).toMatchObject({ value: 2 }); + expect(getStringPropertyValue(entries[2]!, 'uri')).toBe('logo@3x.png'); + expect(getObjectExpressionProperty(entries[2]!, 'scale')).toMatchObject({ value: 3 }); + }); + + it('matches the scale parser used by the target React Native version', async () => { + const source = ` + import { Image } from 'react-native'; + ; + `; + const legacyEntries = getSourceObjects( + getHoistedImageSources(await transformImage(source, 'ios', { reactNativeMinor: 86 }))[0]! + ); + const modernEntries = getSourceObjects( + getHoistedImageSources(await transformImage(source, 'ios', { reactNativeMinor: 87 }))[0]! + ); + + expect(legacyEntries.map((entry) => getObjectExpressionProperty(entry, 'scale'))).toMatchObject([{ value: 1 }]); + expect(modernEntries.map((entry) => getObjectExpressionProperty(entry, 'scale'))).toMatchObject([ + { value: 1.5 }, + { value: 1 }, + ]); + }); + + it('uses the relaxed RN 0.87 separator parser only for RN 0.87', async () => { + const source = ` + import { Image } from 'react-native'; + ; + `; + const legacyOutput = await transformImage(source, 'ios', { reactNativeMinor: 86 }); + const modernOutput = await transformImage(source, 'ios', { reactNativeMinor: 87 }); + + expect(legacyOutput).not.toContain('_NativeImage'); + expect(getSourceObjects(getHoistedImageSources(modernOutput)[0]!)).toHaveLength(2); + }); + + it('bails when no React Native version was identified', async () => { + const output = await transformImage( + `import { Image } from 'react-native';\n;`, + 'ios', + { + reactNativeMinor: null, + } + ); + + expect(output).not.toContain('_NativeImage'); + }); + + it('uses an explicit React Native package from the Babel plugin options', async () => { + const output = await formatTestResult( + transformSync(`import { Image } from 'react-native';\n;`, { + configFile: false, + babelrc: false, + filename: 'case.jsx', + caller: { name: 'metro', platform: 'ios' } as TransformCaller, + plugins: [ + '@babel/plugin-syntax-jsx', + [ + boostPlugin, + { + logLevel: 'silent', + target: { + reactNative: { packageJson: createRequire(import.meta.url).resolve('react-native/package.json') }, + }, + }, + ], + ], + })!.code! + ); + + expect(getSourceObjects(getHoistedImageSources(output)[0]!)).toHaveLength(2); + }); + + it('detects the installed React Native version through the Babel plugin', async () => { + const output = await formatTestResult( + transformSync(`import { Image } from 'react-native';\n;`, { + configFile: false, + babelrc: false, + filename: 'case.jsx', + caller: { name: 'metro', platform: 'ios' } as TransformCaller, + plugins: ['@babel/plugin-syntax-jsx', [boostPlugin, { logLevel: 'silent' }]], + })!.code! + ); + const { version } = createRequire(import.meta.url)('react-native/package.json') as { version: string }; + const minor = Number(version.split('.')[1]); + + if (minor >= 83) { + const entry = getSourceObjects(getHoistedImageSources(output)[0]!)[0]!; + expect(getObjectExpressionProperty(entry, 'scale')).toMatchObject({ value: minor >= 87 ? 1.5 : 1 }); + } else { + expect(output).not.toContain('_NativeImage'); + } + }); + + it('uses src only when srcSet has no 1x source', async () => { + const output = await transformImage( + ` + import { Image } from 'react-native'; + ; + ; + `, + 'ios' + ); + + const [withFallback, withDefault] = getHoistedImageSources(output).map((source) => getSourceObjects(source)); + expect(withFallback!.map((entry) => getStringPropertyValue(entry, 'uri'))).toEqual([ + 'logo@2x.png', + 'logo@3x.png', + 'fallback.png', + ]); + expect(withFallback!.map((entry) => getObjectExpressionProperty(entry, 'scale'))).toMatchObject([ + { value: 2 }, + { value: 3 }, + { value: 1 }, + ]); + expect(withDefault!.map((entry) => getStringPropertyValue(entry, 'uri'))).toEqual(['logo.png', 'logo@2x.png']); + }); + + it('builds static dimensions and request headers', async () => { + const output = await transformImage( + ` + import { Image } from 'react-native'; + ; + `, + 'android' + ); + + const image = getNativeImageAttributes(output)[0]!; + const entry = getSourceObjects(getHoistedImageSources(output)[0]!)[0]!; + const headers = getAttributeExpression(image, 'headers'); + expect(t.isObjectExpression(headers)).toBe(true); + expect(getStringPropertyValue(headers as t.ObjectExpression, 'Access-Control-Allow-Credentials')).toBe('true'); + expect(getStringPropertyValue(headers as t.ObjectExpression, 'Referrer-Policy')).toBe('origin'); + expect(getObjectExpressionProperty(entry, 'width')).toMatchObject({ value: 24 }); + expect(getObjectExpressionProperty(entry, 'height')).toMatchObject({ value: 12 }); + + expect(output).not.toContain('processImageArraySourceDimensions'); + const style = getAttributeExpression(image, 'style') as t.ArrayExpression; + const dimensions = style.elements[0] as t.ObjectExpression; + expect(getObjectExpressionProperty(dimensions, 'width')).toMatchObject({ value: 24 }); + expect(getObjectExpressionProperty(dimensions, 'height')).toMatchObject({ value: 12 }); + }); +}); + +const imageSource = (value: string) => ` + import { Image } from 'react-native'; + ; + `; + +describe('image nullable accessible', () => { + it.each(['maybeAccessible'])('routes accessible={%s} through the runtime helper on Android', async (value) => { + const output = await transformImage(imageSource(value), 'android'); + + expect(output).toContain('processImageAccessibilityProps'); + expect(getAttributeNames(getNativeImageAttributes(output)[0]).has('accessible')).toBe(false); + }); + + it('omits a static null accessible without a helper on Android', async () => { + const output = await transformImage(imageSource('null'), 'android'); + expect(output).not.toContain('processImageAccessibilityProps'); + expect(getAttributeNames(getNativeImageAttributes(output)[0]).has('accessible')).toBe(false); + }); + + it('keeps a provably non-nullish accessible inline on Android', async () => { + const output = await transformImage(imageSource('false'), 'android'); + + expect(output).not.toContain('processImageAccessibilityProps'); + expect(getAttributeNames(getNativeImageAttributes(output)[0]).has('accessible')).toBe(true); + }); + + it('passes accessible straight through on iOS, which forwards it unchanged', async () => { + const output = await transformImage(imageSource('maybeAccessible'), 'ios'); + + expect(output).not.toContain('processImageAccessibilityProps'); + expect(getAttributeNames(getNativeImageAttributes(output)[0]).has('accessible')).toBe(true); + }); +}); + +describe('image consumed props', () => { + it.each(['ios', 'android'] as const)( + 'removes wrapper-consumed size and request props from optimized %s output', + async (platform) => { + const output = await transformImage( + ` + import { Image } from 'react-native'; + ; + ; + `, + platform + ); + + const images = getNativeImageAttributes(output); + expect(images).toHaveLength(2); + + for (const image of images) { + const names = getAttributeNames(image); + expect(names.has('width')).toBe(platform === 'ios'); + expect(names.has('height')).toBe(platform === 'ios'); + expect(names.has('crossOrigin')).toBe(false); + expect(names.has('referrerPolicy')).toBe(false); + } + } + ); +}); diff --git a/packages/react-native-boost/src/plugin/optimizers/image/index.ts b/packages/react-native-boost/src/plugin/optimizers/native-image/index.ts similarity index 59% rename from packages/react-native-boost/src/plugin/optimizers/image/index.ts rename to packages/react-native-boost/src/plugin/optimizers/native-image/index.ts index 75f2ea95..b40adf8d 100644 --- a/packages/react-native-boost/src/plugin/optimizers/image/index.ts +++ b/packages/react-native-boost/src/plugin/optimizers/native-image/index.ts @@ -1,5 +1,5 @@ import { NodePath, types as t } from '@babel/core'; -import { HubFile, Optimizer } from '../../types'; +import type { HubFile, JSXOptimizer } from '../../types'; import PluginError from '../../utils/plugin-error'; import { BailoutCheck, getFirstBailoutReason } from '../../utils/helpers'; import { @@ -15,8 +15,13 @@ import { replaceWithNativeComponent, ancestorBailoutChecks, createStyleOriginResolver, + createTextContextSourceResolver, } from '../../utils/common'; import { RUNTIME_MODULE_NAME } from '../../utils/constants'; +import { createJSXOptimizer } from '../../utils/optimizer'; +import { foldAccessibility } from '../../utils/static-accessibility'; + +const IMAGE_LOAD_CALLBACKS = new Set(['onLoadStart', 'onLoad', 'onLoadEnd', 'onError']); const IMAGE_BAILOUT_PROPS = new Set([ 'aria-live', @@ -29,14 +34,9 @@ const IMAGE_BAILOUT_PROPS = new Set([ 'id', 'internal_analyticTag', 'loadingIndicatorSource', - 'onError', - 'onLoad', - 'onLoadEnd', - 'onLoadStart', 'onPartialLoad', 'onProgress', 'ref', - 'srcSet', 'tabIndex', ]); @@ -52,6 +52,7 @@ const IMAGE_REQUEST_HEADER_PROPS = new Set(['crossOrigin', 'referrerPolicy']); const IMAGE_SPREAD_GUARD_PROPS = new Set([ ...IMAGE_BAILOUT_PROPS, + ...IMAGE_LOAD_CALLBACKS, ...IMAGE_REQUEST_HEADER_PROPS, ...IMAGE_ARIA_STATE_PROPS, 'accessible', @@ -67,6 +68,7 @@ const IMAGE_SPREAD_GUARD_PROPS = new Set([ 'resizeMode', 'source', 'src', + 'srcSet', 'style', 'tintColor', 'width', @@ -82,26 +84,68 @@ const OBJECT_FIT_TO_RESIZE_MODE: Record = { 'scale-down': 'contain', }; -export const imageOptimizer: Optimizer = (path, logger, options, platform, unistylesEnabled) => { +const optimizeNativeImage: JSXOptimizer = (path, { logger, options, platform, unistylesEnabled, reactNativeMinor }) => { if (platform === 'web') return; if (!isReactNativeComponent(path, 'Image')) return; + if (platform !== 'ios' && platform !== 'android') { + logger.skipped({ target: 'Image', path, reason: 'target platform is unknown' }); + return; + } + const parent = path.parent as t.JSXElement; const forced = isForcedLine(path); + const loadCallbacks = path.node.attributes.filter( + (attribute): attribute is t.JSXAttribute => + t.isJSXAttribute(attribute) && t.isJSXIdentifier(attribute.name) && IMAGE_LOAD_CALLBACKS.has(attribute.name.name) + ); + const callbackKinds = loadCallbacks.map((attribute) => + classifyLoadCallback(path, getAttributeValueExpression(attribute)) + ); + const getAncestor = createTextContextSourceResolver(path); + + // Text-context assumptions do not prove that a parent cannot inspect or inject callbacks. + if ( + !forced && + loadCallbacks.length > 0 && + (getAncestor() !== 'safe' || + callbackKinds.includes(undefined) || + (platform === 'android' && reactNativeMinor === undefined) || + (options?.integrations?.uniwind === 'on' && callbackKinds.includes('nullish')) || + findAttribute(path.node.attributes, 'shouldNotifyLoadEvents') || + path.node.attributes.some( + (attribute) => + t.isJSXSpreadAttribute(attribute) || + (t.isJSXAttribute(attribute) && + !loadCallbacks.includes(attribute) && + t.isJSXExpressionContainer(attribute.value) && + !path.scope.isPure(attribute.value.expression)) + ) || + getDirectAttributeNames(path.node.attributes).size !== path.node.attributes.length) + ) { + logger.skipped({ + target: 'Image', + path, + reason: 'load callbacks require proved values, evaluation, and parent safety', + }); + return; + } // In Unistyles mode, classify the direct `style` origin (lazily, once). A `style` carried by a // resolvable spread already bails (`style` is in {@link IMAGE_SPREAD_GUARD_PROPS}), as does an // unresolvable spread. See {@link classifyStyleOrigin}. const getStyleOrigin = createStyleOriginResolver(path, unistylesEnabled); + const srcSetAttribute = findAttribute(path.node.attributes, 'srcSet'); + const staticSrcSetSource = srcSetAttribute + ? buildStaticSrcSetSource(path.node.attributes, platform, reactNativeMinor) + : undefined; - const hardChecks: BailoutCheck[] = [ + const bailoutChecks: BailoutCheck[] = [ { - reason: 'target platform is unknown', - shouldBail: () => platform !== 'ios' && platform !== 'android', + reason: 'has an unresolved ancestor that may inspect or change children', + shouldBail: () => getAncestor() === 'unknown', }, { - // Unlike Text/View, a provably Unistyles-styled Image cannot be routed: Unistyles ships no lean - // Image host, so optimizing it would drop the shadow-tree registration and freeze theme updates. reason: 'has a Unistyles style and there is no lean Image host to route to', shouldBail: () => getStyleOrigin() === 'unistyles', }, @@ -117,30 +161,32 @@ export const imageOptimizer: Optimizer = (path, logger, options, platform, unist reason: 'contains non-empty children', shouldBail: () => parent.children.some((child) => !t.isJSXText(child) || child.value.trim() !== ''), }, + { + reason: 'has an unsupported or dynamic srcSet', + shouldBail: () => srcSetAttribute !== undefined && staticSrcSetSource === undefined, + }, + { + reason: 'has a dynamic style with srcSet', + shouldBail: () => srcSetAttribute !== undefined && buildStaticStyleInfo(path.node.attributes) === null, + }, { reason: 'has an unsupported or dynamic source', shouldBail: () => !hasImageSourceInput(path.node.attributes), }, - ]; - - const overridableChecks: BailoutCheck[] = [ { reason: 'has an unresolved style source that may be a Unistyles style', shouldBail: () => getStyleOrigin() === 'unknown', }, - ...ancestorBailoutChecks(path, options?.dangerouslyOptimizeImageWithUnknownAncestors === true), + // Android through 0.84 selects its inline Image host from the inherited Text context. + ...(platform === 'android' && (reactNativeMinor === undefined || reactNativeMinor <= 84) + ? ancestorBailoutChecks(path, options?.assumptions?.unknownAncestorsDoNotRenderText === true) + : []), ]; - const hardSkipReason = getFirstBailoutReason(hardChecks); - if (hardSkipReason) { - logger.skipped({ component: 'Image', path, reason: hardSkipReason }); - return; - } - if (forced) { - const overriddenReason = getFirstBailoutReason(overridableChecks); + const overriddenReason = getFirstBailoutReason(bailoutChecks); if (overriddenReason) { - logger.forced({ component: 'Image', path, reason: overriddenReason }); + logger.forced({ target: 'Image', path, reason: overriddenReason }); } } else { const skipReason = getFirstBailoutReason([ @@ -148,11 +194,11 @@ export const imageOptimizer: Optimizer = (path, logger, options, platform, unist reason: 'line is marked with @boost-ignore', shouldBail: () => isIgnoredLine(path), }, - ...overridableChecks, + ...bailoutChecks, ]); if (skipReason) { - logger.skipped({ component: 'Image', path, reason: skipReason }); + logger.skipped({ target: 'Image', path, reason: skipReason }); return; } } @@ -164,38 +210,75 @@ export const imageOptimizer: Optimizer = (path, logger, options, platform, unist throw new PluginError('No file found in Babel hub'); } - const nativeSource = buildStaticNativeSource(path.node.attributes, platform); + if (options?.integrations?.uniwind === 'on') { + if (srcSetAttribute) { + logger.skipped({ target: 'Image', path, reason: 'Uniwind srcSet requires the Image wrapper' }); + return; + } + logger.optimized({ target: 'Image', path }); + replaceWithNativeComponent(path, parent, file, 'NativeImage', { moduleName: 'react-native-boost/uniwind' }); + return; + } + + const nativeSource = staticSrcSetSource ?? buildStaticNativeSource(path.node.attributes, platform, reactNativeMinor); const styleInfo = buildStaticStyleInfo(path.node.attributes); - logger.optimized({ component: 'Image', path }); + logger.optimized({ target: 'Image', path }); if (nativeSource && styleInfo !== null) { - processImageProps(path, file, nativeSource, styleInfo, platform); + processImageProps(path, file, nativeSource, styleInfo, platform, reactNativeMinor); } else { - processRuntimeImageProps(path, file, platform); + processRuntimeImageProps(path, file, platform, reactNativeMinor); + } + if ( + platform === 'android' && + (loadCallbacks.length > 0 || (reactNativeMinor !== undefined && reactNativeMinor <= 84)) + ) { + const legacy = reactNativeMinor !== undefined && reactNativeMinor <= 84; + if (!legacy) { + path.node.attributes = path.node.attributes.filter((attribute) => { + const index = t.isJSXAttribute(attribute) ? loadCallbacks.indexOf(attribute) : -1; + return index < 0 || callbackKinds[index] !== 'nullish'; + }); + } + if (legacy || callbackKinds.includes('function')) { + path.node.attributes.push( + makeAttribute('shouldNotifyLoadEvents', t.booleanLiteral(callbackKinds.includes('function'))) + ); + } } replaceWithNativeComponent(path, parent, file, 'NativeImage'); }; +function classifyLoadCallback( + path: NodePath, + expression: t.Expression +): 'function' | 'nullish' | undefined { + if (t.isNullLiteral(expression)) return 'nullish'; + if (t.isArrowFunctionExpression(expression) || t.isFunctionExpression(expression)) return 'function'; + if (!t.isIdentifier(expression)) return; + const binding = path.scope.getBinding(expression.name); + if (expression.name === 'undefined' && !binding) return 'nullish'; + if (!binding?.constant) return; + if (binding.path.isFunctionDeclaration()) return 'function'; + if (binding.kind === 'const' && binding.path.isVariableDeclarator()) { + const initializer = binding.path.node.init; + if (t.isArrowFunctionExpression(initializer) || t.isFunctionExpression(initializer)) return 'function'; + } +} + +export const nativeImageOptimizer = createJSXOptimizer('native-image', optimizeNativeImage); + type NativeSource = { sourceAttributes: t.JSXAttribute[]; requestHeaderAttributes: t.JSXAttribute[]; sourceArray: t.ArrayExpression; - consumesSizeProps: boolean; androidHeaders?: t.Expression; width?: t.Expression; height?: t.Expression; - /** - * `width`/`height` came from an ARRAY source's single entry, which only the wrapper of some RN - * versions propagates into the layout style β€” so they are emitted through the runtime gate - * `processImageArraySourceDimensions` instead of inline. - */ + /** `width`/`height` came from an ARRAY source whose dimension propagation varies by RN version. */ arraySourceDimensions?: boolean; - /** - * `androidHeaders` came from a plain OBJECT source's inline `headers`, which only the wrapper of - * some RN versions lifts to the top-level prop β€” so they are emitted through the runtime gate - * `processImageObjectSourceHeaders` instead of inline. - */ + /** `androidHeaders` came from a plain OBJECT source whose header lift varies by RN version. */ objectSourceHeaders?: boolean; }; @@ -209,7 +292,7 @@ type StyleInfo = { type ImageAccessibilityInfo = { attributes: t.JSXAttribute[]; - spreadAttribute: t.JSXSpreadAttribute; + replacements: (t.JSXAttribute | t.JSXSpreadAttribute)[]; }; type RuntimeImageInfo = { @@ -222,9 +305,10 @@ function processImageProps( file: HubFile, nativeSource: NativeSource, styleInfo: StyleInfo, - platform?: string + platform?: string, + reactNativeMinor?: number ) { - const accessibilityInfo = buildImageAccessibilityInfo(path, file, platform); + const accessibilityInfo = buildImageAccessibilityInfo(path, file, platform, reactNativeMinor); const consumed = new Set([ ...nativeSource.sourceAttributes, ...nativeSource.requestHeaderAttributes, @@ -236,7 +320,8 @@ function processImageProps( if (!t.isJSXAttribute(attribute)) return true; if (consumed.has(attribute)) return false; const name = attribute.name.name; - if (nativeSource.consumesSizeProps && (name === 'width' || name === 'height')) return false; + if (name === 'width' || name === 'height') + return reactNativeMinor !== undefined && reactNativeMinor < (platform === 'ios' ? 88 : 85); return name !== 'resizeMode' && name !== 'tintColor'; }); @@ -245,44 +330,85 @@ function processImageProps( const tintColor = buildTintColor(explicitTintColor, styleInfo?.tintColor, platform); const emitsAndroidProps = platform === 'android'; - // Both gates are Android-only and are wired up only when there is something to gate, so an Image - // that hits neither case keeps a fully static prop bag. - const gatesArrayDimensions = - emitsAndroidProps && + const hasArrayDimensions = nativeSource.arraySourceDimensions === true && (nativeSource.width !== undefined || nativeSource.height !== undefined); - const arrayDimensionsGate = gatesArrayDimensions + const resolvesArrayDimensionsAtRuntime = emitsAndroidProps && hasArrayDimensions && reactNativeMinor === undefined; + const includesDimensions = + nativeSource.arraySourceDimensions !== true || reactNativeMinor === undefined || reactNativeMinor >= 85; + const arrayDimensionsGate = resolvesArrayDimensionsAtRuntime ? addRuntimeHelper(path, file, 'processImageArraySourceDimensions') : undefined; - const androidHeaders = - emitsAndroidProps && nativeSource.androidHeaders - ? nativeSource.objectSourceHeaders - ? t.callExpression(addRuntimeHelper(path, file, 'processImageObjectSourceHeaders'), [ - t.cloneNode(nativeSource.androidHeaders, true), - ]) - : t.cloneNode(nativeSource.androidHeaders, true) - : undefined; + let androidHeaders: t.Expression | undefined; + if (emitsAndroidProps && nativeSource.androidHeaders) { + if (!nativeSource.objectSourceHeaders) { + androidHeaders = t.cloneNode(nativeSource.androidHeaders, true); + } else if (reactNativeMinor === undefined) { + androidHeaders = t.callExpression(addRuntimeHelper(path, file, 'processImageObjectSourceHeaders'), [ + t.cloneNode(nativeSource.androidHeaders, true), + ]); + } else if (reactNativeMinor <= 84 || reactNativeMinor >= 87) { + androidHeaders = t.cloneNode(nativeSource.androidHeaders, true); + } + } + + if ( + t.isNullLiteral(androidHeaders) && + (!nativeSource.objectSourceHeaders || (reactNativeMinor !== undefined && reactNativeMinor >= 85)) + ) + androidHeaders = undefined; path.node.attributes = [ ...remaining, - accessibilityInfo?.spreadAttribute, - makeAttribute('style', buildStyle(nativeSource, styleInfo, arrayDimensionsGate)), - makeAttribute('source', nativeSource.sourceArray), + ...(accessibilityInfo?.replacements ?? []), + makeAttribute('style', buildStyle(nativeSource, styleInfo, includesDimensions, arrayDimensionsGate)), + makeAttribute('source', hoistStaticImageSource(path, file, nativeSource.sourceArray)), androidHeaders ? makeAttribute('headers', androidHeaders) : undefined, makeAttribute('resizeMode', buildResizeMode(explicitResizeMode, styleInfo)), tintColor ? makeAttribute('tintColor', tintColor) : undefined, ].filter((attribute): attribute is t.JSXAttribute | t.JSXSpreadAttribute => attribute !== undefined); } +function hoistStaticImageSource( + path: NodePath, + file: HubFile, + source: t.ArrayExpression +): t.Expression { + if (!isStaticLiteralTree(source)) return source; + + const identifier = path.scope.getProgramParent().generateUidIdentifier('imageSource'); + const declarator = t.variableDeclarator(identifier, source); + + if (file.__staticImageSourceDeclaration) { + file.__staticImageSourceDeclaration.declarations.push(declarator); + } else { + const programPath = path.findParent((ancestor) => ancestor.isProgram()); + if (!programPath?.isProgram()) throw new PluginError('No program found for static Image source'); + + const declaration = t.variableDeclaration('const', [declarator]); + const firstStatement = programPath.get('body').find((statement) => !statement.isImportDeclaration()); + if (firstStatement) firstStatement.insertBefore(declaration); + else programPath.pushContainer('body', declaration); + file.__staticImageSourceDeclaration = declaration; + } + + return t.cloneNode(identifier); +} + function addRuntimeHelper(path: NodePath, file: HubFile, importName: string): t.Identifier { return t.identifier( addFileImportHint({ file, nameHint: importName, path, importName, moduleName: RUNTIME_MODULE_NAME }).name ); } -function processRuntimeImageProps(path: NodePath, file: HubFile, platform?: string) { - const accessibilityInfo = buildImageAccessibilityInfo(path, file, platform); +function processRuntimeImageProps( + path: NodePath, + file: HubFile, + platform?: string, + reactNativeMinor?: number +) { + const accessibilityInfo = buildImageAccessibilityInfo(path, file, platform, reactNativeMinor); const runtimeInfo = buildRuntimeImageInfo(path, file); if (!runtimeInfo) return; @@ -292,7 +418,7 @@ function processRuntimeImageProps(path: NodePath, file: Hub (attribute) => !t.isJSXAttribute(attribute) || !consumed.has(attribute) ); - path.node.attributes = [...remaining, accessibilityInfo?.spreadAttribute, runtimeInfo.spreadAttribute].filter( + path.node.attributes = [...remaining, ...(accessibilityInfo?.replacements ?? []), runtimeInfo.spreadAttribute].filter( (attribute): attribute is t.JSXAttribute | t.JSXSpreadAttribute => attribute !== undefined ); } @@ -300,19 +426,44 @@ function processRuntimeImageProps(path: NodePath, file: Hub function buildImageAccessibilityInfo( path: NodePath, file: HubFile, - platform?: string + platform?: string, + reactNativeMinor?: number ): ImageAccessibilityInfo | undefined { + const groupNames = new Set([ + ...IMAGE_ARIA_STATE_PROPS, + 'alt', + 'accessible', + 'accessibilityLabel', + 'accessibilityLabelledBy', + 'accessibilityState', + 'importantForAccessibility', + 'aria-hidden', + 'aria-label', + 'aria-labelledby', + ]); + const group = path.node.attributes.filter( + (attribute): attribute is t.JSXAttribute => + t.isJSXAttribute(attribute) && t.isJSXIdentifier(attribute.name) && groupNames.has(attribute.name.name) + ); + if (group.length > 0) { + const folded = foldAccessibility(path, group, 'Image', platform, reactNativeMinor); + if (folded) return { attributes: group, replacements: folded }; + } const directNames = getDirectAttributeNames(path.node.attributes); const hasAlt = directNames.has('alt'); const hasLabelTrigger = hasAlt || directNames.has('aria-label'); const hasHiddenTrigger = directNames.has('aria-hidden'); const hasLabelledByTrigger = directNames.has('aria-labelledby'); - const hasStateTrigger = [...IMAGE_ARIA_STATE_PROPS].some((name) => directNames.has(name)); - // Android drops a nullish `accessible`, so passing the authored value through would emit an - // `accessible={null}` the wrapper never sets. iOS forwards it unchanged, so only Android needs the helper. + const hasStateTrigger = + [...IMAGE_ARIA_STATE_PROPS].some((name) => directNames.has(name)) || + ((reactNativeMinor === undefined || reactNativeMinor >= 88) && directNames.has('accessibilityState')); + // Android 0.85–0.87 drops nullish `accessible`; keep the runtime gate for unknown targets. const accessible = getAttributeExpression(path.node.attributes, 'accessible'); const hasNullableAccessible = - platform === 'android' && accessible !== undefined && !isStaticNonNullishExpression(accessible); + platform === 'android' && + (reactNativeMinor === undefined || reactNativeMinor < 88) && + accessible !== undefined && + !isStaticNonNullishExpression(accessible); if (!hasLabelTrigger && !hasHiddenTrigger && !hasLabelledByTrigger && !hasStateTrigger && !hasNullableAccessible) { return undefined; @@ -359,9 +510,11 @@ function buildImageAccessibilityInfo( return { attributes, - spreadAttribute: t.jsxSpreadAttribute( - t.callExpression(t.identifier(helperIdentifier.name), [buildPropertiesFromAttributes(attributes)]) - ), + replacements: [ + t.jsxSpreadAttribute( + t.callExpression(t.identifier(helperIdentifier.name), [buildPropertiesFromAttributes(attributes)]) + ), + ], }; } @@ -396,18 +549,132 @@ function buildRuntimeImageInfo(path: NodePath, file: HubFil }; } +type StaticSrcSetEntry = { + uri: string; + scale: number; +}; + +function buildStaticSrcSetSource( + attributes: Array, + platform?: string, + reactNativeMinor?: number +): NativeSource | undefined { + if (attributes.some((attribute) => t.isJSXSpreadAttribute(attribute))) return undefined; + if (findAttribute(attributes, 'source')) return undefined; + + const srcSetAttribute = findAttribute(attributes, 'srcSet'); + if (!srcSetAttribute) return undefined; + + const srcSet = getAttributeValueExpression(srcSetAttribute); + if (!t.isStringLiteral(srcSet)) return undefined; + + const entries = parseStaticSrcSet(srcSet.value, reactNativeMinor); + if (!entries) return undefined; + + const srcAttribute = findAttribute(attributes, 'src'); + let src: t.StringLiteral | undefined; + if (srcAttribute) { + const value = getAttributeValueExpression(srcAttribute); + if (!t.isStringLiteral(value)) return undefined; + src = value; + } + + const width = getAttributeExpression(attributes, 'width'); + const height = getAttributeExpression(attributes, 'height'); + if (width && !isStaticSrcSetDimension(width)) return undefined; + if (height && !isStaticSrcSetDimension(height)) return undefined; + + const requestHeaders = buildRequestHeaders(attributes); + if (!requestHeaders) return undefined; + + if (src && !entries.some((entry) => entry.scale === 1)) { + entries.push({ uri: src.value, scale: 1 }); + } + + const sourceArray = t.arrayExpression( + entries.map((entry) => + t.objectExpression([ + t.objectProperty(t.identifier('headers'), t.cloneNode(requestHeaders.headers, true)), + t.objectProperty(t.identifier('scale'), t.numericLiteral(entry.scale)), + t.objectProperty(t.identifier('uri'), t.stringLiteral(entry.uri)), + ...(width ? [t.objectProperty(t.identifier('width'), t.cloneNode(width, true))] : []), + ...(height ? [t.objectProperty(t.identifier('height'), t.cloneNode(height, true))] : []), + ]) + ) + ); + const emitsDimensions = platform === 'android' && entries.length === 1; + + return { + sourceAttributes: [srcSetAttribute, srcAttribute].filter( + (attribute): attribute is t.JSXAttribute => attribute !== undefined + ), + requestHeaderAttributes: requestHeaders.attributes, + sourceArray, + androidHeaders: t.cloneNode(requestHeaders.headers, true), + width: emitsDimensions && width ? t.cloneNode(width, true) : undefined, + height: emitsDimensions && height ? t.cloneNode(height, true) : undefined, + arraySourceDimensions: true, + }; +} + +function parseStaticSrcSet(value: string, reactNativeMinor?: number): StaticSrcSetEntry[] | undefined { + if (reactNativeMinor === undefined || reactNativeMinor < 83) return undefined; + + const modernParser = reactNativeMinor >= 87; + const sources = modernParser + ? value + .split(',') + .map((source) => source.trim()) + .filter(Boolean) + : value.split(', '); + const entries: StaticSrcSetEntry[] = []; + + for (const source of sources) { + const parts = source.split(modernParser ? /\s+/ : ' '); + if (parts.length > 2) return undefined; + + const [uri, descriptor = '1x'] = parts; + if (!uri || !/^(?:\d+(?:\.\d+)?|\.\d+)x$/.test(descriptor)) return undefined; + + const scale = modernParser + ? Number.parseFloat(descriptor.slice(0, -1)) + : Number.parseInt(descriptor.split('x')[0]!, 10); + if (!Number.isFinite(scale) || scale <= 0) return undefined; + entries.push({ uri, scale }); + } + + return entries.length > 0 ? entries : undefined; +} + +function isStaticSrcSetDimension(expression: t.Expression): boolean { + return ( + t.isNumericLiteral(expression) || + t.isNullLiteral(expression) || + (t.isUnaryExpression(expression, { operator: '-' }) && t.isNumericLiteral(expression.argument)) + ); +} + function buildStaticNativeSource( attributes: Array, - platform?: string + platform?: string, + reactNativeMinor?: number ): NativeSource | undefined { + // Unknown targets need a runtime gate; dynamic dimensions must not be evaluated twice. + for (const name of ['width', 'height']) { + const value = getAttributeExpression(attributes, name); + if ( + value && + (reactNativeMinor === undefined || + ((platform === 'android' || reactNativeMinor < 88) && !isStaticLiteralTree(value))) + ) + return undefined; + } const requestHeaders = buildRequestHeaders(attributes); if (!requestHeaders) return undefined; // Two Android-only wrapper behaviors vary across the supported RN range: propagating a single-entry // ARRAY source's intrinsic dimensions into the layout style, and lifting a plain OBJECT source's - // inline headers to the top-level `headers` prop. iOS does neither on any version. Both are emitted - // through runtime gates (see the `arraySourceDimensions` / `objectSourceHeaders` flags) so the - // build-time output tracks the installed RN exactly instead of picking one version's semantics. + // inline headers to the top-level `headers` prop. The runtime gates remain only as an unknown-target fallback. const emitsAndroidProps = platform === 'android'; const src = findAttribute(attributes, 'src'); @@ -417,12 +684,6 @@ function buildStaticNativeSource( const source = findAttribute(attributes, 'source'); const width = getAttributeExpression(attributes, 'width'); const height = getAttributeExpression(attributes, 'height'); - // On Android the dimensions are emitted twice (source entry AND style), so a non-literal - // expression would be evaluated twice; defer those to the runtime helper instead. - if (emitsAndroidProps) { - if (width && !isStaticLiteralTree(width)) return undefined; - if (height && !isStaticLiteralTree(height)) return undefined; - } const headers = t.cloneNode(requestHeaders.headers, true); return { sourceAttributes: [src, source].filter((attribute): attribute is t.JSXAttribute => attribute !== undefined), @@ -435,7 +696,6 @@ function buildStaticNativeSource( ...(height ? [t.objectProperty(t.identifier('height'), height)] : []), ]), ]), - consumesSizeProps: true, androidHeaders: t.cloneNode(requestHeaders.headers, true), width: emitsAndroidProps && width ? t.cloneNode(width, true) : undefined, height: emitsAndroidProps && height ? t.cloneNode(height, true) : undefined, @@ -457,7 +717,6 @@ function buildStaticNativeSource( sourceAttributes: [source], requestHeaderAttributes: requestHeaders.attributes, sourceArray: t.cloneNode(sourceExpression, true), - consumesSizeProps: false, androidHeaders: getFirstSourceHeaders(sourceExpression), width: dimensionSource ? getObjectPropertyExpression(dimensionSource, 'width') : undefined, height: dimensionSource ? getObjectPropertyExpression(dimensionSource, 'height') : undefined, @@ -470,24 +729,28 @@ function buildStaticNativeSource( const sourceHeight = getObjectPropertyExpression(sourceObject, 'height'); const width = getNullishFallback(sourceWidth, getAttributeExpression(attributes, 'width')); const height = getNullishFallback(sourceHeight, getAttributeExpression(attributes, 'height')); - const sourceArrayObject = buildSourceObject(sourceObject, requestHeaders); + if ( + reactNativeMinor === undefined && + requestHeaders.headers.properties.length > 0 && + hasObjectProperty(sourceObject, 'headers') + ) + return undefined; + const sourceArrayObject = buildSourceObject(sourceObject, requestHeaders, reactNativeMinor); // An object source with generated request headers and a truthy `uri` goes through ImageSourceUtils // as a single-entry ARRAY source, so the width/height ?? prop fallback does not apply: only the // source entry's own dimensions reach the style, and only on Android. Without generated headers it // stays an OBJECT source, whose own dimensions always reach the style and whose inline `headers` // are lifted only on the RN versions that do so β€” hence the gate. + const uri = getObjectPropertyExpression(sourceObject, 'uri'); const usesGeneratedHeaders = - requestHeaders.headers.properties.length > 0 && hasObjectProperty(sourceArrayObject, 'headers'); + requestHeaders.headers.properties.length > 0 && uri !== undefined && isStaticTruthyForLogicalOr(uri); const arrayDimensionSource = usesGeneratedHeaders && emitsAndroidProps ? sourceArrayObject : undefined; return { sourceAttributes: [source], requestHeaderAttributes: requestHeaders.attributes, sourceArray: t.arrayExpression([sourceArrayObject]), - consumesSizeProps: true, - androidHeaders: usesGeneratedHeaders - ? t.cloneNode(requestHeaders.headers, true) - : getObjectPropertyExpression(sourceArrayObject, 'headers'), + androidHeaders: getObjectPropertyExpression(sourceArrayObject, 'headers'), objectSourceHeaders: !usesGeneratedHeaders, width: usesGeneratedHeaders ? arrayDimensionSource && getObjectPropertyExpression(arrayDimensionSource, 'width') @@ -535,25 +798,38 @@ function buildRequestHeaders(attributes: Array= 88 && sourceHeaders && !isNullishExpression(sourceHeaders)) + headers.properties.push(t.spreadElement(sourceHeaders)); + nativeSource.properties.push(t.objectProperty(t.identifier('headers'), headers)); return nativeSource; } function buildStyle( nativeSource: NativeSource, styleInfo: StyleInfo, + includesDimensions: boolean, arrayDimensionsGate?: t.Identifier ): t.ArrayExpression { const dimensions = t.objectExpression([ - ...(nativeSource.width ? [t.objectProperty(t.identifier('width'), t.cloneNode(nativeSource.width, true))] : []), - ...(nativeSource.height ? [t.objectProperty(t.identifier('height'), t.cloneNode(nativeSource.height, true))] : []), + ...(includesDimensions && nativeSource.width + ? [t.objectProperty(t.identifier('width'), t.cloneNode(nativeSource.width, true))] + : []), + ...(includesDimensions && nativeSource.height + ? [t.objectProperty(t.identifier('height'), t.cloneNode(nativeSource.height, true))] + : []), ]); return t.arrayExpression([ @@ -625,7 +901,11 @@ function flattenStaticStyle(styleExpression: t.Expression): Map): boolean { - return findAttribute(attributes, 'source') !== undefined || findAttribute(attributes, 'src') !== undefined; + return ( + findAttribute(attributes, 'source') !== undefined || + findAttribute(attributes, 'src') !== undefined || + findAttribute(attributes, 'srcSet') !== undefined + ); } function getAttributeExpression( @@ -657,7 +937,7 @@ function findAttribute( } function getObjectPropertyExpression(object: t.ObjectExpression, name: string): t.Expression | undefined { - for (const property of object.properties) { + for (const property of object.properties.toReversed()) { if (!t.isObjectProperty(property) || !t.isExpression(property.value)) continue; if (t.isIdentifier(property.key, { name }) || (t.isStringLiteral(property.key) && property.key.value === name)) { return t.cloneNode(property.value, true); diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures-unistyles-ts/unistyles-cast-style/code.tsx b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures-unistyles-ts/unistyles-cast-style/code.tsx similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures-unistyles-ts/unistyles-cast-style/code.tsx rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures-unistyles-ts/unistyles-cast-style/code.tsx diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures-unistyles-ts/unistyles-cast-style/output.tsx b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures-unistyles-ts/unistyles-cast-style/output.tsx similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures-unistyles-ts/unistyles-cast-style/output.tsx rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures-unistyles-ts/unistyles-cast-style/output.tsx diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures-unistyles/unistyles-aliased-style/code.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures-unistyles/unistyles-aliased-style/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures-unistyles/unistyles-aliased-style/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures-unistyles/unistyles-aliased-style/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures-unistyles/unistyles-aliased-style/output.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures-unistyles/unistyles-aliased-style/output.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures-unistyles/unistyles-aliased-style/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures-unistyles/unistyles-aliased-style/output.js diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures-unistyles/unistyles-imported-style-bails/code.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures-unistyles/unistyles-imported-style-bails/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures-unistyles/unistyles-imported-style-bails/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures-unistyles/unistyles-imported-style-bails/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures-unistyles/unistyles-imported-style-bails/output.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures-unistyles/unistyles-imported-style-bails/output.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures-unistyles/unistyles-imported-style-bails/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures-unistyles/unistyles-imported-style-bails/output.js diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures-unistyles/unistyles-literal-style/code.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures-unistyles/unistyles-literal-style/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures-unistyles/unistyles-literal-style/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures-unistyles/unistyles-literal-style/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures-unistyles/unistyles-no-style/output.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures-unistyles/unistyles-literal-style/output.js similarity index 76% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures-unistyles/unistyles-no-style/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures-unistyles/unistyles-literal-style/output.js index a9272da5..4929aab4 100644 --- a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures-unistyles/unistyles-no-style/output.js +++ b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures-unistyles/unistyles-literal-style/output.js @@ -1,13 +1,15 @@ import { - getDefaultTextStyle as _getDefaultTextStyle, + processTextStyle as _processTextStyle, getDefaultTextAccessible as _getDefaultTextAccessible, NativeText as _NativeText, } from 'react-native-boost/runtime'; import { Text } from 'react-native'; <_NativeText - style={_getDefaultTextStyle()} allowFontScaling={true} ellipsizeMode={'tail'} + {..._processTextStyle({ + color: 'red', + })} accessible={_getDefaultTextAccessible()}> Hello ; diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures-unistyles/unistyles-no-style/code.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures-unistyles/unistyles-no-style/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures-unistyles/unistyles-no-style/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures-unistyles/unistyles-no-style/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures-unistyles/unistyles-literal-style/output.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures-unistyles/unistyles-no-style/output.js similarity index 81% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures-unistyles/unistyles-literal-style/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures-unistyles/unistyles-no-style/output.js index 22915223..a59cbfc7 100644 --- a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures-unistyles/unistyles-literal-style/output.js +++ b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures-unistyles/unistyles-no-style/output.js @@ -5,12 +5,7 @@ import { } from 'react-native-boost/runtime'; import { Text } from 'react-native'; <_NativeText - style={[ - _getDefaultTextStyle(), - { - color: 'red', - }, - ]} + style={_getDefaultTextStyle() ? [_getDefaultTextStyle(), void 0] : void 0} allowFontScaling={true} ellipsizeMode={'tail'} accessible={_getDefaultTextAccessible()}> diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures-unistyles/unistyles-optional-member-style/code.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures-unistyles/unistyles-optional-member-style/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures-unistyles/unistyles-optional-member-style/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures-unistyles/unistyles-optional-member-style/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures-unistyles/unistyles-optional-member-style/output.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures-unistyles/unistyles-optional-member-style/output.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures-unistyles/unistyles-optional-member-style/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures-unistyles/unistyles-optional-member-style/output.js diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures-unistyles/unistyles-plain-rn-style/code.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures-unistyles/unistyles-plain-rn-style/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures-unistyles/unistyles-plain-rn-style/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures-unistyles/unistyles-plain-rn-style/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures-unistyles/unistyles-plain-rn-style/output.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures-unistyles/unistyles-plain-rn-style/output.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures-unistyles/unistyles-plain-rn-style/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures-unistyles/unistyles-plain-rn-style/output.js diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures-unistyles/unistyles-style-array/code.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures-unistyles/unistyles-style-array/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures-unistyles/unistyles-style-array/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures-unistyles/unistyles-style-array/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures-unistyles/unistyles-style-array/output.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures-unistyles/unistyles-style-array/output.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures-unistyles/unistyles-style-array/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures-unistyles/unistyles-style-array/output.js diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures-unistyles/unistyles-style/code.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures-unistyles/unistyles-style/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures-unistyles/unistyles-style/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures-unistyles/unistyles-style/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures-unistyles/unistyles-style/output.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures-unistyles/unistyles-style/output.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures-unistyles/unistyles-style/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures-unistyles/unistyles-style/output.js diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures-unistyles/unistyles-unknown-style-bails/code.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures-unistyles/unistyles-unknown-style-bails/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures-unistyles/unistyles-unknown-style-bails/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures-unistyles/unistyles-unknown-style-bails/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures-unistyles/unistyles-unknown-style-bails/output.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures-unistyles/unistyles-unknown-style-bails/output.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures-unistyles/unistyles-unknown-style-bails/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures-unistyles/unistyles-unknown-style-bails/output.js diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/ambiguous-id-bails/code.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/ambiguous-id-bails/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/ambiguous-id-bails/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/ambiguous-id-bails/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/ambiguous-id-bails/output.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/ambiguous-id-bails/output.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/ambiguous-id-bails/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/ambiguous-id-bails/output.js diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/aria-hidden/code.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/aria-hidden/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/aria-hidden/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/aria-hidden/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/aria-hidden/output.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/aria-hidden/output.js similarity index 83% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/aria-hidden/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/aria-hidden/output.js index 924c1489..c2c9f766 100644 --- a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/aria-hidden/output.js +++ b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/aria-hidden/output.js @@ -8,7 +8,7 @@ import { Text } from 'react-native'; {..._processTextAccessibilityProps({ 'aria-hidden': true, })} - style={_getDefaultTextStyle()} + style={_getDefaultTextStyle() ? [_getDefaultTextStyle(), void 0] : void 0} allowFontScaling={true} ellipsizeMode={'tail'}> test diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/basic/code.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/basic/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/basic/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/basic/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/basic/output.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/basic/output.js similarity index 80% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/basic/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/basic/output.js index 162cda5a..935cbee8 100644 --- a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/basic/output.js +++ b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/basic/output.js @@ -5,7 +5,7 @@ import { } from 'react-native-boost/runtime'; import { Text } from 'react-native'; <_NativeText - style={_getDefaultTextStyle()} + style={_getDefaultTextStyle() ? [_getDefaultTextStyle(), void 0] : void 0} allowFontScaling={true} ellipsizeMode={'tail'} accessible={_getDefaultTextAccessible()} diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/call-expression-child/code.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/call-expression-child/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/call-expression-child/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/call-expression-child/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/call-expression-child/output.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/call-expression-child/output.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/call-expression-child/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/call-expression-child/output.js diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/circular-const-child/code.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/circular-const-child/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/circular-const-child/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/circular-const-child/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/circular-const-child/output.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/circular-const-child/output.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/circular-const-child/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/circular-const-child/output.js diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/const-number-child/code.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/const-number-child/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/const-number-child/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/const-number-child/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/const-number-child/output.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/const-number-child/output.js similarity index 82% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/const-number-child/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/const-number-child/output.js index 6413d3f1..13888df4 100644 --- a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/const-number-child/output.js +++ b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/const-number-child/output.js @@ -6,7 +6,7 @@ import { import { Text } from 'react-native'; const n = 5; <_NativeText - style={_getDefaultTextStyle()} + style={_getDefaultTextStyle() ? [_getDefaultTextStyle(), void 0] : void 0} allowFontScaling={true} ellipsizeMode={'tail'} accessible={_getDefaultTextAccessible()}> diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/default-props/code.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/default-props/code.js similarity index 66% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/default-props/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/default-props/code.js index 5023b35b..0305d072 100644 --- a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/default-props/code.js +++ b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/default-props/code.js @@ -1,10 +1,6 @@ import { Text } from 'react-native'; -const someFunction = () => ({}); - Hello; No Scaling; -const unknownProps = someFunction(); -Unknown; const partialProps = { color: 'blue', ellipsizeMode: 'clip' }; Partial props; diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/default-props/output.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/default-props/output.js similarity index 74% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/default-props/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/default-props/output.js index 0d0d18f7..12cae90d 100644 --- a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/default-props/output.js +++ b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/default-props/output.js @@ -4,29 +4,26 @@ import { NativeText as _NativeText, } from 'react-native-boost/runtime'; import { Text } from 'react-native'; -const someFunction = () => ({}); <_NativeText - style={_getDefaultTextStyle()} + style={_getDefaultTextStyle() ? [_getDefaultTextStyle(), void 0] : void 0} allowFontScaling={true} ellipsizeMode={'tail'} accessible={_getDefaultTextAccessible()}> Hello ; <_NativeText - style={_getDefaultTextStyle()} + style={_getDefaultTextStyle() ? [_getDefaultTextStyle(), void 0] : void 0} allowFontScaling={false} ellipsizeMode={'tail'} accessible={_getDefaultTextAccessible()}> No Scaling ; -const unknownProps = someFunction(); -Unknown; const partialProps = { color: 'blue', ellipsizeMode: 'clip', }; <_NativeText - style={_getDefaultTextStyle()} + style={_getDefaultTextStyle() ? [_getDefaultTextStyle(), void 0] : void 0} {...partialProps} allowFontScaling={true} accessible={_getDefaultTextAccessible()}> diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/dynamic-identifier-child/code.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/dynamic-identifier-child/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/dynamic-identifier-child/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/dynamic-identifier-child/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/dynamic-identifier-child/output.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/dynamic-identifier-child/output.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/dynamic-identifier-child/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/dynamic-identifier-child/output.js diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/dynamic-number-of-lines-clamped/code.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/dynamic-number-of-lines-clamped/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/dynamic-number-of-lines-clamped/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/dynamic-number-of-lines-clamped/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/dynamic-number-of-lines-clamped/output.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/dynamic-number-of-lines-clamped/output.js similarity index 80% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/dynamic-number-of-lines-clamped/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/dynamic-number-of-lines-clamped/output.js index 91e712eb..77cb02f2 100644 --- a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/dynamic-number-of-lines-clamped/output.js +++ b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/dynamic-number-of-lines-clamped/output.js @@ -6,7 +6,7 @@ import { } from 'react-native-boost/runtime'; import { Text } from 'react-native'; <_NativeText - style={_getDefaultTextStyle()} + style={_getDefaultTextStyle() ? [_getDefaultTextStyle(), void 0] : void 0} numberOfLines={_clampNumberOfLines(lineCount)} allowFontScaling={true} ellipsizeMode={'tail'} @@ -14,7 +14,7 @@ import { Text } from 'react-native'; identifier ; <_NativeText - style={_getDefaultTextStyle()} + style={_getDefaultTextStyle() ? [_getDefaultTextStyle(), void 0] : void 0} numberOfLines={_clampNumberOfLines(getLines())} allowFontScaling={true} ellipsizeMode={'tail'} diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/dynamic-style-keeps-helper/code.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/dynamic-style-keeps-helper/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/dynamic-style-keeps-helper/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/dynamic-style-keeps-helper/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/dynamic-style-keeps-helper/output.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/dynamic-style-keeps-helper/output.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/dynamic-style-keeps-helper/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/dynamic-style-keeps-helper/output.js diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/dynamic-style-selectable-order/code.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/dynamic-style-selectable-order/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/dynamic-style-selectable-order/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/dynamic-style-selectable-order/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/dynamic-style-selectable-order/output.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/dynamic-style-selectable-order/output.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/dynamic-style-selectable-order/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/dynamic-style-selectable-order/output.js diff --git a/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/element-children-in-spread/code.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/element-children-in-spread/code.js new file mode 100644 index 00000000..eb0a103c --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/element-children-in-spread/code.js @@ -0,0 +1,6 @@ +import { Text, View } from 'react-native'; + +const props = { children: nested }; + + +; diff --git a/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/element-children-in-spread/output.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/element-children-in-spread/output.js new file mode 100644 index 00000000..394d4906 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/element-children-in-spread/output.js @@ -0,0 +1,7 @@ +import { Text, View } from 'react-native'; +const props = { + children: nested, +}; + + +; diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/element-in-ternary-child/code.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/element-in-ternary-child/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/element-in-ternary-child/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/element-in-ternary-child/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/element-in-ternary-child/output.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/element-in-ternary-child/output.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/element-in-ternary-child/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/element-in-ternary-child/output.js diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/expo-router-link-alias-as-child/code.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/expo-router-link-alias-as-child/code.js similarity index 88% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/expo-router-link-alias-as-child/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/expo-router-link-alias-as-child/code.js index e0162d08..41d6d7d7 100644 --- a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/expo-router-link-alias-as-child/code.js +++ b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/expo-router-link-alias-as-child/code.js @@ -2,7 +2,6 @@ import { Text } from 'react-native'; import { Link as RouterLink } from 'expo-router'; <> - This should be optimized This should NOT be optimized due to aliased Link asChild diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/expo-router-link-alias-as-child/dangerous-output.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/expo-router-link-alias-as-child/dangerous-output.js similarity index 72% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/expo-router-link-alias-as-child/dangerous-output.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/expo-router-link-alias-as-child/dangerous-output.js index 07ce5026..64371a58 100644 --- a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/expo-router-link-alias-as-child/dangerous-output.js +++ b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/expo-router-link-alias-as-child/dangerous-output.js @@ -6,19 +6,12 @@ import { import { Text } from 'react-native'; import { Link as RouterLink } from 'expo-router'; <> - <_NativeText - style={_getDefaultTextStyle()} - allowFontScaling={true} - ellipsizeMode={'tail'} - accessible={_getDefaultTextAccessible()}> - This should be optimized - This should NOT be optimized due to aliased Link asChild <_NativeText - style={_getDefaultTextStyle()} + style={_getDefaultTextStyle() ? [_getDefaultTextStyle(), void 0] : void 0} allowFontScaling={true} ellipsizeMode={'tail'} accessible={_getDefaultTextAccessible()}> diff --git a/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/expo-router-link-alias-as-child/output.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/expo-router-link-alias-as-child/output.js new file mode 100644 index 00000000..783f77bb --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/expo-router-link-alias-as-child/output.js @@ -0,0 +1,10 @@ +import { Text } from 'react-native'; +import { Link as RouterLink } from 'expo-router'; +<> + + This should NOT be optimized due to aliased Link asChild + + + Direct child of aliased Link without asChild + +; diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/expo-router-link-as-child-false-static/code.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/expo-router-link-as-child-false-static/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/expo-router-link-as-child-false-static/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/expo-router-link-as-child-false-static/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/expo-router-link-as-child-false-static/dangerous-output.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/expo-router-link-as-child-false-static/dangerous-output.js similarity index 87% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/expo-router-link-as-child-false-static/dangerous-output.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/expo-router-link-as-child-false-static/dangerous-output.js index 8358b791..dcf72209 100644 --- a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/expo-router-link-as-child-false-static/dangerous-output.js +++ b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/expo-router-link-as-child-false-static/dangerous-output.js @@ -8,7 +8,7 @@ import { Link } from 'expo-router'; <> <_NativeText - style={_getDefaultTextStyle()} + style={_getDefaultTextStyle() ? [_getDefaultTextStyle(), void 0] : void 0} allowFontScaling={true} ellipsizeMode={'tail'} accessible={_getDefaultTextAccessible()}> diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/expo-router-link-as-child-false-static/output.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/expo-router-link-as-child-false-static/output.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/expo-router-link-as-child-false-static/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/expo-router-link-as-child-false-static/output.js diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/expo-router-link-as-child-nested-view/code.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/expo-router-link-as-child-nested-view/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/expo-router-link-as-child-nested-view/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/expo-router-link-as-child-nested-view/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/expo-router-link-as-child-nested-view/dangerous-output.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/expo-router-link-as-child-nested-view/dangerous-output.js similarity index 86% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/expo-router-link-as-child-nested-view/dangerous-output.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/expo-router-link-as-child-nested-view/dangerous-output.js index 2dd992a7..8286640a 100644 --- a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/expo-router-link-as-child-nested-view/dangerous-output.js +++ b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/expo-router-link-as-child-nested-view/dangerous-output.js @@ -8,7 +8,7 @@ import { Link } from 'expo-router'; <_NativeText - style={_getDefaultTextStyle()} + style={_getDefaultTextStyle() ? [_getDefaultTextStyle(), void 0] : void 0} allowFontScaling={true} ellipsizeMode={'tail'} accessible={_getDefaultTextAccessible()}> diff --git a/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/expo-router-link-as-child-nested-view/output.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/expo-router-link-as-child-nested-view/output.js new file mode 100644 index 00000000..8286640a --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/expo-router-link-as-child-nested-view/output.js @@ -0,0 +1,18 @@ +import { + getDefaultTextStyle as _getDefaultTextStyle, + getDefaultTextAccessible as _getDefaultTextAccessible, + NativeText as _NativeText, +} from 'react-native-boost/runtime'; +import { Text, View } from 'react-native'; +import { Link } from 'expo-router'; + + + <_NativeText + style={_getDefaultTextStyle() ? [_getDefaultTextStyle(), void 0] : void 0} + allowFontScaling={true} + ellipsizeMode={'tail'} + accessible={_getDefaultTextAccessible()}> + Inside a View under an asChild Link + + +; diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/expo-router-link-as-child/code.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/expo-router-link-as-child/code.js similarity index 61% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/expo-router-link-as-child/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/expo-router-link-as-child/code.js index 142e7b36..e1afd160 100644 --- a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/expo-router-link-as-child/code.js +++ b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/expo-router-link-as-child/code.js @@ -2,14 +2,10 @@ import { Text } from 'react-native'; import { Link } from 'expo-router'; <> - This should be optimized This should NOT be optimized due to Link asChild Direct child of Link without asChild - - This should NOT be optimized (Link with href and asChild) - ; diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/expo-router-link-as-child/dangerous-output.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/expo-router-link-as-child/dangerous-output.js similarity index 61% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/expo-router-link-as-child/dangerous-output.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/expo-router-link-as-child/dangerous-output.js index 783ca1cb..b69eab07 100644 --- a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/expo-router-link-as-child/dangerous-output.js +++ b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/expo-router-link-as-child/dangerous-output.js @@ -6,26 +6,16 @@ import { import { Text } from 'react-native'; import { Link } from 'expo-router'; <> - <_NativeText - style={_getDefaultTextStyle()} - allowFontScaling={true} - ellipsizeMode={'tail'} - accessible={_getDefaultTextAccessible()}> - This should be optimized - This should NOT be optimized due to Link asChild <_NativeText - style={_getDefaultTextStyle()} + style={_getDefaultTextStyle() ? [_getDefaultTextStyle(), void 0] : void 0} allowFontScaling={true} ellipsizeMode={'tail'} accessible={_getDefaultTextAccessible()}> Direct child of Link without asChild - - This should NOT be optimized (Link with href and asChild) - ; diff --git a/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/expo-router-link-as-child/output.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/expo-router-link-as-child/output.js new file mode 100644 index 00000000..b3b3ec7f --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/expo-router-link-as-child/output.js @@ -0,0 +1,10 @@ +import { Text } from 'react-native'; +import { Link } from 'expo-router'; +<> + + This should NOT be optimized due to Link asChild + + + Direct child of Link without asChild + +; diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/expo-router-link-namespace-as-child/code.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/expo-router-link-namespace-as-child/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/expo-router-link-namespace-as-child/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/expo-router-link-namespace-as-child/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/expo-router-link-namespace-as-child/dangerous-output.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/expo-router-link-namespace-as-child/dangerous-output.js similarity index 88% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/expo-router-link-namespace-as-child/dangerous-output.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/expo-router-link-namespace-as-child/dangerous-output.js index c75752ab..2b977afe 100644 --- a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/expo-router-link-namespace-as-child/dangerous-output.js +++ b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/expo-router-link-namespace-as-child/dangerous-output.js @@ -11,7 +11,7 @@ import * as ExpoRouter from 'expo-router'; <_NativeText - style={_getDefaultTextStyle()} + style={_getDefaultTextStyle() ? [_getDefaultTextStyle(), void 0] : void 0} allowFontScaling={true} ellipsizeMode={'tail'} accessible={_getDefaultTextAccessible()}> diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/expo-router-link-namespace-as-child/output.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/expo-router-link-namespace-as-child/output.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/expo-router-link-namespace-as-child/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/expo-router-link-namespace-as-child/output.js diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/force-comment/code.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/force-comment/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/force-comment/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/force-comment/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/force-comment/output.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/force-comment/output.js similarity index 88% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/force-comment/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/force-comment/output.js index fe1be6ee..856742c5 100644 --- a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/force-comment/output.js +++ b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/force-comment/output.js @@ -13,7 +13,7 @@ import { Text } from 'react-native'; {/* @boost-force */} <_NativeText - style={_getDefaultTextStyle()} + style={_getDefaultTextStyle() ? [_getDefaultTextStyle(), void 0] : void 0} onPress={() => { console.log('pressed'); }} diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/id-in-resolvable-spread-bails/code.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/id-in-resolvable-spread-bails/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/id-in-resolvable-spread-bails/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/id-in-resolvable-spread-bails/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/id-in-resolvable-spread-bails/output.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/id-in-resolvable-spread-bails/output.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/id-in-resolvable-spread-bails/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/id-in-resolvable-spread-bails/output.js diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/id-to-native-id/code.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/id-to-native-id/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/id-to-native-id/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/id-to-native-id/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/id-to-native-id/output.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/id-to-native-id/output.js similarity index 82% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/id-to-native-id/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/id-to-native-id/output.js index 064e5adf..9efa4824 100644 --- a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/id-to-native-id/output.js +++ b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/id-to-native-id/output.js @@ -5,7 +5,7 @@ import { } from 'react-native-boost/runtime'; import { Text } from 'react-native'; <_NativeText - style={_getDefaultTextStyle()} + style={_getDefaultTextStyle() ? [_getDefaultTextStyle(), void 0] : void 0} nativeID="x" allowFontScaling={true} ellipsizeMode={'tail'} diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/id-wins-over-native-id/code.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/id-wins-over-native-id/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/id-wins-over-native-id/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/id-wins-over-native-id/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/id-wins-over-native-id/output.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/id-wins-over-native-id/output.js similarity index 82% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/id-wins-over-native-id/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/id-wins-over-native-id/output.js index 064e5adf..9efa4824 100644 --- a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/id-wins-over-native-id/output.js +++ b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/id-wins-over-native-id/output.js @@ -5,7 +5,7 @@ import { } from 'react-native-boost/runtime'; import { Text } from 'react-native'; <_NativeText - style={_getDefaultTextStyle()} + style={_getDefaultTextStyle() ? [_getDefaultTextStyle(), void 0] : void 0} nativeID="x" allowFontScaling={true} ellipsizeMode={'tail'} diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/ignore-comment/code.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/ignore-comment/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/ignore-comment/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/ignore-comment/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/ignore-comment/output.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/ignore-comment/output.js similarity index 84% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/ignore-comment/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/ignore-comment/output.js index 71577848..93ea4697 100644 --- a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/ignore-comment/output.js +++ b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/ignore-comment/output.js @@ -6,7 +6,7 @@ import { import { Text } from 'react-native'; <> <_NativeText - style={_getDefaultTextStyle()} + style={_getDefaultTextStyle() ? [_getDefaultTextStyle(), void 0] : void 0} allowFontScaling={true} ellipsizeMode={'tail'} accessible={_getDefaultTextAccessible()}> diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/logical-primitive-child/code.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/logical-primitive-child/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/logical-primitive-child/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/logical-primitive-child/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/logical-primitive-child/output.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/logical-primitive-child/output.js similarity index 83% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/logical-primitive-child/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/logical-primitive-child/output.js index c2942927..225a8562 100644 --- a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/logical-primitive-child/output.js +++ b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/logical-primitive-child/output.js @@ -6,7 +6,7 @@ import { import { Text } from 'react-native'; const name = 'John'; <_NativeText - style={_getDefaultTextStyle()} + style={_getDefaultTextStyle() ? [_getDefaultTextStyle(), void 0] : void 0} allowFontScaling={true} ellipsizeMode={'tail'} accessible={_getDefaultTextAccessible()}> diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/native-id-passthrough/code.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/native-id-passthrough/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/native-id-passthrough/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/native-id-passthrough/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/native-id-passthrough/output.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/native-id-passthrough/output.js similarity index 82% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/native-id-passthrough/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/native-id-passthrough/output.js index 7a3247de..3241c4b2 100644 --- a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/native-id-passthrough/output.js +++ b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/native-id-passthrough/output.js @@ -5,7 +5,7 @@ import { } from 'react-native-boost/runtime'; import { Text } from 'react-native'; <_NativeText - style={_getDefaultTextStyle()} + style={_getDefaultTextStyle() ? [_getDefaultTextStyle(), void 0] : void 0} nativeID="y" allowFontScaling={true} ellipsizeMode={'tail'} diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/nested-in-object-with-ignore-comment/code.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/nested-in-object-with-ignore-comment/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/nested-in-object-with-ignore-comment/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/nested-in-object-with-ignore-comment/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/nested-in-object-with-ignore-comment/output.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/nested-in-object-with-ignore-comment/output.js similarity index 88% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/nested-in-object-with-ignore-comment/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/nested-in-object-with-ignore-comment/output.js index 54251e31..c2f334a4 100644 --- a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/nested-in-object-with-ignore-comment/output.js +++ b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/nested-in-object-with-ignore-comment/output.js @@ -11,7 +11,7 @@ const benchmarks = [ optimizedComponent: ( <_NativeText - style={_getDefaultTextStyle()} + style={_getDefaultTextStyle() ? [_getDefaultTextStyle(), void 0] : void 0} allowFontScaling={true} ellipsizeMode={'tail'} accessible={_getDefaultTextAccessible()}> diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/nested-text-force-comment/code.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/nested-text-force-comment/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/nested-text-force-comment/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/nested-text-force-comment/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/nested-text-force-comment/output.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/nested-text-force-comment/output.js similarity index 83% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/nested-text-force-comment/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/nested-text-force-comment/output.js index 2be216cc..1ace4ccc 100644 --- a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/nested-text-force-comment/output.js +++ b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/nested-text-force-comment/output.js @@ -8,7 +8,7 @@ import { Text } from 'react-native'; Hello {/* @boost-force */} <_NativeText - style={_getDefaultTextStyle()} + style={_getDefaultTextStyle() ? [_getDefaultTextStyle(), void 0] : void 0} allowFontScaling={true} ellipsizeMode={'tail'} accessible={_getDefaultTextAccessible()}> diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/nested-text-same-file/code.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/nested-text-same-file/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/nested-text-same-file/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/nested-text-same-file/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/nested-text-same-file/output.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/nested-text-same-file/output.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/nested-text-same-file/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/nested-text-same-file/output.js diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/non-react-native-import/code.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/non-react-native-import/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/non-react-native-import/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/non-react-native-import/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/non-react-native-import/output.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/non-react-native-import/output.js similarity index 85% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/non-react-native-import/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/non-react-native-import/output.js index 9c736f5b..fcbb8f4f 100644 --- a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/non-react-native-import/output.js +++ b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/non-react-native-import/output.js @@ -7,7 +7,7 @@ import { Text } from 'some-other-package'; import { Text as RNText } from 'react-native'; Hello, world!; <_NativeText - style={_getDefaultTextStyle()} + style={_getDefaultTextStyle() ? [_getDefaultTextStyle(), void 0] : void 0} allowFontScaling={true} ellipsizeMode={'tail'} accessible={_getDefaultTextAccessible()}> diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/normalize-accessibility-props-and-flatten-styles/code.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/normalize-accessibility-props-and-flatten-styles/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/normalize-accessibility-props-and-flatten-styles/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/normalize-accessibility-props-and-flatten-styles/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/normalize-accessibility-props-and-flatten-styles/output.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/normalize-accessibility-props-and-flatten-styles/output.js similarity index 81% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/normalize-accessibility-props-and-flatten-styles/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/normalize-accessibility-props-and-flatten-styles/output.js index 1195e29b..ce7b676e 100644 --- a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/normalize-accessibility-props-and-flatten-styles/output.js +++ b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/normalize-accessibility-props-and-flatten-styles/output.js @@ -1,6 +1,6 @@ import { processTextAccessibilityProps as _processTextAccessibilityProps, - getDefaultTextStyle as _getDefaultTextStyle, + processTextStyle as _processTextStyle, NativeText as _NativeText, } from 'react-native-boost/runtime'; import { Text } from 'react-native'; @@ -9,13 +9,14 @@ import { Text } from 'react-native'; 'aria-label': 'test', 'accessibilityLabel': 'test', })} - style={[ - _getDefaultTextStyle(), + allowFontScaling={true} + ellipsizeMode={'tail'} + {..._processTextStyle([ { color: 'red', + }, + { fontSize: 16, }, - ]} - allowFontScaling={true} - ellipsizeMode={'tail'} + ])} />; diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/normalize-accessibility-props/code.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/normalize-accessibility-props/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/normalize-accessibility-props/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/normalize-accessibility-props/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/normalize-accessibility-props/output.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/normalize-accessibility-props/output.js similarity index 83% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/normalize-accessibility-props/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/normalize-accessibility-props/output.js index f620bdf7..0e411f89 100644 --- a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/normalize-accessibility-props/output.js +++ b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/normalize-accessibility-props/output.js @@ -9,7 +9,7 @@ import { Text } from 'react-native'; 'aria-label': 'test', 'accessibilityLabel': 'test', })} - style={_getDefaultTextStyle()} + style={_getDefaultTextStyle() ? [_getDefaultTextStyle(), void 0] : void 0} allowFontScaling={true} ellipsizeMode={'tail'} />; diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/normalized-props-in-spread-bails/code.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/normalized-props-in-spread-bails/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/normalized-props-in-spread-bails/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/normalized-props-in-spread-bails/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/normalized-props-in-spread-bails/output.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/normalized-props-in-spread-bails/output.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/normalized-props-in-spread-bails/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/normalized-props-in-spread-bails/output.js diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/number-of-lines/code.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/number-of-lines/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/number-of-lines/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/number-of-lines/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/number-of-lines/output.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/number-of-lines/output.js similarity index 74% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/number-of-lines/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/number-of-lines/output.js index b6d2f980..115b24ed 100644 --- a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/number-of-lines/output.js +++ b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/number-of-lines/output.js @@ -5,7 +5,7 @@ import { } from 'react-native-boost/runtime'; import { Text } from 'react-native'; <_NativeText - style={_getDefaultTextStyle()} + style={_getDefaultTextStyle() ? [_getDefaultTextStyle(), void 0] : void 0} numberOfLines={10} allowFontScaling={true} ellipsizeMode={'tail'} @@ -13,7 +13,7 @@ import { Text } from 'react-native'; 10 lines ; <_NativeText - style={_getDefaultTextStyle()} + style={_getDefaultTextStyle() ? [_getDefaultTextStyle(), void 0] : void 0} numberOfLines={0} allowFontScaling={true} ellipsizeMode={'tail'} @@ -21,7 +21,7 @@ import { Text } from 'react-native'; -10 lines ; <_NativeText - style={_getDefaultTextStyle()} + style={_getDefaultTextStyle() ? [_getDefaultTextStyle(), void 0] : void 0} numberOfLines={0} allowFontScaling={true} ellipsizeMode={'tail'} diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/numeric-child/code.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/numeric-child/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/numeric-child/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/numeric-child/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/numeric-child/output.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/numeric-child/output.js similarity index 81% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/numeric-child/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/numeric-child/output.js index 4bc19d68..623c5fda 100644 --- a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/numeric-child/output.js +++ b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/numeric-child/output.js @@ -5,7 +5,7 @@ import { } from 'react-native-boost/runtime'; import { Text } from 'react-native'; <_NativeText - style={_getDefaultTextStyle()} + style={_getDefaultTextStyle() ? [_getDefaultTextStyle(), void 0] : void 0} allowFontScaling={true} ellipsizeMode={'tail'} accessible={_getDefaultTextAccessible()}> diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/resolvable-spread-props/code.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/resolvable-spread-props/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/resolvable-spread-props/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/resolvable-spread-props/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/resolvable-spread-props/output.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/resolvable-spread-props/output.js new file mode 100644 index 00000000..a7b9af17 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/resolvable-spread-props/output.js @@ -0,0 +1,5 @@ +import { Text } from 'react-native'; +const props = { + children: 'Hello, world!', +}; +; diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/selection-color-bare/code.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/selection-color-bare/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/selection-color-bare/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/selection-color-bare/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/selection-color-bare/output.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/selection-color-bare/output.js similarity index 84% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/selection-color-bare/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/selection-color-bare/output.js index ef58cae0..722c00be 100644 --- a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/selection-color-bare/output.js +++ b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/selection-color-bare/output.js @@ -7,7 +7,7 @@ import { import { Text } from 'react-native'; <_NativeText {..._processSelectionColor(true)} - style={_getDefaultTextStyle()} + style={_getDefaultTextStyle() ? [_getDefaultTextStyle(), void 0] : void 0} allowFontScaling={true} ellipsizeMode={'tail'} accessible={_getDefaultTextAccessible()}> diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/selection-color-dynamic/code.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/selection-color-dynamic/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/selection-color-dynamic/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/selection-color-dynamic/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/selection-color-dynamic/output.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/selection-color-dynamic/output.js similarity index 85% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/selection-color-dynamic/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/selection-color-dynamic/output.js index 6ad07eaf..fa08869e 100644 --- a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/selection-color-dynamic/output.js +++ b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/selection-color-dynamic/output.js @@ -7,7 +7,7 @@ import { import { Text } from 'react-native'; <_NativeText {..._processSelectionColor(accent)} - style={_getDefaultTextStyle()} + style={_getDefaultTextStyle() ? [_getDefaultTextStyle(), void 0] : void 0} allowFontScaling={true} ellipsizeMode={'tail'} accessible={_getDefaultTextAccessible()}> diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/selection-color-in-spread/code.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/selection-color-in-spread/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/selection-color-in-spread/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/selection-color-in-spread/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/selection-color-in-spread/output.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/selection-color-in-spread/output.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/selection-color-in-spread/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/selection-color-in-spread/output.js diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/selection-color/code.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/selection-color/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/selection-color/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/selection-color/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/selection-color/output.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/selection-color/output.js similarity index 84% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/selection-color/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/selection-color/output.js index f6bf522f..e342fd79 100644 --- a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/selection-color/output.js +++ b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/selection-color/output.js @@ -7,7 +7,7 @@ import { import { Text } from 'react-native'; <_NativeText {..._processSelectionColor('red')} - style={_getDefaultTextStyle()} + style={_getDefaultTextStyle() ? [_getDefaultTextStyle(), void 0] : void 0} allowFontScaling={true} ellipsizeMode={'tail'} accessible={_getDefaultTextAccessible()}> diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/static-style-array-merged/code.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/static-style-array-merged/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/static-style-array-merged/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/static-style-array-merged/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/static-style-array-merged/output.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/static-style-array-merged/output.js similarity index 78% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/static-style-array-merged/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/static-style-array-merged/output.js index c3530c48..a685f8c1 100644 --- a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/static-style-array-merged/output.js +++ b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/static-style-array-merged/output.js @@ -1,18 +1,19 @@ import { - getDefaultTextStyle as _getDefaultTextStyle, + processTextStyle as _processTextStyle, getDefaultTextAccessible as _getDefaultTextAccessible, NativeText as _NativeText, } from 'react-native-boost/runtime'; import { Text } from 'react-native'; <_NativeText - style={[ - _getDefaultTextStyle(), + allowFontScaling={true} + ellipsizeMode={'tail'} + {..._processTextStyle([ { color: 'red', + }, + { fontSize: 16, }, - ]} - allowFontScaling={true} - ellipsizeMode={'tail'} + ])} accessible={_getDefaultTextAccessible()} />; diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/static-style-array-override/code.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/static-style-array-override/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/static-style-array-override/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/static-style-array-override/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/static-style-array-override/output.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/static-style-array-override/output.js similarity index 74% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/static-style-array-override/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/static-style-array-override/output.js index ab0121d1..bc1c37ca 100644 --- a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/static-style-array-override/output.js +++ b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/static-style-array-override/output.js @@ -1,17 +1,19 @@ import { - getDefaultTextStyle as _getDefaultTextStyle, + processTextStyle as _processTextStyle, getDefaultTextAccessible as _getDefaultTextAccessible, NativeText as _NativeText, } from 'react-native-boost/runtime'; import { Text } from 'react-native'; <_NativeText - style={[ - _getDefaultTextStyle(), + allowFontScaling={true} + ellipsizeMode={'tail'} + {..._processTextStyle([ + { + color: 'red', + }, { color: 'blue', }, - ]} - allowFontScaling={true} - ellipsizeMode={'tail'} + ])} accessible={_getDefaultTextAccessible()} />; diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/static-style-font-weight/code.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/static-style-font-weight/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/static-style-font-weight/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/static-style-font-weight/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/static-style-object/output.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/static-style-font-weight/output.js similarity index 69% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/static-style-object/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/static-style-font-weight/output.js index 15c86940..fd0d8d11 100644 --- a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/static-style-object/output.js +++ b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/static-style-font-weight/output.js @@ -1,17 +1,14 @@ import { - getDefaultTextStyle as _getDefaultTextStyle, + processTextStyle as _processTextStyle, getDefaultTextAccessible as _getDefaultTextAccessible, NativeText as _NativeText, } from 'react-native-boost/runtime'; import { Text } from 'react-native'; <_NativeText - style={[ - _getDefaultTextStyle(), - { - color: 'red', - }, - ]} allowFontScaling={true} ellipsizeMode={'tail'} + {..._processTextStyle({ + fontWeight: 700, + })} accessible={_getDefaultTextAccessible()} />; diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/static-style-nested/code.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/static-style-nested/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/static-style-nested/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/static-style-nested/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/static-style-font-weight/output.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/static-style-nested/output.js similarity index 60% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/static-style-font-weight/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/static-style-nested/output.js index e87b3c12..db4bb766 100644 --- a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/static-style-font-weight/output.js +++ b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/static-style-nested/output.js @@ -1,17 +1,22 @@ import { - getDefaultTextStyle as _getDefaultTextStyle, + processTextStyle as _processTextStyle, getDefaultTextAccessible as _getDefaultTextAccessible, NativeText as _NativeText, } from 'react-native-boost/runtime'; import { Text } from 'react-native'; <_NativeText - style={[ - _getDefaultTextStyle(), - { - fontWeight: '700', - }, - ]} allowFontScaling={true} ellipsizeMode={'tail'} + {..._processTextStyle({ + shadowOffset: { + width: 1, + height: 1, + }, + transform: [ + { + scale: 2, + }, + ], + })} accessible={_getDefaultTextAccessible()} />; diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/static-style-null-element/code.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/static-style-null-element/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/static-style-null-element/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/static-style-null-element/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/static-style-null-element/output.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/static-style-null-element/output.js similarity index 77% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/static-style-null-element/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/static-style-null-element/output.js index dd6d480a..3736fe41 100644 --- a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/static-style-null-element/output.js +++ b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/static-style-null-element/output.js @@ -1,17 +1,17 @@ import { - getDefaultTextStyle as _getDefaultTextStyle, + processTextStyle as _processTextStyle, getDefaultTextAccessible as _getDefaultTextAccessible, NativeText as _NativeText, } from 'react-native-boost/runtime'; import { Text } from 'react-native'; <_NativeText - style={[ - _getDefaultTextStyle(), + allowFontScaling={true} + ellipsizeMode={'tail'} + {..._processTextStyle([ { a: 1, }, - ]} - allowFontScaling={true} - ellipsizeMode={'tail'} + null, + ])} accessible={_getDefaultTextAccessible()} />; diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/static-style-object/code.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/static-style-object/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/static-style-object/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/static-style-object/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/static-style-object/output.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/static-style-object/output.js new file mode 100644 index 00000000..c6a71693 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/static-style-object/output.js @@ -0,0 +1,14 @@ +import { + processTextStyle as _processTextStyle, + getDefaultTextAccessible as _getDefaultTextAccessible, + NativeText as _NativeText, +} from 'react-native-boost/runtime'; +import { Text } from 'react-native'; +<_NativeText + allowFontScaling={true} + ellipsizeMode={'tail'} + {..._processTextStyle({ + color: 'red', + })} + accessible={_getDefaultTextAccessible()} +/>; diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/static-style-user-select-overrides-selectable/code.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/static-style-user-select-overrides-selectable/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/static-style-user-select-overrides-selectable/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/static-style-user-select-overrides-selectable/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/static-style-user-select/output.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/static-style-user-select-overrides-selectable/output.js similarity index 70% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/static-style-user-select/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/static-style-user-select-overrides-selectable/output.js index 34e9527a..4c432f00 100644 --- a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/static-style-user-select/output.js +++ b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/static-style-user-select-overrides-selectable/output.js @@ -1,18 +1,16 @@ import { - getDefaultTextStyle as _getDefaultTextStyle, + processTextStyle as _processTextStyle, getDefaultTextAccessible as _getDefaultTextAccessible, NativeText as _NativeText, } from 'react-native-boost/runtime'; import { Text } from 'react-native'; <_NativeText - style={[ - _getDefaultTextStyle(), - { - color: 'red', - }, - ]} + selectable={false} allowFontScaling={true} ellipsizeMode={'tail'} - selectable={false} + {..._processTextStyle({ + userSelect: 'text', + color: 'red', + })} accessible={_getDefaultTextAccessible()} />; diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/static-style-user-select/code.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/static-style-user-select/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/static-style-user-select/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/static-style-user-select/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/static-style-user-select/output.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/static-style-user-select/output.js new file mode 100644 index 00000000..7716e4ce --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/static-style-user-select/output.js @@ -0,0 +1,15 @@ +import { + processTextStyle as _processTextStyle, + getDefaultTextAccessible as _getDefaultTextAccessible, + NativeText as _NativeText, +} from 'react-native-boost/runtime'; +import { Text } from 'react-native'; +<_NativeText + allowFontScaling={true} + ellipsizeMode={'tail'} + {..._processTextStyle({ + userSelect: 'none', + color: 'red', + })} + accessible={_getDefaultTextAccessible()} +/>; diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/static-style-vertical-align-unknown/code.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/static-style-vertical-align-unknown/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/static-style-vertical-align-unknown/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/static-style-vertical-align-unknown/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/static-style-vertical-align-unknown/output.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/static-style-vertical-align-unknown/output.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/static-style-vertical-align-unknown/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/static-style-vertical-align-unknown/output.js diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/static-style-vertical-align/code.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/static-style-vertical-align/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/static-style-vertical-align/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/static-style-vertical-align/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/static-style-vertical-align/output.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/static-style-vertical-align/output.js new file mode 100644 index 00000000..504ade81 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/static-style-vertical-align/output.js @@ -0,0 +1,14 @@ +import { + processTextStyle as _processTextStyle, + getDefaultTextAccessible as _getDefaultTextAccessible, + NativeText as _NativeText, +} from 'react-native-boost/runtime'; +import { Text } from 'react-native'; +<_NativeText + allowFontScaling={true} + ellipsizeMode={'tail'} + {..._processTextStyle({ + verticalAlign: 'middle', + })} + accessible={_getDefaultTextAccessible()} +/>; diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/string-concat-child/code.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/string-concat-child/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/string-concat-child/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/string-concat-child/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/string-concat-child/output.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/string-concat-child/output.js similarity index 82% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/string-concat-child/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/string-concat-child/output.js index ff561333..f7222ad9 100644 --- a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/string-concat-child/output.js +++ b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/string-concat-child/output.js @@ -5,7 +5,7 @@ import { } from 'react-native-boost/runtime'; import { Text } from 'react-native'; <_NativeText - style={_getDefaultTextStyle()} + style={_getDefaultTextStyle() ? [_getDefaultTextStyle(), void 0] : void 0} allowFontScaling={true} ellipsizeMode={'tail'} accessible={_getDefaultTextAccessible()}> diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/template-literal-child/code.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/template-literal-child/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/template-literal-child/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/template-literal-child/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/template-literal-child/output.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/template-literal-child/output.js similarity index 82% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/template-literal-child/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/template-literal-child/output.js index ac60d7ee..e8f38f94 100644 --- a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/template-literal-child/output.js +++ b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/template-literal-child/output.js @@ -5,7 +5,7 @@ import { } from 'react-native-boost/runtime'; import { Text } from 'react-native'; <_NativeText - style={_getDefaultTextStyle()} + style={_getDefaultTextStyle() ? [_getDefaultTextStyle(), void 0] : void 0} allowFontScaling={true} ellipsizeMode={'tail'} accessible={_getDefaultTextAccessible()}>{`Hi ${user.name}`}; diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/ternary-primitive-child/code.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/ternary-primitive-child/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/ternary-primitive-child/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/ternary-primitive-child/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/ternary-primitive-child/output.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/ternary-primitive-child/output.js similarity index 82% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/ternary-primitive-child/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/ternary-primitive-child/output.js index 5c842a89..872c5e46 100644 --- a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/ternary-primitive-child/output.js +++ b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/ternary-primitive-child/output.js @@ -5,7 +5,7 @@ import { } from 'react-native-boost/runtime'; import { Text } from 'react-native'; <_NativeText - style={_getDefaultTextStyle()} + style={_getDefaultTextStyle() ? [_getDefaultTextStyle(), void 0] : void 0} allowFontScaling={true} ellipsizeMode={'tail'} accessible={_getDefaultTextAccessible()}> diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/text-content-as-explicit-child-prop/code.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/text-content-as-explicit-child-prop/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/text-content-as-explicit-child-prop/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/text-content-as-explicit-child-prop/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/text-content-as-explicit-child-prop/output.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/text-content-as-explicit-child-prop/output.js similarity index 82% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/text-content-as-explicit-child-prop/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/text-content-as-explicit-child-prop/output.js index 0bc521cb..20d73695 100644 --- a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/text-content-as-explicit-child-prop/output.js +++ b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/text-content-as-explicit-child-prop/output.js @@ -5,7 +5,7 @@ import { } from 'react-native-boost/runtime'; import { Text } from 'react-native'; <_NativeText - style={_getDefaultTextStyle()} + style={_getDefaultTextStyle() ? [_getDefaultTextStyle(), void 0] : void 0} children="Hello, world!" allowFontScaling={true} ellipsizeMode={'tail'} diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/text-content/code.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/text-content/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/text-content/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/text-content/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/text-content/output.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/text-content/output.js similarity index 77% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/text-content/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/text-content/output.js index 1da873ec..ea33d14a 100644 --- a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/text-content/output.js +++ b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/text-content/output.js @@ -5,7 +5,7 @@ import { } from 'react-native-boost/runtime'; import { Text } from 'react-native'; <_NativeText - style={_getDefaultTextStyle()} + style={_getDefaultTextStyle() ? [_getDefaultTextStyle(), void 0] : void 0} allowFontScaling={true} ellipsizeMode={'tail'} accessible={_getDefaultTextAccessible()}> @@ -13,7 +13,7 @@ import { Text } from 'react-native'; ; const text = 'Hello again, world!'; <_NativeText - style={_getDefaultTextStyle()} + style={_getDefaultTextStyle() ? [_getDefaultTextStyle(), void 0] : void 0} allowFontScaling={true} ellipsizeMode={'tail'} accessible={_getDefaultTextAccessible()}> diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/text-in-render-prop/code.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/text-in-render-prop/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/text-in-render-prop/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/text-in-render-prop/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/text-in-render-prop/output.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/text-in-render-prop/output.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/text-in-render-prop/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/text-in-render-prop/output.js diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/text-under-namespace-text-ancestor/code.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/text-under-namespace-text-ancestor/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/text-under-namespace-text-ancestor/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/text-under-namespace-text-ancestor/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/text-under-namespace-text-ancestor/output.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/text-under-namespace-text-ancestor/output.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/text-under-namespace-text-ancestor/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/text-under-namespace-text-ancestor/output.js diff --git a/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/text-under-react-native-scroll-view-ancestor/code.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/text-under-react-native-scroll-view-ancestor/code.js new file mode 100644 index 00000000..64c5de5e --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/text-under-react-native-scroll-view-ancestor/code.js @@ -0,0 +1,4 @@ +import { ScrollView, Text } from 'react-native'; + + hello +; diff --git a/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/text-under-react-native-scroll-view-ancestor/output.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/text-under-react-native-scroll-view-ancestor/output.js new file mode 100644 index 00000000..64c5de5e --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/text-under-react-native-scroll-view-ancestor/output.js @@ -0,0 +1,4 @@ +import { ScrollView, Text } from 'react-native'; + + hello +; diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/text-under-runtime-fragment/code.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/text-under-runtime-fragment/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/text-under-runtime-fragment/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/text-under-runtime-fragment/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/text-under-runtime-fragment/output.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/text-under-runtime-fragment/output.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/text-under-runtime-fragment/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/text-under-runtime-fragment/output.js diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/text-under-safe-ancestor/code.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/text-under-safe-ancestor/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/text-under-safe-ancestor/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/text-under-safe-ancestor/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/text-under-safe-ancestor/output.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/text-under-safe-ancestor/output.js similarity index 82% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/text-under-safe-ancestor/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/text-under-safe-ancestor/output.js index 5d1467f1..d1a44752 100644 --- a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/text-under-safe-ancestor/output.js +++ b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/text-under-safe-ancestor/output.js @@ -6,7 +6,7 @@ import { import { Text, View } from 'react-native'; <_NativeText - style={_getDefaultTextStyle()} + style={_getDefaultTextStyle() ? [_getDefaultTextStyle(), void 0] : void 0} allowFontScaling={true} ellipsizeMode={'tail'} accessible={_getDefaultTextAccessible()}> diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/text-under-unknown-ancestor/code.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/text-under-unknown-ancestor/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/text-under-unknown-ancestor/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/text-under-unknown-ancestor/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/text-under-unknown-ancestor/dangerous-output.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/text-under-unknown-ancestor/dangerous-output.js similarity index 83% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/text-under-unknown-ancestor/dangerous-output.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/text-under-unknown-ancestor/dangerous-output.js index f06e003d..6d1914f7 100644 --- a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/text-under-unknown-ancestor/dangerous-output.js +++ b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/text-under-unknown-ancestor/dangerous-output.js @@ -7,7 +7,7 @@ import { Text } from 'react-native'; import { Custom } from './Custom'; <_NativeText - style={_getDefaultTextStyle()} + style={_getDefaultTextStyle() ? [_getDefaultTextStyle(), void 0] : void 0} allowFontScaling={true} ellipsizeMode={'tail'} accessible={_getDefaultTextAccessible()}> diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/text-under-unknown-ancestor/output.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/text-under-unknown-ancestor/output.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/text-under-unknown-ancestor/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/text-under-unknown-ancestor/output.js diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/text-with-runtime-parent/code.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/text-with-runtime-parent/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/text-with-runtime-parent/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/text-with-runtime-parent/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/text-with-runtime-parent/dangerous-output.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/text-with-runtime-parent/dangerous-output.js similarity index 83% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/text-with-runtime-parent/dangerous-output.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/text-with-runtime-parent/dangerous-output.js index ed4e1f96..7346ad7a 100644 --- a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/text-with-runtime-parent/dangerous-output.js +++ b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/text-with-runtime-parent/dangerous-output.js @@ -6,7 +6,7 @@ import { import { Text } from 'react-native'; const renderBreak = () => ( <_NativeText - style={_getDefaultTextStyle()} + style={_getDefaultTextStyle() ? [_getDefaultTextStyle(), void 0] : void 0} allowFontScaling={true} ellipsizeMode={'tail'} accessible={_getDefaultTextAccessible()}> diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/text-with-runtime-parent/output.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/text-with-runtime-parent/output.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/text-with-runtime-parent/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/text-with-runtime-parent/output.js diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/unresolvable-spread-props/code.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/unresolvable-spread-props/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/unresolvable-spread-props/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/unresolvable-spread-props/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/unresolvable-spread-props/output.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/unresolvable-spread-props/output.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/unresolvable-spread-props/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/unresolvable-spread-props/output.js diff --git a/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/variable-child-no-string/code.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/variable-child-no-string/code.js new file mode 100644 index 00000000..45ddfda0 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/variable-child-no-string/code.js @@ -0,0 +1,3 @@ +import { Text } from 'react-native'; +const child = ; +{child}; diff --git a/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/variable-child-no-string/output.js b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/variable-child-no-string/output.js new file mode 100644 index 00000000..45ddfda0 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/fixtures/variable-child-no-string/output.js @@ -0,0 +1,3 @@ +import { Text } from 'react-native'; +const child = ; +{child}; diff --git a/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/index.test.ts b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/index.test.ts new file mode 100644 index 00000000..52721c19 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-text/__tests__/index.test.ts @@ -0,0 +1,253 @@ +import { createRequire } from 'node:module'; +import path from 'node:path'; +import { transformSync, type TransformCaller } from '@babel/core'; +import { pluginTester } from 'babel-plugin-tester'; +import { describe, expect, it } from 'vitest'; +import { generateTestPlugin } from '../../../utils/generate-test-plugin'; +import { formatTestResult } from '../../../utils/format-test-result'; +import boostPlugin from '../../../index'; +import { nativeTextOptimizer } from '..'; + +const reactNativePackageJson = createRequire(import.meta.url).resolve('react-native/package.json'); + +pluginTester({ + plugin: generateTestPlugin(nativeTextOptimizer), + title: 'text', + fixtures: path.resolve(import.meta.dirname, 'fixtures'), + babelOptions: { + plugins: ['@babel/plugin-syntax-jsx'], + }, + formatResult: formatTestResult, +}); + +// The expo-router cases also pin that the `asChild` bail is independent of the ancestor assumption. +const unknownAncestorAssumptionFixtures = [ + 'text-under-unknown-ancestor', + 'text-with-runtime-parent', + 'expo-router-link-as-child', + 'expo-router-link-as-child-false-static', + 'expo-router-link-alias-as-child', + 'expo-router-link-namespace-as-child', + 'expo-router-link-as-child-nested-view', +]; + +pluginTester({ + plugin: generateTestPlugin(nativeTextOptimizer, { + assumptions: { unknownAncestorsDoNotRenderText: true }, + }), + title: 'text unknown ancestor assumption', + babelOptions: { + plugins: ['@babel/plugin-syntax-jsx'], + }, + formatResult: formatTestResult, + tests: unknownAncestorAssumptionFixtures.map((name) => ({ + title: name, + fixture: path.resolve(import.meta.dirname, `fixtures/${name}/code.js`), + outputFixture: path.resolve(import.meta.dirname, `fixtures/${name}/dangerous-output.js`), + })), +}); + +pluginTester({ + plugin: generateTestPlugin(nativeTextOptimizer, { integrations: { unistyles: 'on' } }), + title: 'text unistyles', + fixtures: path.resolve(import.meta.dirname, 'fixtures-unistyles'), + babelOptions: { + plugins: ['@babel/plugin-syntax-jsx'], + }, + formatResult: formatTestResult, +}); + +pluginTester({ + plugin: generateTestPlugin(nativeTextOptimizer, { integrations: { unistyles: 'on' } }), + title: 'text unistyles typescript', + fixtures: path.resolve(import.meta.dirname, 'fixtures-unistyles-ts'), + babelOptions: { + plugins: ['@babel/plugin-syntax-jsx', ['@babel/plugin-syntax-typescript', { isTSX: true }]], + }, + formatResult: formatTestResult, +}); + +const transformText = async (reactNativeMinor: number, platform?: 'ios'): Promise => + formatTestResult( + transformSync( + ` + import { Text } from 'react-native'; + plain; + static; + dynamic; + `, + { + configFile: false, + babelrc: false, + plugins: ['@babel/plugin-syntax-jsx', generateTestPlugin(nativeTextOptimizer, {}, platform, reactNativeMinor)], + } + )!.code! + ); + +const transformSelectionColor = async ( + attribute: string, + platform?: 'ios' | 'android', + preamble = '', + packageJson = reactNativePackageJson +): Promise => + await formatTestResult( + transformSync(`import { Text } from 'react-native'; ${preamble} hello;`, { + configFile: false, + babelrc: false, + caller: { name: 'test', platform } as TransformCaller, + plugins: [ + '@babel/plugin-syntax-jsx', + [boostPlugin, { logLevel: 'silent', target: { reactNative: { packageJson } } }], + ], + })!.code! + ); + +describe('selectionColor', () => { + it.each([ + ['ios', 'selectionColor={4284887961}'], + ['android', 'selectionColor={-10079335}'], + ] as const)('precomputes literal colors for %s', async (platform, expected) => { + const output = await transformSelectionColor('selectionColor="rebeccapurple"', platform); + + expect(output).toContain(expected); + expect(output).not.toContain('processSelectionColor'); + }); + + it('precomputes constant colors', async () => { + const output = await transformSelectionColor( + 'selectionColor={accent}', + 'ios', + 'const base = "rebeccapurple"; const accent = base;' + ); + + expect(output).toContain('selectionColor={4284887961}'); + expect(output).not.toContain('processSelectionColor'); + }); + + it('uses runtime processing for dynamic colors', async () => { + const output = await transformSelectionColor('selectionColor={accent}', 'ios', 'const accent = getAccent();'); + + expect(output).toContain('processSelectionColor'); + expect(output).toContain('_processSelectionColor(accent)'); + }); + + it('uses runtime processing when the platform is unknown', async () => { + const output = await transformSelectionColor('selectionColor="red"'); + + expect(output).toContain('processSelectionColor'); + expect(output).toContain("_processSelectionColor('red')"); + }); + + it('uses runtime processing when the target parser cannot be resolved', async () => { + const output = await transformSelectionColor('selectionColor="red"', 'ios', '', 'missing-package.json'); + + expect(output).toContain('processSelectionColor'); + expect(output).toContain("_processSelectionColor('red')"); + }); + + it('omits a statically invalid color', async () => { + const output = await transformSelectionColor('selectionColor="invalid"', 'ios'); + + expect(output).not.toContain('selectionColor'); + expect(output).not.toContain('processSelectionColor'); + }); +}); + +describe('web Text styles', () => { + it('keeps native style normalization in the runtime when the platform is unknown', async () => { + const output = await formatTestResult( + transformSync( + `import { Text } from 'react-native'; label;`, + { + configFile: false, + babelrc: false, + plugins: ['@babel/plugin-syntax-jsx', generateTestPlugin(nativeTextOptimizer)], + } + )!.code! + ); + expect(output).toContain('processTextStyle'); + expect(output).toContain('fontWeight: 400'); + expect(output).toContain("userSelect: 'text'"); + expect(output).not.toContain('selectable='); + }); + + it('leaves authored styles and selectable to the web wrapper', async () => { + const output = await formatTestResult( + transformSync( + ` + import { Text } from 'react-native'; + label; + `, + { + configFile: false, + babelrc: false, + plugins: ['@babel/plugin-syntax-jsx', generateTestPlugin(nativeTextOptimizer, {}, 'web', 87)], + } + )!.code! + ); + expect(output).toContain('selectable={false}'); + expect(output).toContain('fontWeight: 400'); + expect(output).toContain("userSelect: 'text'"); + expect(output).toContain("verticalAlign: 'middle'"); + expect(output).not.toContain('processTextStyle'); + expect(output).not.toContain('textAlignVertical'); + expect(output).not.toContain('getDefaultTextStyle'); + }); +}); + +describe('Text caller-state mutation timing', () => { + it.each(['disabled={null}', 'disabled={false} aria-busy={true}'])( + 'keeps safe shared-state cases optimized: %s', + (attributes) => { + const output = transformSync( + `import { Text } from 'react-native'; const state = {disabled:true}; label;`, + { + configFile: false, + babelrc: false, + plugins: ['@babel/plugin-syntax-jsx', generateTestPlugin(nativeTextOptimizer, {}, 'ios', 87)], + } + )!.code!; + expect(output).toContain('processTextAccessibilityProps'); + } + ); + + it.each([83, 84, 85, 86, 87])('keeps shared conflicting state in the wrapper from RN 0.85 (target 0.%i)', (minor) => { + const output = transformSync( + `import { Text } from 'react-native'; const state = {disabled:true}; label;`, + { + configFile: false, + babelrc: false, + plugins: ['@babel/plugin-syntax-jsx', generateTestPlugin(nativeTextOptimizer, {}, 'ios', minor)], + } + )!.code!; + expect(output.includes('react-native-boost/runtime')).toBe(minor < 85); + }); +}); + +describe('text version defaults', () => { + it('omits the default overflow style before RN 0.85', async () => { + const output = await transformText(84, 'ios'); + + expect(output).not.toContain('getDefaultTextStyle'); + expect(output).not.toContain('textDefaultOverflowStyle'); + expect(output).not.toContain("overflow: 'hidden'"); + expect(output).toContain('processTextStyle(dynamicStyle, false)'); + }); + + it('uses the default overflow style from RN 0.85', async () => { + const output = await transformText(85, 'ios'); + + expect(output).not.toContain('getDefaultTextStyle'); + expect(output).toContain('textDefaultOverflowStyle as _textDefaultOverflowStyle'); + expect(output).toContain('style={[_textDefaultOverflowStyle, void 0]}'); + expect(output).toContain('processTextStyle(dynamicStyle, true)'); + }); + + it('keeps the runtime fallback when the platform is unknown', async () => { + const output = await transformText(85); + + expect(output).toContain('getDefaultTextStyle'); + expect(output).not.toContain('textDefaultOverflowStyle'); + expect(output).toContain('processTextStyle(dynamicStyle)'); + }); +}); diff --git a/packages/react-native-boost/src/plugin/optimizers/text/index.ts b/packages/react-native-boost/src/plugin/optimizers/native-text/index.ts similarity index 64% rename from packages/react-native-boost/src/plugin/optimizers/text/index.ts rename to packages/react-native-boost/src/plugin/optimizers/native-text/index.ts index fef8d2fb..3ee9ba9a 100644 --- a/packages/react-native-boost/src/plugin/optimizers/text/index.ts +++ b/packages/react-native-boost/src/plugin/optimizers/native-text/index.ts @@ -1,5 +1,5 @@ import { NodePath, types as t } from '@babel/core'; -import { HubFile, Optimizer, PluginLogger } from '../../types'; +import type { HubFile, JSXOptimizer, PluginLogger, ReactNativeColorNormalizer } from '../../types'; import PluginError from '../../utils/plugin-error'; import { BailoutCheck, getFirstBailoutReason } from '../../utils/helpers'; import { @@ -19,14 +19,16 @@ import { hasExpoRouterLinkParentWithAsChild, extractStyleAttribute, extractSelectionColor, - extractSelectableAndUpdateStyle, + extractStaticTextSelectable, tryBuildStaticTextStyle, + isStaticLiteralTree, ancestorBailoutChecks, - inheritsTextContextFromRuntimeParent, createStyleOriginResolver, UNISTYLES_TEXT_HOST, } from '../../utils/common'; import { ACCESSIBILITY_PROPERTIES, RUNTIME_MODULE_NAME } from '../../utils/constants'; +import { createJSXOptimizer } from '../../utils/optimizer'; +import { foldAccessibility } from '../../utils/static-accessibility'; export const textBlacklistedProperties = new Set([ 'onLongPress', @@ -71,6 +73,7 @@ const NORMALIZED_PROPERTIES = new Set([ */ const TEXT_SPREAD_GUARD_KEYS = new Set([ ...NORMALIZED_PROPERTIES, + 'children', 'style', 'numberOfLines', 'id', @@ -84,7 +87,11 @@ const TEXT_SPREAD_GUARD_KEYS = new Set([ const isNormalizedProperty = (attribute: t.JSXAttribute | t.JSXSpreadAttribute): attribute is t.JSXAttribute => t.isJSXAttribute(attribute) && t.isJSXIdentifier(attribute.name) && NORMALIZED_PROPERTIES.has(attribute.name.name); -export const textOptimizer: Optimizer = (path, logger, options, platform, unistylesEnabled) => { +const optimizeNativeText: JSXOptimizer = ( + path, + { logger, options, platform, unistylesEnabled, reactNativeMinor, normalizeColor } +) => { + if (platform === 'web' && options?.integrations?.uniwind === 'on') return; if (!isReactNativeComponent(path, 'Text')) return; const parent = path.parent as t.JSXElement; @@ -103,6 +110,21 @@ export const textOptimizer: Optimizer = (path, logger, options, platform, unisty reason: 'has a spread that may carry a translated, normalized, or clamped prop', shouldBail: () => hasBlacklistedPropertyInSpread(path, TEXT_SPREAD_GUARD_KEYS), }, + { + reason: 'disabled reconciliation may mutate caller state before React renders Text', + shouldBail: () => + platform !== 'web' && + options?.integrations?.uniwind !== 'on' && + (reactNativeMinor === undefined || reactNativeMinor >= 85) && + mayMutateCallerState(path) && + foldAccessibility( + path, + path.node.attributes.filter(isNormalizedProperty), + 'Text', + platform, + reactNativeMinor + ) === undefined, + }, { reason: 'has an unresolved style source that may be a Unistyles style', shouldBail: () => getStyleOrigin() === 'unknown', @@ -121,19 +143,14 @@ export const textOptimizer: Optimizer = (path, logger, options, platform, unisty reason: 'contains non-primitive children', shouldBail: () => hasInvalidChildren(path, parent), }, - { - reason: 'has unresolved runtime parent that may render Text', - shouldBail: () => - options?.dangerouslyOptimizeTextWithUnknownAncestors !== true && inheritsTextContextFromRuntimeParent(path), - }, - ...ancestorBailoutChecks(path, options?.dangerouslyOptimizeTextWithUnknownAncestors === true), + ...ancestorBailoutChecks(path, options?.assumptions?.unknownAncestorsDoNotRenderText === true), ]; if (forced) { const overriddenReason = getFirstBailoutReason(overridableChecks); if (overriddenReason) { - logger.forced({ component: 'Text', path, reason: overriddenReason }); + logger.forced({ target: 'Text', path, reason: overriddenReason }); } } else { const skipReason = getFirstBailoutReason([ @@ -145,7 +162,7 @@ export const textOptimizer: Optimizer = (path, logger, options, platform, unisty ]); if (skipReason) { - logger.skipped({ component: 'Text', path, reason: skipReason }); + logger.skipped({ target: 'Text', path, reason: skipReason }); return; } } @@ -157,11 +174,21 @@ export const textOptimizer: Optimizer = (path, logger, options, platform, unisty throw new PluginError('No file found in Babel hub'); } + if (options?.integrations?.uniwind === 'on' && getStyleOrigin() !== 'plain') { + logger.skipped({ target: 'Text', path, reason: 'Uniwind cannot flatten a Unistyles style' }); + return; + } + logger.optimized({ - component: 'Text', + target: 'Text', path, }); + if (options?.integrations?.uniwind === 'on') { + replaceWithNativeComponent(path, parent, file, 'NativeText', { moduleName: 'react-native-boost/uniwind' }); + return; + } + const routeToUnistyles = getStyleOrigin() === 'unistyles'; // Process props @@ -169,7 +196,7 @@ export const textOptimizer: Optimizer = (path, logger, options, platform, unisty renameIdToNativeID(path); addDefaultProperty(path, 'allowFontScaling', t.booleanLiteral(true)); addDefaultProperty(path, 'ellipsizeMode', t.stringLiteral('tail')); - processProps(path, file, platform, routeToUnistyles); + processProps(path, file, platform, routeToUnistyles, reactNativeMinor, normalizeColor); // A Unistyles-styled Text routes to Unistyles' lean host (a registering wrapper around `RCTText`); its // `style` is passed by identity (see `processProps`) so the Unistyles native-state β€” and therefore the @@ -177,6 +204,31 @@ export const textOptimizer: Optimizer = (path, logger, options, platform, unisty replaceWithNativeComponent(path, parent, file, 'NativeText', routeToUnistyles ? UNISTYLES_TEXT_HOST : undefined); }; +export const nativeTextOptimizer = createJSXOptimizer('native-text', optimizeNativeText); + +function mayMutateCallerState(path: NodePath): boolean { + const attributes = path.node.attributes.filter((attribute): attribute is t.JSXAttribute => + t.isJSXAttribute(attribute) + ); + const disabled = attributes.find((attribute) => t.isJSXIdentifier(attribute.name, { name: 'disabled' })); + const state = attributes.find((attribute) => t.isJSXIdentifier(attribute.name, { name: 'accessibilityState' })); + if (!disabled || !state) return false; + if (t.isJSXExpressionContainer(disabled.value) && t.isNullLiteral(disabled.value.expression)) return false; + if (t.isJSXExpressionContainer(state.value) && isStaticLiteralTree(state.value.expression)) return false; + // A definite ARIA state override makes RN copy the caller's state before reconciling disabled. + return !attributes.some( + (attribute) => + t.isJSXIdentifier(attribute.name) && + ['aria-busy', 'aria-checked', 'aria-disabled', 'aria-expanded', 'aria-selected'].includes(attribute.name.name) && + (attribute.value === null || + t.isStringLiteral(attribute.value) || + (t.isJSXExpressionContainer(attribute.value) && + (t.isBooleanLiteral(attribute.value.expression) || + t.isStringLiteral(attribute.value.expression) || + t.isNumericLiteral(attribute.value.expression)))) + ); +} + /** * Checks if the Text component has any invalid children or blacklisted properties. * This function combines the checks for both attribute-based children and JSX children. @@ -236,7 +288,7 @@ function fixNegativeNumberOfLines({ if (literalValue !== undefined) { if (literalValue < 0) { logger.warning({ - component: 'Text', + target: 'Text', path, message: `'numberOfLines' must be a non-negative number, received: ${literalValue}. The value will be set to 0.`, }); @@ -283,8 +335,11 @@ function processProps( path: NodePath, file: HubFile, platform?: string, - passStyleByIdentity = false + passStyleByIdentity = false, + reactNativeMinor?: number, + normalizeColor?: ReactNativeColorNormalizer ) { + passStyleByIdentity ||= platform === 'web'; // Grab the up-to-date list of attributes const currentAttributes = [...path.node.attributes]; @@ -305,22 +360,39 @@ function processProps( // ============================================ let accessibilitySpread: t.JSXSpreadAttribute | undefined; + let accessibilityAttributes: t.JSXAttribute[] | undefined; // --- Accessibility & `disabled` --- if (shouldNormalize) { const normalizedAttributes = currentAttributes.filter((attribute) => isNormalizedProperty(attribute)); - const normalizeIdentifier = addFileImportHint({ - file, - nameHint: 'processTextAccessibilityProps', + accessibilityAttributes = foldAccessibility( path, - importName: 'processTextAccessibilityProps', - moduleName: RUNTIME_MODULE_NAME, - }); + currentAttributes.filter( + (attribute): attribute is t.JSXAttribute => + t.isJSXAttribute(attribute) && + t.isJSXIdentifier(attribute.name) && + (NORMALIZED_PROPERTIES.has(attribute.name.name) || + attribute.name.name === 'role' || + attribute.name.name === 'accessibilityRole') + ), + 'Text', + platform, + reactNativeMinor + ); + if (!accessibilityAttributes) { + const normalizeIdentifier = addFileImportHint({ + file, + nameHint: 'processTextAccessibilityProps', + path, + importName: 'processTextAccessibilityProps', + moduleName: RUNTIME_MODULE_NAME, + }); - const accessibilityObject = buildPropertiesFromAttributes(normalizedAttributes); - const accessibilityExpr = t.callExpression(t.identifier(normalizeIdentifier.name), [accessibilityObject]); - accessibilitySpread = t.jsxSpreadAttribute(accessibilityExpr); + const accessibilityObject = buildPropertiesFromAttributes(normalizedAttributes); + const accessibilityExpr = t.callExpression(t.identifier(normalizeIdentifier.name), [accessibilityObject]); + accessibilitySpread = t.jsxSpreadAttribute(accessibilityExpr); + } } // --- Style --- @@ -332,10 +404,24 @@ function processProps( let staticStyleAttribute: t.JSXAttribute | undefined; let styleSpread: t.JSXSpreadAttribute | undefined; - // RN prepends `{ overflow: 'hidden' }` from 0.85. The runtime returns this release default or - // `undefined`, so user styles still win. Web and Unistyles skip it; dynamic styles use `processTextStyle`. + // RN prepends `{ overflow: 'hidden' }` from 0.85. Use the build target when both its version and + // native platform are known. The runtime fallback preserves unknown-platform and Babel-only builds. const emitsDefaultStyle = !passStyleByIdentity && platform !== 'web'; - const buildDefaultTextStyleExpression = (): t.Expression => { + const resolvesDefaultStyleAtBuildTime = + emitsDefaultStyle && reactNativeMinor !== undefined && (platform === 'ios' || platform === 'android'); + const buildDefaultTextStyleExpression = (): t.Expression | undefined => { + if (resolvesDefaultStyleAtBuildTime) { + if (reactNativeMinor < 85) return undefined; + const defaultStyleIdentifier = addFileImportHint({ + file, + nameHint: 'textDefaultOverflowStyle', + path, + importName: 'textDefaultOverflowStyle', + moduleName: RUNTIME_MODULE_NAME, + }); + return t.identifier(defaultStyleIdentifier.name); + } + const defaultStyleIdentifier = addFileImportHint({ file, nameHint: 'getDefaultTextStyle', @@ -346,10 +432,21 @@ function processProps( return t.callExpression(t.identifier(defaultStyleIdentifier.name), []); }; + const withDefaultTextStyle = (style: t.Expression): t.Expression => { + const defaultStyle = emitsDefaultStyle ? buildDefaultTextStyleExpression() : undefined; + if (!defaultStyle) return style; + const composed = t.arrayExpression([t.cloneNode(defaultStyle), style]); + return resolvesDefaultStyleAtBuildTime + ? composed + : t.conditionalExpression(defaultStyle, composed, t.cloneNode(style, true)); + }; + // `passStyleByIdentity` (Unistyles routing) skips all style transforms β€” the original `style` // attribute is left untouched in the collected attributes below so it reaches the native host intact. if (styleExpr && !passStyleByIdentity) { - const selectable = extractSelectableAndUpdateStyle(styleExpr); + // An unknown platform can load the web runtime, which must receive the unmodified style. + const staticStyle = platform === undefined ? undefined : tryBuildStaticTextStyle(styleExpr); + const selectable = staticStyle ? extractStaticTextSelectable(styleExpr) : undefined; if (selectable) { selectableAttribute = t.jsxAttribute( @@ -362,16 +459,11 @@ function processProps( ); } - // A fully static style is normalized at build time and emitted as a direct `style={...}` object, - // dropping the per-render `processTextStyle` call. Dynamic styles still go through the runtime - // helper, where the WeakMap reference cache and `StyleSheet.flatten` are the actual win. - const staticStyle = tryBuildStaticTextStyle(styleExpr); + // Static styles skip runtime inspection, but keep the authored entries before RN's overrides. if (staticStyle) { staticStyleAttribute = t.jsxAttribute( t.jsxIdentifier('style'), - t.jsxExpressionContainer( - emitsDefaultStyle ? t.arrayExpression([buildDefaultTextStyleExpression(), staticStyle]) : staticStyle - ) + t.jsxExpressionContainer(withDefaultTextStyle(staticStyle)) ); } else { const flattenIdentifier = addFileImportHint({ @@ -381,39 +473,45 @@ function processProps( importName: 'processTextStyle', moduleName: RUNTIME_MODULE_NAME, }); - const flattenedStyleExpr = t.callExpression(t.identifier(flattenIdentifier.name), [styleExpr]); + const helperArguments = [styleExpr]; + if (resolvesDefaultStyleAtBuildTime) { + helperArguments.push(t.booleanLiteral(reactNativeMinor >= 85)); + } + const flattenedStyleExpr = t.callExpression(t.identifier(flattenIdentifier.name), helperArguments); styleSpread = t.jsxSpreadAttribute(flattenedStyleExpr); } - } else if (!styleAttribute && emitsDefaultStyle) { + } else if (!styleAttribute && emitsDefaultStyle && (!resolvesDefaultStyleAtBuildTime || reactNativeMinor >= 85)) { // No style at all still gets the wrapper's default. A style attribute without an extractable // expression (e.g. `style=""`) is left verbatim instead β€” emitting a second `style` would have the // later attribute silently discard one of the two. staticStyleAttribute = t.jsxAttribute( t.jsxIdentifier('style'), - t.jsxExpressionContainer(buildDefaultTextStyleExpression()) + t.jsxExpressionContainer(withDefaultTextStyle(t.unaryExpression('void', t.numericLiteral(0)))) ); } // --- selectionColor --- - // `Text` runs `selectionColor` through `processColor` before handing it to its native host (a CSS color - // string / `PlatformColor` becomes the packed value the host expects). We reproduce that single - // normalization at runtime via a `processSelectionColor` spread, mirroring the `processTextStyle` - // mechanism β€” any expression flows through unchanged, so no `@boost-force` is needed for a dynamic - // value. A spread-carried `selectionColor` bails upstream (`TEXT_SPREAD_GUARD_KEYS`) rather than - // forwarding the prop un-normalized. const { selectionColorAttribute, selectionColorExpr } = extractSelectionColor(currentAttributes); - let selectionColorSpread: t.JSXSpreadAttribute | undefined; + let selectionColorReplacement: t.JSXAttribute | t.JSXSpreadAttribute | undefined; if (selectionColorExpr) { - const selectionColorIdentifier = addFileImportHint({ - file, - nameHint: 'processSelectionColor', - path, - importName: 'processSelectionColor', - moduleName: RUNTIME_MODULE_NAME, - }); - selectionColorSpread = t.jsxSpreadAttribute( - t.callExpression(t.identifier(selectionColorIdentifier.name), [selectionColorExpr]) - ); + const staticSelectionColor = processStaticSelectionColor(path, selectionColorExpr, platform, normalizeColor); + if (staticSelectionColor === undefined) { + const selectionColorIdentifier = addFileImportHint({ + file, + nameHint: 'processSelectionColor', + path, + importName: 'processSelectionColor', + moduleName: RUNTIME_MODULE_NAME, + }); + selectionColorReplacement = t.jsxSpreadAttribute( + t.callExpression(t.identifier(selectionColorIdentifier.name), [selectionColorExpr]) + ); + } else if (staticSelectionColor !== null) { + selectionColorReplacement = t.jsxAttribute( + t.jsxIdentifier('selectionColor'), + t.jsxExpressionContainer(t.numericLiteral(staticSelectionColor)) + ); + } } // ============================================ @@ -428,8 +526,15 @@ function processProps( // Skip the props we routed through `processTextAccessibilityProps` if (shouldNormalize && isNormalizedProperty(attribute)) continue; + if ( + accessibilityAttributes && + t.isJSXAttribute(attribute) && + t.isJSXIdentifier(attribute.name) && + (attribute.name.name === 'role' || attribute.name.name === 'accessibilityRole') + ) + continue; - // Skip the selectionColor attribute (replaced with a `processSelectionColor` spread) + // Skip the selectionColor attribute after preparing its replacement. if (selectionColorAttribute && attribute === selectionColorAttribute) continue; // A build-time `userSelect`-derived `selectable` supersedes a direct `selectable` (RN's rule), so @@ -461,11 +566,11 @@ function processProps( // ============================================ // `selectableAttribute` and `styleSpread` are emitted AFTER the remaining attributes so style-derived // `userSelect` overrides direct or spread-carried `selectable`, mirroring `Text`'s late override. - // `selectionColorSpread` writes a disjoint key (`selectionColor`), so its position is free β€” it leads - // for readability. + // The selection color writes a disjoint key, so its position is free β€” it leads for readability. path.node.attributes = [ - selectionColorSpread, + selectionColorReplacement, accessibilitySpread, + ...(accessibilityAttributes ?? []), staticStyleAttribute, ...remainingAttributes, selectableAttribute, @@ -474,6 +579,57 @@ function processProps( ].filter((attribute): attribute is t.JSXAttribute | t.JSXSpreadAttribute => attribute !== undefined); } +function processStaticSelectionColor( + path: NodePath, + expression: t.Expression, + platform: string | undefined, + normalizeColor: ReactNativeColorNormalizer | undefined, + resolved = new Set() +): number | null | undefined { + if ((platform !== 'ios' && platform !== 'android') || !normalizeColor) return undefined; + + if (t.isIdentifier(expression)) { + if (expression.name === 'undefined' && !path.scope.getBinding('undefined')) return null; + if (resolved.has(expression.name)) return undefined; + const binding = path.scope.getBinding(expression.name); + if ( + binding?.kind !== 'const' || + !binding.constant || + !t.isVariableDeclarator(binding.path.node) || + !t.isExpression(binding.path.node.init) || + binding.path.node.start == null || + expression.start == null || + binding.path.node.start > expression.start + ) { + return undefined; + } + resolved.add(expression.name); + return processStaticSelectionColor(path, binding.path.node.init, platform, normalizeColor, resolved); + } + + let color: string | number; + if (t.isStringLiteral(expression) || t.isNumericLiteral(expression)) { + color = expression.value; + } else if (t.isTemplateLiteral(expression) && expression.expressions.length === 0) { + color = expression.quasis[0]!.value.cooked!; + } else if ( + t.isUnaryExpression(expression) && + expression.operator === '-' && + t.isNumericLiteral(expression.argument) + ) { + color = -expression.argument.value; + } else if (t.isNullLiteral(expression) || t.isBooleanLiteral(expression)) { + return null; + } else { + return undefined; + } + + let processedColor = normalizeColor(color); + if (typeof processedColor !== 'number') return null; + processedColor = ((processedColor << 24) | (processedColor >>> 8)) >>> 0; + return platform === 'android' && processedColor > 0x7f_ff_ff_ff ? processedColor - 0x1_00_00_00_00 : processedColor; +} + /** * Builds the `accessible` default attribute `Text` applies when the prop is omitted: `true` on iOS, * `false` on Android, omitted on web. Metro bundles per platform and reports the target on the Babel diff --git a/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures-unistyles/runtime-parent/code.js b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures-unistyles/runtime-parent/code.js new file mode 100644 index 00000000..f31e1dd7 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures-unistyles/runtime-parent/code.js @@ -0,0 +1,5 @@ +import { View } from 'react-native'; +import { StyleSheet } from 'react-native-unistyles'; + +const styles = StyleSheet.create({ box: { flex: 1 } }); +const Box = ({ children }) => {children}; diff --git a/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures-unistyles/runtime-parent/output.js b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures-unistyles/runtime-parent/output.js new file mode 100644 index 00000000..36f40d54 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures-unistyles/runtime-parent/output.js @@ -0,0 +1,8 @@ +import { View } from 'react-native'; +import { StyleSheet } from 'react-native-unistyles'; +const styles = StyleSheet.create({ + box: { + flex: 1, + }, +}); +const Box = ({ children }) => {children}; diff --git a/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures-unistyles/text-context/code.js b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures-unistyles/text-context/code.js new file mode 100644 index 00000000..bb93cac4 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures-unistyles/text-context/code.js @@ -0,0 +1,26 @@ +import { Text, View } from 'react-native'; +import { StyleSheet } from 'react-native-unistyles'; + +const styles = StyleSheet.create({ box: { flex: 1 } }); +function Mixed({ children, inline }) { + if (inline) return {children}; + return children; +} + +<> + + + label + + + + + label + + + + + label + + +; diff --git a/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures-unistyles/text-context/output.js b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures-unistyles/text-context/output.js new file mode 100644 index 00000000..818227ca --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures-unistyles/text-context/output.js @@ -0,0 +1,32 @@ +import { NativeViewWithContext as _NativeViewWithContext } from 'react-native-boost/runtime'; +import { Text, View } from 'react-native'; +import { StyleSheet } from 'react-native-unistyles'; +const styles = StyleSheet.create({ + box: { + flex: 1, + }, +}); +function Mixed({ children, inline }) { + if (inline) return {children}; + return children; +} +<> + + + label + + + + + label + + + + <_NativeViewWithContext + style={{ + flex: 1, + }}> + label + + +; diff --git a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures-unistyles/unistyles-imported-style-bails/code.js b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures-unistyles/unistyles-imported-style-bails/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures-unistyles/unistyles-imported-style-bails/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures-unistyles/unistyles-imported-style-bails/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures-unistyles/unistyles-imported-style-bails/output.js b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures-unistyles/unistyles-imported-style-bails/output.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures-unistyles/unistyles-imported-style-bails/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures-unistyles/unistyles-imported-style-bails/output.js diff --git a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures-unistyles/unistyles-plain-rn-style/code.js b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures-unistyles/unistyles-plain-rn-style/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures-unistyles/unistyles-plain-rn-style/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures-unistyles/unistyles-plain-rn-style/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures-unistyles/unistyles-plain-rn-style/output.js b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures-unistyles/unistyles-plain-rn-style/output.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures-unistyles/unistyles-plain-rn-style/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures-unistyles/unistyles-plain-rn-style/output.js diff --git a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures-unistyles/unistyles-style-in-resolvable-spread-bails/code.js b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures-unistyles/unistyles-style-in-resolvable-spread-bails/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures-unistyles/unistyles-style-in-resolvable-spread-bails/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures-unistyles/unistyles-style-in-resolvable-spread-bails/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures-unistyles/unistyles-style-in-resolvable-spread-bails/output.js b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures-unistyles/unistyles-style-in-resolvable-spread-bails/output.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures-unistyles/unistyles-style-in-resolvable-spread-bails/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures-unistyles/unistyles-style-in-resolvable-spread-bails/output.js diff --git a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures-unistyles/unistyles-style/code.js b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures-unistyles/unistyles-style/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures-unistyles/unistyles-style/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures-unistyles/unistyles-style/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures-unistyles/unistyles-style/output.js b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures-unistyles/unistyles-style/output.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures-unistyles/unistyles-style/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures-unistyles/unistyles-style/output.js diff --git a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures-unistyles/unistyles-unknown-style-bails/code.js b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures-unistyles/unistyles-unknown-style-bails/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures-unistyles/unistyles-unknown-style-bails/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures-unistyles/unistyles-unknown-style-bails/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures-unistyles/unistyles-unknown-style-bails/output.js b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures-unistyles/unistyles-unknown-style-bails/output.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures-unistyles/unistyles-unknown-style-bails/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures-unistyles/unistyles-unknown-style-bails/output.js diff --git a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/accessibility-state/code.js b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/accessibility-state/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/accessibility-state/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/accessibility-state/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/accessibility-state/output.js b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/accessibility-state/output.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/accessibility-state/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/accessibility-state/output.js diff --git a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/ambiguous-id-bails/code.js b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/ambiguous-id-bails/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/ambiguous-id-bails/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/ambiguous-id-bails/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/ambiguous-id-bails/output.js b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/ambiguous-id-bails/output.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/ambiguous-id-bails/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/ambiguous-id-bails/output.js diff --git a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/aria-busy/code.js b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/aria-busy/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/aria-busy/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/aria-busy/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/aria-busy/output.js b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/aria-busy/output.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/aria-busy/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/aria-busy/output.js diff --git a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/aria-hidden/code.js b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/aria-hidden/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/aria-hidden/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/aria-hidden/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/aria-hidden/output.js b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/aria-hidden/output.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/aria-hidden/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/aria-hidden/output.js diff --git a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/aria-in-resolvable-spread-bails/code.js b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/aria-in-resolvable-spread-bails/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/aria-in-resolvable-spread-bails/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/aria-in-resolvable-spread-bails/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/aria-in-resolvable-spread-bails/output.js b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/aria-in-resolvable-spread-bails/output.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/aria-in-resolvable-spread-bails/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/aria-in-resolvable-spread-bails/output.js diff --git a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/aria-label-collision/code.js b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/aria-label-collision/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/aria-label-collision/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/aria-label-collision/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/accessibility-label/output.js b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/aria-label-collision/output.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/accessibility-label/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/aria-label-collision/output.js diff --git a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/aria-label-dynamic/code.js b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/aria-label-dynamic/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/aria-label-dynamic/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/aria-label-dynamic/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/aria-label-dynamic/output.js b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/aria-label-dynamic/output.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/aria-label-dynamic/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/aria-label-dynamic/output.js diff --git a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/aria-labelledby/code.js b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/aria-labelledby/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/aria-labelledby/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/aria-labelledby/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/aria-labelledby/output.js b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/aria-labelledby/output.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/aria-labelledby/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/aria-labelledby/output.js diff --git a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/aria-live/code.js b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/aria-live/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/aria-live/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/aria-live/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/aria-live/output.js b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/aria-live/output.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/aria-live/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/aria-live/output.js diff --git a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/aria-value-aggregation/code.js b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/aria-value-aggregation/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/aria-value-aggregation/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/aria-value-aggregation/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/aria-value-aggregation/output.js b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/aria-value-aggregation/output.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/aria-value-aggregation/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/aria-value-aggregation/output.js diff --git a/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/bailout-unsafe-props/code.js b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/bailout-unsafe-props/code.js new file mode 100644 index 00000000..243b9853 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/bailout-unsafe-props/code.js @@ -0,0 +1,26 @@ +import { View } from 'react-native'; + +const nested = { ...more }; +const computed = { [property]: 'label' }; +let reassigned = {}; +reassigned = { 'aria-label': 'label' }; +const mutated = {}; +mutated['aria-label'] = 'label'; +const getter = { + get title() { + return 'label'; + }, +}; + +; +; +; +; +; +; +; +const Box = ({ children }) => ( + + {children} + +); diff --git a/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/bailout-unsafe-props/output.js b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/bailout-unsafe-props/output.js new file mode 100644 index 00000000..14d0cfac --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/bailout-unsafe-props/output.js @@ -0,0 +1,30 @@ +import { View } from 'react-native'; +const nested = { + ...more, +}; +const computed = { + [property]: 'label', +}; +let reassigned = {}; +reassigned = { + 'aria-label': 'label', +}; +const mutated = {}; +mutated['aria-label'] = 'label'; +const getter = { + get title() { + return 'label'; + }, +}; +; +; +; +; +; +; +; +const Box = ({ children }) => ( + + {children} + +); diff --git a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures-unistyles/unistyles-no-style/code.js b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/basic/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures-unistyles/unistyles-no-style/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/basic/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures-unistyles/unistyles-no-style/output.js b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/basic/output.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures-unistyles/unistyles-no-style/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/basic/output.js diff --git a/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/context-only-ancestry/code.js b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/context-only-ancestry/code.js new file mode 100644 index 00000000..7cfdf35c --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/context-only-ancestry/code.js @@ -0,0 +1,58 @@ +import React from 'react'; +import { Text, View, TouchableWithoutFeedback } from 'react-native'; + +function Mixed({ children, inline }) { + if (inline) return {children}; + return children; +} +function Clone({ children }) { + return React.cloneElement(children, { 'aria-label': 'Name' }); +} +function UnsafeBranch({ children, inline }) { + if (inline) return {children}; + return React.cloneElement(children, { 'aria-label': 'Name' }); +} +function Inspect({ children }) { + return children.type === View ? children : null; +} + +<> + + + label + + + + + label + + + + + label + + + + + label + + + + + label + + + + + label + + + + + label + + + + + +; diff --git a/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/context-only-ancestry/output.js b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/context-only-ancestry/output.js new file mode 100644 index 00000000..42d81799 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/context-only-ancestry/output.js @@ -0,0 +1,61 @@ +import { NativeViewWithContext as _NativeViewWithContext, NativeView as _NativeView } from 'react-native-boost/runtime'; +import React from 'react'; +import { Text, View, TouchableWithoutFeedback } from 'react-native'; +function Mixed({ children, inline }) { + if (inline) return {children}; + return children; +} +function Clone({ children }) { + return React.cloneElement(children, { + 'aria-label': 'Name', + }); +} +function UnsafeBranch({ children, inline }) { + if (inline) return {children}; + return React.cloneElement(children, { + 'aria-label': 'Name', + }); +} +function Inspect({ children }) { + return children.type === View ? children : null; +} +<> + + <_NativeViewWithContext accessibilityLabel="Name"> + label + + + + <_NativeViewWithContext> + label + + + + + label + + + + + label + + + + + label + + + + + label + + + + + label + + + + <_NativeView /> + +; diff --git a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/force-aria-translation/code.js b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/force-aria-translation/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/force-aria-translation/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/force-aria-translation/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/force-aria-translation/output.js b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/force-aria-translation/output.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/force-aria-translation/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/force-aria-translation/output.js diff --git a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/force-comment/code.js b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/force-comment/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/force-comment/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/force-comment/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/force-comment/output.js b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/force-comment/output.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/force-comment/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/force-comment/output.js diff --git a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/id-in-resolvable-spread-bails/code.js b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/id-in-resolvable-spread-bails/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/id-in-resolvable-spread-bails/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/id-in-resolvable-spread-bails/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/id-in-resolvable-spread-bails/output.js b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/id-in-resolvable-spread-bails/output.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/id-in-resolvable-spread-bails/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/id-in-resolvable-spread-bails/output.js diff --git a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/id-to-native-id/code.js b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/id-to-native-id/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/id-to-native-id/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/id-to-native-id/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/id-to-native-id/output.js b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/id-to-native-id/output.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/id-to-native-id/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/id-to-native-id/output.js diff --git a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/id-wins-over-native-id/code.js b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/id-wins-over-native-id/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/id-wins-over-native-id/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/id-wins-over-native-id/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/id-wins-over-native-id/output.js b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/id-wins-over-native-id/output.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/id-wins-over-native-id/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/id-wins-over-native-id/output.js diff --git a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/id-wins-over-spread-native-id/code.js b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/id-wins-over-spread-native-id/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/id-wins-over-spread-native-id/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/id-wins-over-spread-native-id/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/id-wins-over-spread-native-id/output.js b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/id-wins-over-spread-native-id/output.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/id-wins-over-spread-native-id/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/id-wins-over-spread-native-id/output.js diff --git a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/ignore-comment/code.js b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/ignore-comment/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/ignore-comment/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/ignore-comment/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/ignore-comment/output.js b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/ignore-comment/output.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/ignore-comment/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/ignore-comment/output.js diff --git a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/indirect-text-ancestor/code.js b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/indirect-text-ancestor/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/indirect-text-ancestor/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/indirect-text-ancestor/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/indirect-text-ancestor/output.js b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/indirect-text-ancestor/output.js similarity index 55% rename from packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/indirect-text-ancestor/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/indirect-text-ancestor/output.js index 5762e10a..82a1b552 100644 --- a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/indirect-text-ancestor/output.js +++ b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/indirect-text-ancestor/output.js @@ -1,4 +1,4 @@ -import { NativeView as _NativeView } from 'react-native-boost/runtime'; +import { NativeView as _NativeView, NativeViewWithContext as _NativeViewWithContext } from 'react-native-boost/runtime'; import { Text, View } from 'react-native'; const Custom = ({ children }) => { return {children}; @@ -8,8 +8,8 @@ const Custom = ({ children }) => { - + <_NativeViewWithContext> - + ; diff --git a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/react-native-namespace-text-ancestor/code.js b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/react-native-namespace-text-ancestor/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/react-native-namespace-text-ancestor/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/react-native-namespace-text-ancestor/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/react-native-namespace-text-ancestor/output.js b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/react-native-namespace-text-ancestor/output.js new file mode 100644 index 00000000..4a6857aa --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/react-native-namespace-text-ancestor/output.js @@ -0,0 +1,8 @@ +import { NativeViewWithContext as _NativeViewWithContext } from 'react-native-boost/runtime'; +import * as ReactNative from 'react-native'; +import { View } from 'react-native'; + + <_NativeViewWithContext> + + +; diff --git a/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/runtime-parent/code.js b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/runtime-parent/code.js new file mode 100644 index 00000000..e1db0cc5 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/runtime-parent/code.js @@ -0,0 +1,14 @@ +import { View } from 'react-native'; + +const Box = ({ children }) => {children}; +const ExplicitChildren = ({ children }) => ; +const Empty = () => ; +const Nested = ({ children }) => ( + + {children} + +); + +function renderBox(children) { + return {children}; +} diff --git a/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/runtime-parent/output.js b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/runtime-parent/output.js new file mode 100644 index 00000000..daf9aeda --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/runtime-parent/output.js @@ -0,0 +1,13 @@ +import { NativeViewWithContext as _NativeViewWithContext, NativeView as _NativeView } from 'react-native-boost/runtime'; +import { View } from 'react-native'; +const Box = ({ children }) => <_NativeViewWithContext testID="box">{children}; +const ExplicitChildren = ({ children }) => <_NativeViewWithContext children={children} />; +const Empty = () => <_NativeView />; +const Nested = ({ children }) => ( + <_NativeViewWithContext> + <_NativeView>{children} + +); +function renderBox(children) { + return <_NativeViewWithContext>{children}; +} diff --git a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/same-file-safe-ancestor/code.js b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/same-file-safe-ancestor/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/same-file-safe-ancestor/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/same-file-safe-ancestor/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/same-file-safe-ancestor/output.js b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/same-file-safe-ancestor/output.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/same-file-safe-ancestor/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/same-file-safe-ancestor/output.js diff --git a/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/same-file-text-sibling-safe-ancestor/code.js b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/same-file-text-sibling-safe-ancestor/code.js new file mode 100644 index 00000000..ccc148a8 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/same-file-text-sibling-safe-ancestor/code.js @@ -0,0 +1,12 @@ +import { Text, View } from 'react-native'; + +const Card = ({ children }) => ( + + Title + {children} + +); + + + +; diff --git a/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/same-file-text-sibling-safe-ancestor/output.js b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/same-file-text-sibling-safe-ancestor/output.js new file mode 100644 index 00000000..f26db67a --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/same-file-text-sibling-safe-ancestor/output.js @@ -0,0 +1,11 @@ +import { NativeViewWithContext as _NativeViewWithContext, NativeView as _NativeView } from 'react-native-boost/runtime'; +import { Text, View } from 'react-native'; +const Card = ({ children }) => ( + <_NativeViewWithContext> + Title + {children} + +); + + <_NativeView /> +; diff --git a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/same-file-unknown-ancestor/code.js b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/same-file-unknown-ancestor/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/same-file-unknown-ancestor/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/same-file-unknown-ancestor/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/same-file-unknown-ancestor/output.js b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/same-file-unknown-ancestor/output.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/same-file-unknown-ancestor/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/same-file-unknown-ancestor/output.js diff --git a/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/spread-helper-children/code.js b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/spread-helper-children/code.js new file mode 100644 index 00000000..eb3ae64e --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/spread-helper-children/code.js @@ -0,0 +1,11 @@ +import { Text, View } from 'react-native'; + +function getProps(children) { + return { children }; +} + +export const Cell = () => ( + + cell)} /> + +); diff --git a/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/spread-helper-children/output.js b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/spread-helper-children/output.js new file mode 100644 index 00000000..10948464 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/spread-helper-children/output.js @@ -0,0 +1,12 @@ +import { NativeViewWithContext as _NativeViewWithContext } from 'react-native-boost/runtime'; +import { Text, View } from 'react-native'; +function getProps(children) { + return { + children, + }; +} +export const Cell = () => ( + + <_NativeViewWithContext {...getProps(cell)} /> + +); diff --git a/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/spread-helper-guards/code.js b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/spread-helper-guards/code.js new file mode 100644 index 00000000..5acda283 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/spread-helper-guards/code.js @@ -0,0 +1,25 @@ +import { View } from 'react-native'; + +function getLabel() { + return { 'aria-label': 'label' }; +} +function getUnknown(input) { + return input; +} +function getProps() { + recordCall(); + return { testID: 'cell' }; +} + +export function Screen({ input }) { + return ( + + + + + + + + + ); +} diff --git a/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/spread-helper-guards/output.js b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/spread-helper-guards/output.js new file mode 100644 index 00000000..7d90f636 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/spread-helper-guards/output.js @@ -0,0 +1,28 @@ +import { NativeViewWithContext as _NativeViewWithContext } from 'react-native-boost/runtime'; +import { View } from 'react-native'; +function getLabel() { + return { + 'aria-label': 'label', + }; +} +function getUnknown(input) { + return input; +} +function getProps() { + recordCall(); + return { + testID: 'cell', + }; +} +export function Screen({ input }) { + return ( + <_NativeViewWithContext> + + + + + + + + ); +} diff --git a/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/spread-helper/code.js b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/spread-helper/code.js new file mode 100644 index 00000000..fecbbed6 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/spread-helper/code.js @@ -0,0 +1,10 @@ +import { View } from 'react-native'; + +function getProps(enabled, index) { + if (!enabled) return {}; + const props = { role: 'cell' }; + if (index !== undefined) props['aria-colindex'] = index; + return props; +} + +export const Cell = ({ enabled, index }) => ; diff --git a/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/spread-helper/output.js b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/spread-helper/output.js new file mode 100644 index 00000000..864a03de --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/spread-helper/output.js @@ -0,0 +1,11 @@ +import { NativeView as _NativeView } from 'react-native-boost/runtime'; +import { View } from 'react-native'; +function getProps(enabled, index) { + if (!enabled) return {}; + const props = { + role: 'cell', + }; + if (index !== undefined) props['aria-colindex'] = index; + return props; +} +export const Cell = ({ enabled, index }) => <_NativeView {...getProps(enabled, index)} />; diff --git a/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/style-passthrough/code.js b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/style-passthrough/code.js new file mode 100644 index 00000000..f5213c85 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/style-passthrough/code.js @@ -0,0 +1,9 @@ +import { View } from 'react-native'; +; diff --git a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/style-passthrough/output.js b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/style-passthrough/output.js similarity index 80% rename from packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/style-passthrough/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/style-passthrough/output.js index f5d2750e..b3a8ffec 100644 --- a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/style-passthrough/output.js +++ b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/style-passthrough/output.js @@ -11,4 +11,7 @@ import { View } from 'react-native'; ]} testID="v" pointerEvents="none" + accessible + accessibilityLabel="box" + nativeID="box" />; diff --git a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/tab-index-dynamic/code.js b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/tab-index-dynamic/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/tab-index-dynamic/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/tab-index-dynamic/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/tab-index-dynamic/output.js b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/tab-index-dynamic/output.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/tab-index-dynamic/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/tab-index-dynamic/output.js diff --git a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/tab-index/code.js b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/tab-index/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/tab-index/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/tab-index/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/tab-index/output.js b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/tab-index/output.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/tab-index/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/tab-index/output.js diff --git a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/text-ancestor/code.js b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/text-ancestor/code.js similarity index 100% rename from packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/text-ancestor/code.js rename to packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/text-ancestor/code.js diff --git a/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/text-ancestor/output.js b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/text-ancestor/output.js new file mode 100644 index 00000000..ed04afd6 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/text-ancestor/output.js @@ -0,0 +1,12 @@ +import { NativeView as _NativeView, NativeViewWithContext as _NativeViewWithContext } from 'react-native-boost/runtime'; +import { Text, View } from 'react-native'; +<> + <_NativeView> + + + + <_NativeViewWithContext> + + + +; diff --git a/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/unknown-imported-ancestor/code.js b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/unknown-imported-ancestor/code.js new file mode 100644 index 00000000..9def063a --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/unknown-imported-ancestor/code.js @@ -0,0 +1,8 @@ +import { View } from 'react-native'; +import { ExternalWrapper } from './ExternalWrapper'; + + + + + +; diff --git a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/unknown-imported-ancestor/output.js b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/unknown-imported-ancestor/dangerous-output.js similarity index 63% rename from packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/unknown-imported-ancestor/output.js rename to packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/unknown-imported-ancestor/dangerous-output.js index 667f4a2b..08e1e5ed 100644 --- a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/unknown-imported-ancestor/output.js +++ b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/unknown-imported-ancestor/dangerous-output.js @@ -1,13 +1,8 @@ import { NativeView as _NativeView } from 'react-native-boost/runtime'; import { View } from 'react-native'; import { ExternalWrapper } from './ExternalWrapper'; -<> + <_NativeView> - + - - - - - -; +; diff --git a/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/unknown-imported-ancestor/output.js b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/unknown-imported-ancestor/output.js new file mode 100644 index 00000000..99f465e1 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/fixtures/unknown-imported-ancestor/output.js @@ -0,0 +1,7 @@ +import { View } from 'react-native'; +import { ExternalWrapper } from './ExternalWrapper'; + + + + +; diff --git a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/index.test.ts b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/index.test.ts similarity index 63% rename from packages/react-native-boost/src/plugin/optimizers/view/__tests__/index.test.ts rename to packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/index.test.ts index 7d303a95..42f73d6b 100644 --- a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/index.test.ts +++ b/packages/react-native-boost/src/plugin/optimizers/native-view/__tests__/index.test.ts @@ -2,10 +2,10 @@ import path from 'node:path'; import { pluginTester } from 'babel-plugin-tester'; import { generateTestPlugin } from '../../../utils/generate-test-plugin'; import { formatTestResult } from '../../../utils/format-test-result'; -import { viewOptimizer } from '..'; +import { nativeViewOptimizer } from '..'; pluginTester({ - plugin: generateTestPlugin(viewOptimizer), + plugin: generateTestPlugin(nativeViewOptimizer), title: 'view', fixtures: path.resolve(import.meta.dirname, 'fixtures'), babelOptions: { @@ -15,10 +15,10 @@ pluginTester({ }); pluginTester({ - plugin: generateTestPlugin(viewOptimizer, { - dangerouslyOptimizeViewWithUnknownAncestors: true, + plugin: generateTestPlugin(nativeViewOptimizer, { + assumptions: { unknownAncestorsDoNotRenderText: true }, }), - title: 'view dangerous unknown ancestors', + title: 'view unknown ancestor assumption', babelOptions: { plugins: ['@babel/plugin-syntax-jsx'], }, @@ -29,11 +29,16 @@ pluginTester({ fixture: path.resolve(import.meta.dirname, 'fixtures/unknown-imported-ancestor/code.js'), outputFixture: path.resolve(import.meta.dirname, 'fixtures/unknown-imported-ancestor/dangerous-output.js'), }, + { + title: 'still preserves known Text context when the assumption is enabled', + fixture: path.resolve(import.meta.dirname, 'fixtures/text-ancestor/code.js'), + outputFixture: path.resolve(import.meta.dirname, 'fixtures/text-ancestor/output.js'), + }, ], }); pluginTester({ - plugin: generateTestPlugin(viewOptimizer, { unistyles: true }), + plugin: generateTestPlugin(nativeViewOptimizer, { integrations: { unistyles: 'on' } }), title: 'view unistyles', fixtures: path.resolve(import.meta.dirname, 'fixtures-unistyles'), babelOptions: { diff --git a/packages/react-native-boost/src/plugin/optimizers/view/index.ts b/packages/react-native-boost/src/plugin/optimizers/native-view/index.ts similarity index 68% rename from packages/react-native-boost/src/plugin/optimizers/view/index.ts rename to packages/react-native-boost/src/plugin/optimizers/native-view/index.ts index 26c13307..6122391e 100644 --- a/packages/react-native-boost/src/plugin/optimizers/view/index.ts +++ b/packages/react-native-boost/src/plugin/optimizers/native-view/index.ts @@ -1,5 +1,5 @@ import { NodePath, types as t } from '@babel/core'; -import { HubFile, Optimizer } from '../../types'; +import type { HubFile, JSXOptimizer } from '../../types'; import PluginError from '../../utils/plugin-error'; import { BailoutCheck, getFirstBailoutReason } from '../../utils/helpers'; import { @@ -11,12 +11,14 @@ import { isIgnoredLine, isReactNativeComponent, replaceWithNativeComponent, - ancestorBailoutChecks, + createTextContextSourceResolver, createStyleOriginResolver, makeAttribute, UNISTYLES_VIEW_HOST, } from '../../utils/common'; import { RUNTIME_MODULE_NAME } from '../../utils/constants'; +import { createJSXOptimizer } from '../../utils/optimizer'; +import { foldAccessibility } from '../../utils/static-accessibility'; /** * Props the `View` wrapper destructures and transforms before handing off to its native host. The @@ -49,6 +51,7 @@ const VIEW_SPREAD_GUARD_KEYS = new Set([ // Unistyles style), so the guard set additionally includes `style`. Precomputed to avoid rebuilding it // per element. const VIEW_SPREAD_GUARD_KEYS_UNISTYLES = new Set([...VIEW_SPREAD_GUARD_KEYS, 'style']); +const VIEW_CHILDREN_PROP = new Set(['children']); // ARIA siblings that trigger aggregation. Their presence routes the whole matching group (including a // passed `accessibilityState`/`accessibilityValue`) through the runtime helper, because the wrapper @@ -56,7 +59,8 @@ const VIEW_SPREAD_GUARD_KEYS_UNISTYLES = new Set([...VIEW_SPREAD_GUARD_KEYS, 'st const ARIA_STATE_PROPERTIES = new Set(['aria-busy', 'aria-checked', 'aria-disabled', 'aria-expanded', 'aria-selected']); const ARIA_VALUE_PROPERTIES = new Set(['aria-valuemax', 'aria-valuemin', 'aria-valuenow', 'aria-valuetext']); -export const viewOptimizer: Optimizer = (path, logger, options, _platform, unistylesEnabled) => { +const optimizeNativeView: JSXOptimizer = (path, { logger, options, unistylesEnabled, platform, reactNativeMinor }) => { + if (platform === 'web' && options?.integrations?.uniwind === 'on') return; if (!isReactNativeComponent(path, 'View')) return; const forced = isForcedLine(path); @@ -67,12 +71,42 @@ export const viewOptimizer: Optimizer = (path, logger, options, _platform, unist const getStyleOrigin = createStyleOriginResolver(path, unistylesEnabled); const spreadGuardKeys = unistylesEnabled ? VIEW_SPREAD_GUARD_KEYS_UNISTYLES : VIEW_SPREAD_GUARD_KEYS; + const getAncestor = createTextContextSourceResolver(path); + const hasVariableTextContext = () => getAncestor() === 'text' || getAncestor() === 'context'; + const needsRuntimeTextContext = () => + hasChildren(path) && + (hasVariableTextContext() || + (options?.assumptions?.unknownAncestorsDoNotRenderText !== true && getAncestor() === 'runtime')); const overridableChecks: BailoutCheck[] = [ { reason: 'has a spread that may carry a translated prop', shouldBail: () => hasBlacklistedPropertyInSpread(path, spreadGuardKeys), }, + { + reason: 'contains an impure prop expression that cannot be safely reordered', + shouldBail: () => + path.node.attributes.length > 1 && + path.node.attributes.some( + (attribute) => + t.isJSXAttribute(attribute) && + t.isJSXIdentifier(attribute.name) && + (attribute.name.name === 'id' || + attribute.name.name === 'tabIndex' || + (attribute.name.name.startsWith('aria-') && VIEW_SPREAD_GUARD_KEYS.has(attribute.name.name))) + ) && + path.node.attributes.some( + (attribute) => + (t.isJSXSpreadAttribute(attribute) && !path.scope.isPure(attribute.argument)) || + (t.isJSXAttribute(attribute) && + t.isJSXExpressionContainer(attribute.value) && + t.isExpression(attribute.value.expression) && + !path.scope.isPure(attribute.value.expression) && + !( + t.isIdentifier(attribute.value.expression, { name: 'undefined' }) && !path.scope.getBinding('undefined') + )) + ), + }, { reason: 'has an unresolved style source that may be a Unistyles style', shouldBail: () => getStyleOrigin() === 'unknown', @@ -81,14 +115,21 @@ export const viewOptimizer: Optimizer = (path, logger, options, _platform, unist reason: 'has both a dynamic `id` and a `nativeID` (ambiguous precedence)', shouldBail: () => hasAmbiguousIdNativeID(path), }, - ...ancestorBailoutChecks(path, options?.dangerouslyOptimizeViewWithUnknownAncestors === true), + { + reason: 'has unresolved runtime parent that may render Text', + shouldBail: () => getStyleOrigin() === 'unistyles' && (hasVariableTextContext() || needsRuntimeTextContext()), + }, + { + reason: 'has unresolved ancestor that may inspect or change children', + shouldBail: () => getAncestor() === 'unknown' && options?.assumptions?.unknownAncestorsDoNotRenderText !== true, + }, ]; if (forced) { const overriddenReason = getFirstBailoutReason(overridableChecks); if (overriddenReason) { - logger.forced({ component: 'View', path, reason: overriddenReason }); + logger.forced({ target: 'View', path, reason: overriddenReason }); } } else { const skipReason = getFirstBailoutReason([ @@ -100,7 +141,7 @@ export const viewOptimizer: Optimizer = (path, logger, options, _platform, unist ]); if (skipReason) { - logger.skipped({ component: 'View', path, reason: skipReason }); + logger.skipped({ target: 'View', path, reason: skipReason }); return; } } @@ -112,22 +153,64 @@ export const viewOptimizer: Optimizer = (path, logger, options, _platform, unist throw new PluginError('No file found in Babel hub'); } + if (options?.integrations?.uniwind === 'on' && getStyleOrigin() !== 'plain') { + logger.skipped({ target: 'View', path, reason: 'Uniwind cannot flatten a Unistyles style' }); + return; + } + + const preservesTextContext = !forced && needsRuntimeTextContext(); logger.optimized({ - component: 'View', + target: 'View', path, + note: preservesTextContext ? 'preserves runtime Text context' : undefined, }); const parent = path.parent as t.JSXElement; - processViewProps(path, file); + if (options?.integrations?.uniwind === 'on') { + const name = preservesTextContext ? 'NativeViewWithContext' : 'NativeView'; + replaceWithNativeComponent(path, parent, file, 'NativeView', { + moduleName: 'react-native-boost/uniwind', + importName: name, + nameHint: `Uniwind${name}`, + }); + return; + } + + processViewProps(path, file, platform, reactNativeMinor); - // A Unistyles-styled View routes to Unistyles' lean host (a registering wrapper around `RCTView`) so - // its shadow-tree registration survives; the `style` already passes through verbatim. Everything else - // optimizes to Boost's own raw host as usual. - const viewHost = getStyleOrigin() === 'unistyles' ? UNISTYLES_VIEW_HOST : undefined; + // Keep Unistyles' host so its shadow-tree registration survives. + const viewHost = + getStyleOrigin() === 'unistyles' + ? UNISTYLES_VIEW_HOST + : preservesTextContext + ? { importName: 'NativeViewWithContext', nameHint: 'NativeViewWithContext' } + : undefined; replaceWithNativeComponent(path, parent, file, 'NativeView', viewHost); }; +export const nativeViewOptimizer = createJSXOptimizer('native-view', optimizeNativeView); + +function hasChildren(path: NodePath): boolean { + if ( + path.node.attributes.some( + (attribute) => t.isJSXAttribute(attribute) && t.isJSXIdentifier(attribute.name, { name: 'children' }) + ) || + hasBlacklistedPropertyInSpread(path, VIEW_CHILDREN_PROP) + ) + return true; + + const parent = path.parent; + return ( + t.isJSXElement(parent) && + parent.children.some( + (child) => + (!t.isJSXText(child) || child.value.trim() !== '') && + (!t.isJSXExpressionContainer(child) || !t.isJSXEmptyExpression(child.expression)) + ) + ); +} + /** * Reproduces the `View` wrapper's ergonomic-prop translation for an optimized element: renames * `id` β†’ `nativeID`, translates the ARIA cluster / `tabIndex` into their native counterparts, and @@ -137,7 +220,12 @@ export const viewOptimizer: Optimizer = (path, logger, options, _platform, unist * Translated props are emitted LAST so they win over any pass-through spread carrying the same native * key β€” mirroring the wrapper, which applies its translations on top of `...otherProps`. */ -function processViewProps(path: NodePath, file: HubFile) { +function processViewProps( + path: NodePath, + file: HubFile, + platform?: string, + reactNativeMinor?: number +) { const currentAttributes = [...path.node.attributes]; const stateGroupTriggered = currentAttributes.some((attribute) => isAriaStateAttribute(attribute)); @@ -151,9 +239,19 @@ function processViewProps(path: NodePath, file: HubFile) { if (!t.isJSXAttribute(attribute) || !t.isJSXIdentifier(attribute.name)) continue; const name = attribute.name.name; - // Aggregation groups β†’ route every present member (including a passed base prop) through the - // helper, but only when an ARIA sibling forces the merge. A lone `accessibilityState`/ - // `accessibilityValue` is value-equal to a pass-through after normalization, so it is left alone. + if ( + (name === 'accessibilityState' && !stateGroupTriggered) || + (name === 'accessibilityValue' && !valueGroupTriggered) + ) { + const folded = foldAccessibility(path, [attribute], 'View', platform, reactNativeMinor); + if (folded) { + literalReplacements.push(...folded); + consumed.add(attribute); + } + continue; + } + + // An ARIA sibling requires the complete state/value group, including the native base prop. if ((name === 'accessibilityState' || ARIA_STATE_PROPERTIES.has(name)) && stateGroupTriggered) { helperBag.push(attribute); consumed.add(attribute); @@ -202,7 +300,10 @@ function processViewProps(path: NodePath, file: HubFile) { } const spreadAttributes: t.JSXSpreadAttribute[] = []; - if (helperBag.length > 0) { + const folded = + helperBag.length > 0 ? foldAccessibility(path, helperBag, 'View', platform, reactNativeMinor) : undefined; + if (folded) literalReplacements.push(...folded); + if (helperBag.length > 0 && !folded) { const helperIdentifier = addFileImportHint({ file, nameHint: 'processViewAccessibilityProps', diff --git a/packages/react-native-boost/src/plugin/optimizers/platform-folding/__tests__/fixtures-android/select/code.js b/packages/react-native-boost/src/plugin/optimizers/platform-folding/__tests__/fixtures-android/select/code.js new file mode 100644 index 00000000..4b981704 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/platform-folding/__tests__/fixtures-android/select/code.js @@ -0,0 +1,9 @@ +import { Platform } from 'react-native'; + +const selected = Platform.select({ + default: 'default', + native: 'native', + android: 'android', +}); +const fallback = Platform.select({ default: 'default', native: 'native' }); +const branch = Platform.OS !== 'android' ? 'other' : 'android'; diff --git a/packages/react-native-boost/src/plugin/optimizers/platform-folding/__tests__/fixtures-android/select/output.js b/packages/react-native-boost/src/plugin/optimizers/platform-folding/__tests__/fixtures-android/select/output.js new file mode 100644 index 00000000..6b9e9c06 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/platform-folding/__tests__/fixtures-android/select/output.js @@ -0,0 +1,4 @@ +import { Platform } from 'react-native'; +const selected = 'android'; +const fallback = 'native'; +const branch = 'android'; diff --git a/packages/react-native-boost/src/plugin/optimizers/platform-folding/__tests__/fixtures/bailouts/code.js b/packages/react-native-boost/src/plugin/optimizers/platform-folding/__tests__/fixtures/bailouts/code.js new file mode 100644 index 00000000..7df5c3d2 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/platform-folding/__tests__/fixtures/bailouts/code.js @@ -0,0 +1,24 @@ +import { Platform } from 'react-native'; +import { Platform as OtherPlatform } from 'other-library'; + +const computed = Platform.select({ [key]: 'computed', ios: 'ios' }); +const spread = Platform.select({ ...spec, ios: 'ios' }); +const getter = Platform.select({ + get ios() { + return 'getter'; + }, +}); +const setter = Platform.select({ set ios(value) {}, default: 'default' }); +const method = Platform.select({ + ios() { + return 'method'; + }, +}); +const prototype = Platform.select({ __proto__: null, ios: 'ios' }); +const extraArgument = Platform.select({ ios: 'ios' }, sideEffect()); +const loose = Platform.OS == 'ios' ? 'yes' : 'no'; +const dynamic = Platform.OS === target ? 'yes' : 'no'; +const version = Platform.Version === 'ios' ? 'yes' : 'no'; +const constants = Platform.constants === 'ios' ? 'yes' : 'no'; +const otherSelected = OtherPlatform.select({ ios: 'ios' }); +const otherBranch = OtherPlatform.OS === 'ios' ? 'yes' : 'no'; diff --git a/packages/react-native-boost/src/plugin/optimizers/platform-folding/__tests__/fixtures/bailouts/output.js b/packages/react-native-boost/src/plugin/optimizers/platform-folding/__tests__/fixtures/bailouts/output.js new file mode 100644 index 00000000..fe3ff2b7 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/platform-folding/__tests__/fixtures/bailouts/output.js @@ -0,0 +1,42 @@ +import { Platform } from 'react-native'; +import { Platform as OtherPlatform } from 'other-library'; +const computed = Platform.select({ + [key]: 'computed', + ios: 'ios', +}); +const spread = Platform.select({ + ...spec, + ios: 'ios', +}); +const getter = Platform.select({ + get ios() { + return 'getter'; + }, +}); +const setter = Platform.select({ + set ios(value) {}, + default: 'default', +}); +const method = Platform.select({ + ios() { + return 'method'; + }, +}); +const prototype = Platform.select({ + __proto__: null, + ios: 'ios', +}); +const extraArgument = Platform.select( + { + ios: 'ios', + }, + sideEffect() +); +const loose = 'ios' == 'ios' ? 'yes' : 'no'; +const dynamic = 'ios' === target ? 'yes' : 'no'; +const version = Platform.Version === 'ios' ? 'yes' : 'no'; +const constants = Platform.constants === 'ios' ? 'yes' : 'no'; +const otherSelected = OtherPlatform.select({ + ios: 'ios', +}); +const otherBranch = OtherPlatform.OS === 'ios' ? 'yes' : 'no'; diff --git a/packages/react-native-boost/src/plugin/optimizers/platform-folding/__tests__/fixtures/hoisted-binding/code.js b/packages/react-native-boost/src/plugin/optimizers/platform-folding/__tests__/fixtures/hoisted-binding/code.js new file mode 100644 index 00000000..3f48999d --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/platform-folding/__tests__/fixtures/hoisted-binding/code.js @@ -0,0 +1,8 @@ +import { Platform } from 'react-native'; + +if (Platform.OS === 'ios') { + installIOS(); +} else { + var installer = require('./install-android'); +} +use(installer); diff --git a/packages/react-native-boost/src/plugin/optimizers/platform-folding/__tests__/fixtures/hoisted-binding/output.js b/packages/react-native-boost/src/plugin/optimizers/platform-folding/__tests__/fixtures/hoisted-binding/output.js new file mode 100644 index 00000000..5416dbae --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/platform-folding/__tests__/fixtures/hoisted-binding/output.js @@ -0,0 +1,7 @@ +import { Platform } from 'react-native'; +if (Platform.OS === 'ios') { + installIOS(); +} else { + var installer = require('./install-android'); +} +use(installer); diff --git a/packages/react-native-boost/src/plugin/optimizers/platform-folding/__tests__/fixtures/if-branches/code.js b/packages/react-native-boost/src/plugin/optimizers/platform-folding/__tests__/fixtures/if-branches/code.js new file mode 100644 index 00000000..9d8c3d9b --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/platform-folding/__tests__/fixtures/if-branches/code.js @@ -0,0 +1,8 @@ +import { Platform } from 'react-native'; + +if (Platform.OS === 'ios') { + require('./install-ios'); +} else { + require('./install-android'); +} +if (Platform.OS !== 'ios') require('./not-ios'); diff --git a/packages/react-native-boost/src/plugin/optimizers/platform-folding/__tests__/fixtures/if-branches/output.js b/packages/react-native-boost/src/plugin/optimizers/platform-folding/__tests__/fixtures/if-branches/output.js new file mode 100644 index 00000000..1490231f --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/platform-folding/__tests__/fixtures/if-branches/output.js @@ -0,0 +1,4 @@ +import { Platform } from 'react-native'; +{ + require('./install-ios'); +} diff --git a/packages/react-native-boost/src/plugin/optimizers/platform-folding/__tests__/fixtures/logical-branches/code.js b/packages/react-native-boost/src/plugin/optimizers/platform-folding/__tests__/fixtures/logical-branches/code.js new file mode 100644 index 00000000..f39a385c --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/platform-folding/__tests__/fixtures/logical-branches/code.js @@ -0,0 +1,6 @@ +import { Platform } from 'react-native'; + +const accessory = Platform.OS === 'ios' && ; +const androidAccessory = Platform.OS === 'android' && missing(); +const fallback = Platform.OS === 'ios' || missing(); +const androidFallback = Platform.OS === 'android' || ; diff --git a/packages/react-native-boost/src/plugin/optimizers/platform-folding/__tests__/fixtures/logical-branches/output.js b/packages/react-native-boost/src/plugin/optimizers/platform-folding/__tests__/fixtures/logical-branches/output.js new file mode 100644 index 00000000..aadf76bf --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/platform-folding/__tests__/fixtures/logical-branches/output.js @@ -0,0 +1,5 @@ +import { Platform } from 'react-native'; +const accessory = ; +const androidAccessory = false; +const fallback = true; +const androidFallback = ; diff --git a/packages/react-native-boost/src/plugin/optimizers/platform-folding/__tests__/fixtures/namespace/code.js b/packages/react-native-boost/src/plugin/optimizers/platform-folding/__tests__/fixtures/namespace/code.js new file mode 100644 index 00000000..f501aed5 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/platform-folding/__tests__/fixtures/namespace/code.js @@ -0,0 +1,3 @@ +import * as ReactNative from 'react-native'; + +const component = ReactNative.Platform.OS !== 'android' ? : ; diff --git a/packages/react-native-boost/src/plugin/optimizers/platform-folding/__tests__/fixtures/namespace/output.js b/packages/react-native-boost/src/plugin/optimizers/platform-folding/__tests__/fixtures/namespace/output.js new file mode 100644 index 00000000..1cbe26ed --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/platform-folding/__tests__/fixtures/namespace/output.js @@ -0,0 +1,2 @@ +import * as ReactNative from 'react-native'; +const component = ; diff --git a/packages/react-native-boost/src/plugin/optimizers/platform-folding/__tests__/fixtures/reads-and-comparisons/code.js b/packages/react-native-boost/src/plugin/optimizers/platform-folding/__tests__/fixtures/reads-and-comparisons/code.js new file mode 100644 index 00000000..23b4ebe1 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/platform-folding/__tests__/fixtures/reads-and-comparisons/code.js @@ -0,0 +1,6 @@ +import { Platform } from 'react-native'; + +const platform = Platform.OS; +const cacheKey = `${Platform.OS}-cache`; +const isIOS = 'ios' === Platform.OS; +const isAndroid = Platform.OS !== 'ios'; diff --git a/packages/react-native-boost/src/plugin/optimizers/platform-folding/__tests__/fixtures/reads-and-comparisons/output.js b/packages/react-native-boost/src/plugin/optimizers/platform-folding/__tests__/fixtures/reads-and-comparisons/output.js new file mode 100644 index 00000000..1512130d --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/platform-folding/__tests__/fixtures/reads-and-comparisons/output.js @@ -0,0 +1,5 @@ +import { Platform } from 'react-native'; +const platform = 'ios'; +const cacheKey = `${'ios'}-cache`; +const isIOS = true; +const isAndroid = false; diff --git a/packages/react-native-boost/src/plugin/optimizers/platform-folding/__tests__/fixtures/select/code.js b/packages/react-native-boost/src/plugin/optimizers/platform-folding/__tests__/fixtures/select/code.js new file mode 100644 index 00000000..5f43e1d8 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/platform-folding/__tests__/fixtures/select/code.js @@ -0,0 +1,10 @@ +import { Platform as RNPlatform } from 'react-native'; + +const selected = RNPlatform.select({ + default: 'default', + native: 'native', + ios: 'ios', +}); +const fallback = RNPlatform.select({ default: 'default', native: 'native' }); +const defaultOnly = RNPlatform.select({ default: 'default' }); +const branch = RNPlatform.OS === 'ios' ? 'yes' : 'no'; diff --git a/packages/react-native-boost/src/plugin/optimizers/platform-folding/__tests__/fixtures/select/output.js b/packages/react-native-boost/src/plugin/optimizers/platform-folding/__tests__/fixtures/select/output.js new file mode 100644 index 00000000..dad84a4d --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/platform-folding/__tests__/fixtures/select/output.js @@ -0,0 +1,5 @@ +import { Platform as RNPlatform } from 'react-native'; +const selected = 'ios'; +const fallback = 'native'; +const defaultOnly = 'default'; +const branch = 'yes'; diff --git a/packages/react-native-boost/src/plugin/optimizers/platform-folding/__tests__/fixtures/writes/code.js b/packages/react-native-boost/src/plugin/optimizers/platform-folding/__tests__/fixtures/writes/code.js new file mode 100644 index 00000000..a54fecdc --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/platform-folding/__tests__/fixtures/writes/code.js @@ -0,0 +1,8 @@ +import { Platform } from 'react-native'; + +Platform.OS = 'android'; +Platform.OS++; +delete Platform.OS; +for (Platform.OS of platforms) { +} +({ platform: Platform.OS } = source); diff --git a/packages/react-native-boost/src/plugin/optimizers/platform-folding/__tests__/fixtures/writes/output.js b/packages/react-native-boost/src/plugin/optimizers/platform-folding/__tests__/fixtures/writes/output.js new file mode 100644 index 00000000..bba6fd08 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/platform-folding/__tests__/fixtures/writes/output.js @@ -0,0 +1,7 @@ +import { Platform } from 'react-native'; +Platform.OS = 'android'; +Platform.OS++; +delete Platform.OS; +for (Platform.OS of platforms) { +} +({ platform: Platform.OS } = source); diff --git a/packages/react-native-boost/src/plugin/optimizers/platform-folding/__tests__/index.test.ts b/packages/react-native-boost/src/plugin/optimizers/platform-folding/__tests__/index.test.ts new file mode 100644 index 00000000..6b7c6dc9 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/platform-folding/__tests__/index.test.ts @@ -0,0 +1,132 @@ +import { createRequire } from 'node:module'; +import path from 'node:path'; +import { transformSync, type PluginObj, type TransformCaller } from '@babel/core'; +import { pluginTester } from 'babel-plugin-tester'; +import { describe, expect, it, vi, type MockInstance } from 'vitest'; +import boostPlugin from '../../../index'; +import { generateTestPlugin } from '../../../utils/generate-test-plugin'; +import { formatTestResult } from '../../../utils/format-test-result'; +import { platformFoldingOptimizer } from '..'; + +const reactNativePackageJson = createRequire(import.meta.url).resolve('react-native/package.json'); +const babelOptions = { plugins: ['@babel/plugin-syntax-jsx'] }; + +pluginTester({ + plugin: generateTestPlugin(platformFoldingOptimizer, {}, 'ios'), + title: 'platform folding ios', + fixtures: path.resolve(import.meta.dirname, 'fixtures'), + babelOptions, + formatResult: formatTestResult, +}); + +pluginTester({ + plugin: generateTestPlugin(platformFoldingOptimizer, {}, 'android'), + title: 'platform folding android', + fixtures: path.resolve(import.meta.dirname, 'fixtures-android'), + babelOptions, + formatResult: formatTestResult, +}); + +function transformPlatform(source: string): string { + return transformSync(source, { + configFile: false, + babelrc: false, + plugins: ['@babel/plugin-syntax-jsx', generateTestPlugin(platformFoldingOptimizer, {}, 'ios')], + })!.code!; +} + +function transformWithBoost(source: string, options: Record = {}): string { + return transformSync(source, { + configFile: false, + babelrc: false, + caller: { name: 'test', platform: 'ios' } as TransformCaller, + plugins: [ + '@babel/plugin-syntax-jsx', + [ + boostPlugin, + { + logLevel: 'silent', + target: { reactNative: { packageJson: reactNativePackageJson } }, + assumptions: { unknownAncestorsDoNotRenderText: true }, + ...options, + }, + ], + ], + })!.code!; +} + +describe('platform folding semantics and integration', () => { + it.each([ + [`import { View } from 'react-native'; const value = ;`, false], + [`import { Platform as Target } from 'react-native'; const value = Target.OS;`, true], + [`import * as Native from 'react-native'; const value = Native.Platform.OS;`, true], + [`const Platform = { OS: 'custom' }; const value = Platform.OS;`, false], + ])('only traverses modules with a supported Platform import: %s', (source, traversed) => { + let visits: MockInstance | undefined; + transformSync(source, { + configFile: false, + babelrc: false, + plugins: [ + '@babel/plugin-syntax-jsx', + (): PluginObj => ({ + visitor: { + Program(program) { + visits = vi.spyOn(program, 'traverse'); + }, + }, + }), + generateTestPlugin(platformFoldingOptimizer, {}, 'ios'), + ], + }); + expect(visits).toHaveBeenCalledTimes(traversed ? 1 : 0); + }); + + it('preserves discarded value evaluations, order, duplicates, and present undefined', () => { + const output = transformPlatform(` + import { Platform } from 'react-native'; + const calls = []; + const record = (name, value) => { calls.push(name); return value; }; + const selected = Platform.select({ + ios: record('first ios', 'first'), + android: record('android', 'android'), + ios: record('second ios', undefined), + native: record('native', 'native'), + }); + globalThis.result = { calls, selected }; + `); + const executable = output.replace(/^import .*;\n/, ''); + const globalObject: { result?: { calls: string[]; selected: unknown } } = {}; + new Function('globalThis', executable)(globalObject); + + expect(globalObject.result).toEqual({ + calls: ['first ios', 'android', 'second ios', 'native'], + selected: undefined, + }); + expect(output).not.toContain('Platform.select'); + }); + + it('respects the optimization setting', () => { + const output = transformWithBoost( + `import { Platform } from 'react-native'; const value = Platform.select({ ios: 'ios' });`, + { optimizations: { 'platform-folding': 'off' } } + ); + + expect(output).toContain('Platform.select({'); + }); + + it('runs before the component optimizers', () => { + const output = transformWithBoost(` + import { Platform, StyleSheet, Text, View } from 'react-native'; + const style = Platform.select({ ios: StyleSheet.flatten([{ opacity: 1 }]), android: { opacity: 2 } }); + const component = Platform.OS === 'ios' ? ready : ; + const styled = styled; + `); + + expect(output).toContain('StyleSheet.flatten, {'); + expect(output).toContain('<_NativeText'); + expect(output).toContain('opacity: 1'); + expect(output).not.toContain('processTextStyle'); + expect(output).not.toContain('Platform.select'); + expect(output).not.toContain('Platform.OS'); + }); +}); diff --git a/packages/react-native-boost/src/plugin/optimizers/platform-folding/index.ts b/packages/react-native-boost/src/plugin/optimizers/platform-folding/index.ts new file mode 100644 index 00000000..cafbe855 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/platform-folding/index.ts @@ -0,0 +1,237 @@ +import { NodePath, types as t } from '@babel/core'; +import type { Optimizer, OptimizerState, TargetPlatform } from '../../types'; +import { isIgnoredLine, isModuleImportBinding, isStaticLiteralTree } from '../../utils/common'; + +const platformFoldingVisitor = { + CallExpression(path: NodePath, state: OptimizerState) { + if (!isPlatformSelect(path)) return; + + const { logger, platform } = state.optimizerContext; + if (!platform) { + logger.skipped({ target: 'Platform.select', path, reason: 'target platform is unknown' }); + return; + } + if (isIgnoredLine(path)) return; + + const replacement = buildSelectReplacement(path, platform); + if (!replacement) { + logger.skipped({ target: 'Platform.select', path, reason: 'select spec is not a plain object literal' }); + return; + } + + t.inheritsComments(replacement, path.node); + path.replaceWith(replacement); + logger.optimized({ target: 'Platform.select', path }); + }, + IfStatement(path: NodePath, state: OptimizerState) { + const comparison = getPlatformComparison(path, path.node.test); + if (!comparison) return; + + const platform = getTargetPlatform(path, state); + if (!platform) return; + + const matches = platformMatches(comparison, platform); + const branch = path.get(matches ? 'consequent' : 'alternate'); + const discarded = path.get(matches ? 'alternate' : 'consequent'); + if (discarded.node && hasHoistedDeclaration(discarded as NodePath)) { + path.skip(); + return; + } + + state.optimizerContext.logger.optimized({ target: 'Platform.OS', path }); + if (branch.node) { + const replacement = t.cloneNode(branch.node, true); + t.inheritsComments(replacement, path.node); + path.replaceWith(replacement); + } else { + path.remove(); + } + }, + LogicalExpression(path: NodePath, state: OptimizerState) { + if (path.node.operator !== '&&' && path.node.operator !== '||') return; + const comparison = getPlatformComparison(path, path.node.left); + if (!comparison) return; + + const platform = getTargetPlatform(path, state); + if (!platform) return; + + const left = platformMatches(comparison, platform); + const replacement = (path.node.operator === '&&' ? left : !left) + ? t.cloneNode(path.node.right, true) + : t.booleanLiteral(left); + t.inheritsComments(replacement, path.node); + state.optimizerContext.logger.optimized({ target: 'Platform.OS', path }); + path.replaceWith(replacement); + }, + ConditionalExpression(path: NodePath, state: OptimizerState) { + const comparison = getPlatformComparison(path, path.node.test); + if (!comparison) return; + + const platform = getTargetPlatform(path, state); + if (!platform) return; + + const replacement = t.cloneNode( + platformMatches(comparison, platform) ? path.node.consequent : path.node.alternate, + true + ); + t.inheritsComments(replacement, path.node); + state.optimizerContext.logger.optimized({ target: 'Platform.OS', path }); + path.replaceWith(replacement); + }, + BinaryExpression(path: NodePath, state: OptimizerState) { + const comparison = getPlatformComparison(path, path.node); + if (!comparison) return; + + const platform = getTargetPlatform(path, state); + if (!platform) return; + + state.optimizerContext.logger.optimized({ target: 'Platform.OS', path }); + path.replaceWith(t.booleanLiteral(platformMatches(comparison, platform))); + }, + MemberExpression(path: NodePath, state: OptimizerState) { + if (!isPlatformOSReference(path, path.node) || !isReadOnly(path)) return; + + const platform = getTargetPlatform(path, state); + if (!platform) return; + + state.optimizerContext.logger.optimized({ target: 'Platform.OS', path }); + path.replaceWith(t.stringLiteral(platform)); + }, +}; + +export const platformFoldingOptimizer: Optimizer = { + name: 'platform-folding', + visitor: { + Program(path, state) { + if (!state.enabledOptimizations.has('platform-folding')) return; + if ( + !Object.values(path.scope.bindings).some( + (binding) => + isModuleImportBinding(binding, 'react-native', t.isImportSpecifier, 'Platform') || + isModuleImportBinding(binding, 'react-native', t.isImportNamespaceSpecifier) + ) + ) + return; + // Fold before JSXOpeningElement optimizers inspect their attributes and children. + path.traverse(platformFoldingVisitor, state); + }, + }, +}; + +function isPlatformSelect(path: NodePath): boolean { + const callee = path.node.callee; + return ( + t.isMemberExpression(callee) && + !callee.computed && + t.isIdentifier(callee.property, { name: 'select' }) && + isPlatformReference(path, callee.object) + ); +} + +type PlatformComparison = { operator: '===' | '!=='; value: string }; + +function getPlatformComparison(path: NodePath, expression: t.Node): PlatformComparison | undefined { + if (!t.isBinaryExpression(expression) || (expression.operator !== '===' && expression.operator !== '!==')) return; + + if (isPlatformOSReference(path, expression.left) && t.isStringLiteral(expression.right)) { + return { operator: expression.operator, value: expression.right.value }; + } + if (isPlatformOSReference(path, expression.right) && t.isStringLiteral(expression.left)) { + return { operator: expression.operator, value: expression.left.value }; + } +} + +function platformMatches(comparison: PlatformComparison, platform: TargetPlatform): boolean { + return (comparison.value === platform) === (comparison.operator === '==='); +} + +function getTargetPlatform(path: NodePath, state: OptimizerState): TargetPlatform | undefined { + const { logger, platform } = state.optimizerContext; + if (!platform) { + logger.skipped({ target: 'Platform.OS', path, reason: 'target platform is unknown' }); + return; + } + if (!isIgnoredLine(path)) return platform; +} + +function buildSelectReplacement(path: NodePath, platform: TargetPlatform): t.Expression | undefined { + const [argument] = path.node.arguments; + if (path.node.arguments.length !== 1 || !t.isObjectExpression(argument)) return; + + const properties = new Map(); + for (const property of argument.properties) { + if (!t.isObjectProperty(property) || property.computed) return; + + const name = getStaticPropertyName(property.key); + if (name === '__proto__') return; + if (name !== undefined) properties.set(name, property); + } + + const selected = properties.get(platform) ?? properties.get('native') ?? properties.get('default'); + const hasUnsafeDiscardedValue = argument.properties.some( + (property) => property !== selected && t.isObjectProperty(property) && !isStaticLiteralTree(property.value) + ); + if (hasUnsafeDiscardedValue) { + const property = t.stringLiteral(selected ? getStaticPropertyName(selected.key)! : platform); + return t.memberExpression(t.cloneNode(argument, true), property, true); + } + if (selected) return t.cloneNode(selected.value, true) as t.Expression; + return t.unaryExpression('void', t.numericLiteral(0)); +} + +function isPlatformOSReference(path: NodePath, expression: t.Node): boolean { + return ( + t.isMemberExpression(expression) && + !expression.computed && + t.isIdentifier(expression.property, { name: 'OS' }) && + isPlatformReference(path, expression.object) + ); +} + +function hasHoistedDeclaration(path: NodePath): boolean { + let found = t.isVariableDeclaration(path.node, { kind: 'var' }) || t.isFunctionDeclaration(path.node); + path.traverse({ + Function(functionPath) { + if (functionPath.isFunctionDeclaration()) found = true; + functionPath.skip(); + }, + VariableDeclaration(variablePath) { + if (variablePath.node.kind === 'var') found = true; + }, + }); + return found; +} + +function isReadOnly(path: NodePath): boolean { + const parent = path.parent; + if (!path.isReferenced()) return false; + if (path.key === 'value' && t.isObjectProperty(parent) && t.isObjectPattern(path.parentPath.parent)) return false; + if (t.isUpdateExpression(parent) || (t.isUnaryExpression(parent) && parent.operator === 'delete')) return false; + if ((t.isForInStatement(parent) || t.isForOfStatement(parent)) && parent.left === path.node) return false; + return true; +} + +function isPlatformReference(path: NodePath, expression: t.Node): boolean { + if (t.isIdentifier(expression)) { + const binding = path.scope.getBinding(expression.name); + return isModuleImportBinding(binding, 'react-native', t.isImportSpecifier, 'Platform'); + } + + if ( + !t.isMemberExpression(expression) || + expression.computed || + !t.isIdentifier(expression.object) || + !t.isIdentifier(expression.property, { name: 'Platform' }) + ) { + return false; + } + + const binding = path.scope.getBinding(expression.object.name); + return isModuleImportBinding(binding, 'react-native', t.isImportNamespaceSpecifier); +} + +function getStaticPropertyName(key: t.Node): string | undefined { + if (t.isIdentifier(key)) return key.name; + if (t.isStringLiteral(key)) return key.value; + return; +} diff --git a/packages/react-native-boost/src/plugin/optimizers/stylesheet-operations/__tests__/fixtures/bindings/code.js b/packages/react-native-boost/src/plugin/optimizers/stylesheet-operations/__tests__/fixtures/bindings/code.js new file mode 100644 index 00000000..5d4dc46c --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/stylesheet-operations/__tests__/fixtures/bindings/code.js @@ -0,0 +1,5 @@ +import { StyleSheet as RNStyleSheet } from 'react-native'; +import { StyleSheet } from 'other-library'; + +const optimized = RNStyleSheet.flatten([{ opacity: 1 }]); +const untouched = StyleSheet.flatten([{ opacity: 1 }]); diff --git a/packages/react-native-boost/src/plugin/optimizers/stylesheet-operations/__tests__/fixtures/bindings/output.js b/packages/react-native-boost/src/plugin/optimizers/stylesheet-operations/__tests__/fixtures/bindings/output.js new file mode 100644 index 00000000..514dced0 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/stylesheet-operations/__tests__/fixtures/bindings/output.js @@ -0,0 +1,12 @@ +import { StyleSheet as RNStyleSheet } from 'react-native'; +import { StyleSheet } from 'other-library'; +const optimized = + (RNStyleSheet.flatten, + { + opacity: 1, + }); +const untouched = StyleSheet.flatten([ + { + opacity: 1, + }, +]); diff --git a/packages/react-native-boost/src/plugin/optimizers/stylesheet-operations/__tests__/fixtures/compose/code.js b/packages/react-native-boost/src/plugin/optimizers/stylesheet-operations/__tests__/fixtures/compose/code.js new file mode 100644 index 00000000..53348a1f --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/stylesheet-operations/__tests__/fixtures/compose/code.js @@ -0,0 +1,9 @@ +import { StyleSheet } from 'react-native'; + +export function composeWithNull(style) { + return StyleSheet.compose(null, style); +} + +export function composeStatic() { + return StyleSheet.compose({ color: 'red' }, { opacity: 1 }); +} diff --git a/packages/react-native-boost/src/plugin/optimizers/stylesheet-operations/__tests__/fixtures/compose/output.js b/packages/react-native-boost/src/plugin/optimizers/stylesheet-operations/__tests__/fixtures/compose/output.js new file mode 100644 index 00000000..8a521f49 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/stylesheet-operations/__tests__/fixtures/compose/output.js @@ -0,0 +1,17 @@ +import { StyleSheet } from 'react-native'; +export function composeWithNull(style) { + return (StyleSheet.compose, style); +} +export function composeStatic() { + return ( + StyleSheet.compose, + [ + { + color: 'red', + }, + { + opacity: 1, + }, + ] + ); +} diff --git a/packages/react-native-boost/src/plugin/optimizers/stylesheet-operations/__tests__/fixtures/dynamic-and-force/code.js b/packages/react-native-boost/src/plugin/optimizers/stylesheet-operations/__tests__/fixtures/dynamic-and-force/code.js new file mode 100644 index 00000000..635d119e --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/stylesheet-operations/__tests__/fixtures/dynamic-and-force/code.js @@ -0,0 +1,19 @@ +import { StyleSheet } from 'react-native'; + +function regularFlatten(styles) { + return StyleSheet.flatten(styles); +} + +function regularCompose(first, second) { + return StyleSheet.compose(first, second); +} + +function forcedFlatten(styles) { + /* @boost-force */ + return StyleSheet.flatten(styles); +} + +function forcedCompose(first, second) { + /* @boost-force */ + return StyleSheet.compose(first, second); +} diff --git a/packages/react-native-boost/src/plugin/optimizers/stylesheet-operations/__tests__/fixtures/dynamic-and-force/output.js b/packages/react-native-boost/src/plugin/optimizers/stylesheet-operations/__tests__/fixtures/dynamic-and-force/output.js new file mode 100644 index 00000000..97fa2ae8 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/stylesheet-operations/__tests__/fixtures/dynamic-and-force/output.js @@ -0,0 +1,15 @@ +import { StyleSheet } from 'react-native'; +function regularFlatten(styles) { + return StyleSheet.flatten(styles); +} +function regularCompose(first, second) { + return StyleSheet.compose(first, second); +} +function forcedFlatten(styles) { + /* @boost-force */ + return (StyleSheet.flatten, styles); +} +function forcedCompose(first, second) { + /* @boost-force */ + return (StyleSheet.compose, [first, second]); +} diff --git a/packages/react-native-boost/src/plugin/optimizers/stylesheet-operations/__tests__/fixtures/flatten/code.js b/packages/react-native-boost/src/plugin/optimizers/stylesheet-operations/__tests__/fixtures/flatten/code.js new file mode 100644 index 00000000..9a16094a --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/stylesheet-operations/__tests__/fixtures/flatten/code.js @@ -0,0 +1,10 @@ +import { StyleSheet } from 'react-native'; + +function makeStyle() { + return StyleSheet.flatten([ + { padding: 8, color: 'red', transform: [{ scale: 2 }] }, + null, + false, + [{ color: 'blue' }, { opacity: 0.5 }], + ]); +} diff --git a/packages/react-native-boost/src/plugin/optimizers/stylesheet-operations/__tests__/fixtures/flatten/output.js b/packages/react-native-boost/src/plugin/optimizers/stylesheet-operations/__tests__/fixtures/flatten/output.js new file mode 100644 index 00000000..44f1b362 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/stylesheet-operations/__tests__/fixtures/flatten/output.js @@ -0,0 +1,16 @@ +import { StyleSheet } from 'react-native'; +function makeStyle() { + return ( + StyleSheet.flatten, + { + padding: 8, + color: 'blue', + transform: [ + { + scale: 2, + }, + ], + opacity: 0.5, + } + ); +} diff --git a/packages/react-native-boost/src/plugin/optimizers/stylesheet-operations/__tests__/fixtures/ignore-and-shadowed-undefined/code.js b/packages/react-native-boost/src/plugin/optimizers/stylesheet-operations/__tests__/fixtures/ignore-and-shadowed-undefined/code.js new file mode 100644 index 00000000..4bcade78 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/stylesheet-operations/__tests__/fixtures/ignore-and-shadowed-undefined/code.js @@ -0,0 +1,8 @@ +import { StyleSheet } from 'react-native'; + +/* @boost-ignore */ +const ignored = StyleSheet.flatten([{ color: 'red' }]); + +function flatten(undefined) { + return StyleSheet.flatten([undefined, { opacity: 1 }]); +} diff --git a/packages/react-native-boost/src/plugin/optimizers/stylesheet-operations/__tests__/fixtures/ignore-and-shadowed-undefined/output.js b/packages/react-native-boost/src/plugin/optimizers/stylesheet-operations/__tests__/fixtures/ignore-and-shadowed-undefined/output.js new file mode 100644 index 00000000..51b5d67e --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/stylesheet-operations/__tests__/fixtures/ignore-and-shadowed-undefined/output.js @@ -0,0 +1,16 @@ +import { StyleSheet } from 'react-native'; + +/* @boost-ignore */ +const ignored = StyleSheet.flatten([ + { + color: 'red', + }, +]); +function flatten(undefined) { + return StyleSheet.flatten([ + undefined, + { + opacity: 1, + }, + ]); +} diff --git a/packages/react-native-boost/src/plugin/optimizers/stylesheet-operations/__tests__/index.test.ts b/packages/react-native-boost/src/plugin/optimizers/stylesheet-operations/__tests__/index.test.ts new file mode 100644 index 00000000..04015dac --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/stylesheet-operations/__tests__/index.test.ts @@ -0,0 +1,55 @@ +import { createRequire } from 'node:module'; +import path from 'node:path'; +import { transformSync, type TransformCaller } from '@babel/core'; +import { pluginTester } from 'babel-plugin-tester'; +import { describe, expect, it } from 'vitest'; +import boostPlugin from '../../../index'; +import { generateTestPlugin } from '../../../utils/generate-test-plugin'; +import { formatTestResult } from '../../../utils/format-test-result'; +import { stylesheetOperationsOptimizer } from '..'; + +const reactNativePackageJson = createRequire(import.meta.url).resolve('react-native/package.json'); + +pluginTester({ + plugin: generateTestPlugin(stylesheetOperationsOptimizer, {}, 'ios'), + title: 'stylesheet operations', + fixtures: path.resolve(import.meta.dirname, 'fixtures'), + formatResult: formatTestResult, +}); + +function transformWithBoost( + source: string, + options: Record = {}, + filename = '/app/source.js' +): string { + return transformSync(source, { + configFile: false, + babelrc: false, + filename, + caller: { name: 'test', platform: 'ios' } as TransformCaller, + plugins: [ + [ + boostPlugin, + { + logLevel: 'silent', + target: { reactNative: { packageJson: reactNativePackageJson } }, + ...options, + }, + ], + ], + })!.code!; +} + +describe('stylesheet operations integration', () => { + it('runs through the general plugin visitor and respects configuration and file ignores', () => { + const source = `import { StyleSheet } from 'react-native';\nconst style = StyleSheet.flatten([{ opacity: 1 }]);`; + + expect(transformWithBoost(source)).toContain('StyleSheet.flatten, {'); + expect(transformWithBoost(source, { optimizations: { 'stylesheet-operations': 'off' } })).toContain( + 'StyleSheet.flatten([{' + ); + expect(transformWithBoost(source, { ignores: ['/app/ignored.js'] }, '/app/ignored.js')).toContain( + 'StyleSheet.flatten([{' + ); + }); +}); diff --git a/packages/react-native-boost/src/plugin/optimizers/stylesheet-operations/index.ts b/packages/react-native-boost/src/plugin/optimizers/stylesheet-operations/index.ts new file mode 100644 index 00000000..119ea923 --- /dev/null +++ b/packages/react-native-boost/src/plugin/optimizers/stylesheet-operations/index.ts @@ -0,0 +1,134 @@ +import { NodePath, types as t } from '@babel/core'; +import type { Optimizer } from '../../types'; +import { isForcedLine, isIgnoredLine, isStaticLiteralTree, tryFlattenStaticStyle } from '../../utils/common'; + +type StyleSheetOperation = 'compose' | 'flatten'; + +export const stylesheetOperationsOptimizer: Optimizer = { + name: 'stylesheet-operations', + visitor: { + CallExpression(path, state) { + if (!state.enabledOptimizations.has('stylesheet-operations')) return; + + const operation = getStyleSheetOperation(path); + if (!operation) return; + + const { logger, platform } = state.optimizerContext; + const target = `StyleSheet.${operation}`; + if (platform !== 'ios' && platform !== 'android') { + logger.skipped({ target, path, reason: 'target platform is unknown' }); + return; + } + + const forced = isForcedLine(path); + if (!forced && isIgnoredLine(path)) { + logger.skipped({ target, path, reason: 'line is marked with @boost-ignore' }); + return; + } + + const staticReplacement = buildStaticReplacement(path, operation); + const replacement = staticReplacement ?? (forced ? buildForcedReplacement(path, operation) : undefined); + if (!replacement) { + const reason = forced ? 'call has unsupported arguments' : 'result cannot be resolved at build time'; + logger.skipped({ target, path, reason }); + return; + } + if (forced && !staticReplacement) { + logger.forced({ target, path, reason: 'result cannot be resolved at build time' }); + } + + const result = t.sequenceExpression([t.cloneNode(path.node.callee as t.MemberExpression, true), replacement]); + t.inheritsComments(result, path.node); + logger.optimized({ target, path }); + path.replaceWith(result); + }, + }, +}; + +function getStyleSheetOperation(path: NodePath): StyleSheetOperation | undefined { + const callee = path.node.callee; + if ( + !t.isMemberExpression(callee) || + callee.computed || + !t.isIdentifier(callee.object) || + !t.isIdentifier(callee.property) || + (callee.property.name !== 'compose' && callee.property.name !== 'flatten') + ) { + return; + } + + const binding = path.scope.getBinding(callee.object.name); + if ( + binding?.kind !== 'module' || + !t.isImportSpecifier(binding.path.node) || + !t.isIdentifier(binding.path.node.imported, { name: 'StyleSheet' }) || + !t.isImportDeclaration(binding.path.parent) || + binding.path.parent.source.value !== 'react-native' + ) { + return; + } + + return callee.property.name; +} + +function buildStaticReplacement( + path: NodePath, + operation: StyleSheetOperation +): t.Expression | undefined { + const arguments_ = path.node.arguments; + if (operation === 'flatten') { + if (arguments_.length !== 1 || !t.isExpression(arguments_[0])) return; + return buildStaticFlatten(path, arguments_[0]); + } + + if (arguments_.length !== 2 || !t.isExpression(arguments_[0]) || !t.isExpression(arguments_[1])) return; + const [first, second] = arguments_; + if (isStaticNullish(path, first)) return t.cloneNode(second, true); + if (isStaticNullish(path, second)) return t.cloneNode(first, true); + if (isStaticNonNullish(first) && isStaticNonNullish(second)) { + return t.arrayExpression([t.cloneNode(first, true), t.cloneNode(second, true)]); + } +} + +function buildStaticFlatten(path: NodePath, expression: t.Expression): t.Expression | undefined { + const flattened = tryFlattenStaticStyle(expression, path.scope.getBinding('undefined') === undefined); + if (flattened) { + return t.objectExpression( + [...flattened].map(([name, value]) => + t.objectProperty(t.isValidIdentifier(name, false) ? t.identifier(name) : t.stringLiteral(name), value) + ) + ); + } + + if (isStaticNullish(path, expression) || isStaticNonObjectLiteral(expression)) { + return t.unaryExpression('void', t.numericLiteral(0)); + } +} + +function buildForcedReplacement( + path: NodePath, + operation: StyleSheetOperation +): t.Expression | undefined { + const arguments_ = path.node.arguments; + if (operation === 'flatten') { + return arguments_.length === 1 && t.isExpression(arguments_[0]) ? t.cloneNode(arguments_[0], true) : undefined; + } + if (arguments_.length !== 2 || !t.isExpression(arguments_[0]) || !t.isExpression(arguments_[1])) return; + return t.arrayExpression([t.cloneNode(arguments_[0], true), t.cloneNode(arguments_[1], true)]); +} + +function isStaticNullish(path: NodePath, expression: t.Expression): boolean { + return ( + t.isNullLiteral(expression) || + (t.isIdentifier(expression, { name: 'undefined' }) && path.scope.getBinding('undefined') === undefined) || + (t.isUnaryExpression(expression, { operator: 'void' }) && t.isNumericLiteral(expression.argument)) + ); +} + +function isStaticNonNullish(expression: t.Expression): boolean { + return !t.isNullLiteral(expression) && isStaticLiteralTree(expression); +} + +function isStaticNonObjectLiteral(expression: t.Expression): boolean { + return isStaticNonNullish(expression) && !t.isObjectExpression(expression) && !t.isArrayExpression(expression); +} diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/complex-example/code.js b/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/complex-example/code.js deleted file mode 100644 index 634dec68..00000000 --- a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/complex-example/code.js +++ /dev/null @@ -1,18 +0,0 @@ -import { Text } from 'react-native'; - -export default function TextBenchmark(props) { - const optimizedViews = Array.from({ length: props.count }, (_, index) => Nice text); - const unoptimizedViews = Array.from({ length: props.count }, (_, index) => ( - // @boost-ignore - Nice text - )); - - if (props.status === 'pending') return Pending...; - - return ( - - {optimizedViews} - {unoptimizedViews} - - ); -} diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/complex-example/output.js b/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/complex-example/output.js deleted file mode 100644 index b9b0316f..00000000 --- a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/complex-example/output.js +++ /dev/null @@ -1,25 +0,0 @@ -import { Text } from 'react-native'; -export default function TextBenchmark(props) { - const optimizedViews = Array.from( - { - length: props.count, - }, - (_, index) => Nice text - ); - const unoptimizedViews = Array.from( - { - length: props.count, - }, - (_, index) => ( - // @boost-ignore - Nice text - ) - ); - if (props.status === 'pending') return Pending...; - return ( - - {optimizedViews} - {unoptimizedViews} - - ); -} diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/expo-router-link-alias-as-child/output.js b/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/expo-router-link-alias-as-child/output.js deleted file mode 100644 index 942a04d0..00000000 --- a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/expo-router-link-alias-as-child/output.js +++ /dev/null @@ -1,22 +0,0 @@ -import { - getDefaultTextStyle as _getDefaultTextStyle, - getDefaultTextAccessible as _getDefaultTextAccessible, - NativeText as _NativeText, -} from 'react-native-boost/runtime'; -import { Text } from 'react-native'; -import { Link as RouterLink } from 'expo-router'; -<> - <_NativeText - style={_getDefaultTextStyle()} - allowFontScaling={true} - ellipsizeMode={'tail'} - accessible={_getDefaultTextAccessible()}> - This should be optimized - - - This should NOT be optimized due to aliased Link asChild - - - Direct child of aliased Link without asChild - -; diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/expo-router-link-as-child-nested-view/output.js b/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/expo-router-link-as-child-nested-view/output.js deleted file mode 100644 index 6d59f278..00000000 --- a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/expo-router-link-as-child-nested-view/output.js +++ /dev/null @@ -1,7 +0,0 @@ -import { Text, View } from 'react-native'; -import { Link } from 'expo-router'; - - - Inside a View under an asChild Link - -; diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/expo-router-link-as-child/output.js b/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/expo-router-link-as-child/output.js deleted file mode 100644 index 803d7a4e..00000000 --- a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/expo-router-link-as-child/output.js +++ /dev/null @@ -1,25 +0,0 @@ -import { - getDefaultTextStyle as _getDefaultTextStyle, - getDefaultTextAccessible as _getDefaultTextAccessible, - NativeText as _NativeText, -} from 'react-native-boost/runtime'; -import { Text } from 'react-native'; -import { Link } from 'expo-router'; -<> - <_NativeText - style={_getDefaultTextStyle()} - allowFontScaling={true} - ellipsizeMode={'tail'} - accessible={_getDefaultTextAccessible()}> - This should be optimized - - - This should NOT be optimized due to Link asChild - - - Direct child of Link without asChild - - - This should NOT be optimized (Link with href and asChild) - -; diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/mixed-children-types/code.js b/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/mixed-children-types/code.js deleted file mode 100644 index 446fd0ac..00000000 --- a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/mixed-children-types/code.js +++ /dev/null @@ -1,6 +0,0 @@ -import { Text } from 'react-native'; -const name = 'John'; -Hello {name}!; - - Click here: -; diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/mixed-children-types/output.js b/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/mixed-children-types/output.js deleted file mode 100644 index f7c455ff..00000000 --- a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/mixed-children-types/output.js +++ /dev/null @@ -1,17 +0,0 @@ -import { - getDefaultTextStyle as _getDefaultTextStyle, - getDefaultTextAccessible as _getDefaultTextAccessible, - NativeText as _NativeText, -} from 'react-native-boost/runtime'; -import { Text } from 'react-native'; -const name = 'John'; -<_NativeText - style={_getDefaultTextStyle()} - allowFontScaling={true} - ellipsizeMode={'tail'} - accessible={_getDefaultTextAccessible()}> - Hello {name}! -; - - Click here: -; diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/numeric-concat-child/code.js b/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/numeric-concat-child/code.js deleted file mode 100644 index 47a357d5..00000000 --- a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/numeric-concat-child/code.js +++ /dev/null @@ -1,2 +0,0 @@ -import { Text } from 'react-native'; -{1 + 2}; diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/numeric-concat-child/output.js b/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/numeric-concat-child/output.js deleted file mode 100644 index 59815ea0..00000000 --- a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/numeric-concat-child/output.js +++ /dev/null @@ -1,13 +0,0 @@ -import { - getDefaultTextStyle as _getDefaultTextStyle, - getDefaultTextAccessible as _getDefaultTextAccessible, - NativeText as _NativeText, -} from 'react-native-boost/runtime'; -import { Text } from 'react-native'; -<_NativeText - style={_getDefaultTextStyle()} - allowFontScaling={true} - ellipsizeMode={'tail'} - accessible={_getDefaultTextAccessible()}> - {1 + 2} -; diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/pressables/code.js b/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/pressables/code.js deleted file mode 100644 index d2788b1d..00000000 --- a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/pressables/code.js +++ /dev/null @@ -1,8 +0,0 @@ -import { Text } from 'react-native'; -Hello, world!; - { - console.log('pressed'); - }}> - Hello, world! -; diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/pressables/output.js b/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/pressables/output.js deleted file mode 100644 index d04ae156..00000000 --- a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/pressables/output.js +++ /dev/null @@ -1,19 +0,0 @@ -import { - getDefaultTextStyle as _getDefaultTextStyle, - getDefaultTextAccessible as _getDefaultTextAccessible, - NativeText as _NativeText, -} from 'react-native-boost/runtime'; -import { Text } from 'react-native'; -<_NativeText - style={_getDefaultTextStyle()} - allowFontScaling={true} - ellipsizeMode={'tail'} - accessible={_getDefaultTextAccessible()}> - Hello, world! -; - { - console.log('pressed'); - }}> - Hello, world! -; diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/resolvable-spread-props/output.js b/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/resolvable-spread-props/output.js deleted file mode 100644 index 94d89a07..00000000 --- a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/resolvable-spread-props/output.js +++ /dev/null @@ -1,16 +0,0 @@ -import { - getDefaultTextStyle as _getDefaultTextStyle, - getDefaultTextAccessible as _getDefaultTextAccessible, - NativeText as _NativeText, -} from 'react-native-boost/runtime'; -import { Text } from 'react-native'; -const props = { - children: 'Hello, world!', -}; -<_NativeText - style={_getDefaultTextStyle()} - {...props} - allowFontScaling={true} - ellipsizeMode={'tail'} - accessible={_getDefaultTextAccessible()} -/>; diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/static-style-nested/output.js b/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/static-style-nested/output.js deleted file mode 100644 index fde84748..00000000 --- a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/static-style-nested/output.js +++ /dev/null @@ -1,25 +0,0 @@ -import { - getDefaultTextStyle as _getDefaultTextStyle, - getDefaultTextAccessible as _getDefaultTextAccessible, - NativeText as _NativeText, -} from 'react-native-boost/runtime'; -import { Text } from 'react-native'; -<_NativeText - style={[ - _getDefaultTextStyle(), - { - shadowOffset: { - width: 1, - height: 1, - }, - transform: [ - { - scale: 2, - }, - ], - }, - ]} - allowFontScaling={true} - ellipsizeMode={'tail'} - accessible={_getDefaultTextAccessible()} -/>; diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/static-style-user-select-overrides-selectable/output.js b/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/static-style-user-select-overrides-selectable/output.js deleted file mode 100644 index e560966d..00000000 --- a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/static-style-user-select-overrides-selectable/output.js +++ /dev/null @@ -1,18 +0,0 @@ -import { - getDefaultTextStyle as _getDefaultTextStyle, - getDefaultTextAccessible as _getDefaultTextAccessible, - NativeText as _NativeText, -} from 'react-native-boost/runtime'; -import { Text } from 'react-native'; -<_NativeText - style={[ - _getDefaultTextStyle(), - { - color: 'red', - }, - ]} - allowFontScaling={true} - ellipsizeMode={'tail'} - selectable={true} - accessible={_getDefaultTextAccessible()} -/>; diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/static-style-vertical-align/output.js b/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/static-style-vertical-align/output.js deleted file mode 100644 index f2afff3e..00000000 --- a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/static-style-vertical-align/output.js +++ /dev/null @@ -1,17 +0,0 @@ -import { - getDefaultTextStyle as _getDefaultTextStyle, - getDefaultTextAccessible as _getDefaultTextAccessible, - NativeText as _NativeText, -} from 'react-native-boost/runtime'; -import { Text } from 'react-native'; -<_NativeText - style={[ - _getDefaultTextStyle(), - { - textAlignVertical: 'center', - }, - ]} - allowFontScaling={true} - ellipsizeMode={'tail'} - accessible={_getDefaultTextAccessible()} -/>; diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/two-components/code.js b/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/two-components/code.js deleted file mode 100644 index 929f7ac4..00000000 --- a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/two-components/code.js +++ /dev/null @@ -1,3 +0,0 @@ -import { Text } from 'react-native'; -; -; diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/two-components/output.js b/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/two-components/output.js deleted file mode 100644 index feda20e4..00000000 --- a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/two-components/output.js +++ /dev/null @@ -1,17 +0,0 @@ -import { - getDefaultTextStyle as _getDefaultTextStyle, - getDefaultTextAccessible as _getDefaultTextAccessible, - NativeText as _NativeText, -} from 'react-native-boost/runtime'; -import { Text } from 'react-native'; -<_NativeText - style={_getDefaultTextStyle()} - allowFontScaling={true} - ellipsizeMode={'tail'} - accessible={_getDefaultTextAccessible()} -/>; -<_NativeText - style={_getDefaultTextStyle()} - allowFontScaling={true} - ellipsizeMode={'tail'} - accessible={_getDefaultTextAccessible()}>; diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/variable-child-no-string/code.js b/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/variable-child-no-string/code.js deleted file mode 100644 index ddadb621..00000000 --- a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/variable-child-no-string/code.js +++ /dev/null @@ -1,4 +0,0 @@ -import { Text } from 'react-native'; -Hello, world!; -const test = Test; -{test}; diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/variable-child-no-string/output.js b/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/variable-child-no-string/output.js deleted file mode 100644 index 0eb169cf..00000000 --- a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/variable-child-no-string/output.js +++ /dev/null @@ -1,15 +0,0 @@ -import { - getDefaultTextStyle as _getDefaultTextStyle, - getDefaultTextAccessible as _getDefaultTextAccessible, - NativeText as _NativeText, -} from 'react-native-boost/runtime'; -import { Text } from 'react-native'; -<_NativeText - style={_getDefaultTextStyle()} - allowFontScaling={true} - ellipsizeMode={'tail'} - accessible={_getDefaultTextAccessible()}> - Hello, world! -; -const test = Test; -{test}; diff --git a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/index.test.ts b/packages/react-native-boost/src/plugin/optimizers/text/__tests__/index.test.ts deleted file mode 100644 index 799e4e2f..00000000 --- a/packages/react-native-boost/src/plugin/optimizers/text/__tests__/index.test.ts +++ /dev/null @@ -1,64 +0,0 @@ -import path from 'node:path'; -import { pluginTester } from 'babel-plugin-tester'; -import { generateTestPlugin } from '../../../utils/generate-test-plugin'; -import { formatTestResult } from '../../../utils/format-test-result'; -import { textOptimizer } from '..'; - -pluginTester({ - plugin: generateTestPlugin(textOptimizer), - title: 'text', - fixtures: path.resolve(import.meta.dirname, 'fixtures'), - babelOptions: { - plugins: ['@babel/plugin-syntax-jsx'], - }, - formatResult: formatTestResult, -}); - -// Fixtures whose `'unknown'` ancestor bail is lifted by the dangerous flag. The expo-router cases also -// pin that the `asChild` bail is independent of the flag: a Text directly inside `` still -// bails (it would be made pressable), while a Text under a plain `` optimizes. -const dangerousUnknownAncestorFixtures = [ - 'text-under-unknown-ancestor', - 'text-with-runtime-parent', - 'expo-router-link-as-child', - 'expo-router-link-as-child-false-static', - 'expo-router-link-alias-as-child', - 'expo-router-link-namespace-as-child', - 'expo-router-link-as-child-nested-view', -]; - -pluginTester({ - plugin: generateTestPlugin(textOptimizer, { - dangerouslyOptimizeTextWithUnknownAncestors: true, - }), - title: 'text dangerous unknown ancestors', - babelOptions: { - plugins: ['@babel/plugin-syntax-jsx'], - }, - formatResult: formatTestResult, - tests: dangerousUnknownAncestorFixtures.map((name) => ({ - title: name, - fixture: path.resolve(import.meta.dirname, `fixtures/${name}/code.js`), - outputFixture: path.resolve(import.meta.dirname, `fixtures/${name}/dangerous-output.js`), - })), -}); - -pluginTester({ - plugin: generateTestPlugin(textOptimizer, { unistyles: true }), - title: 'text unistyles', - fixtures: path.resolve(import.meta.dirname, 'fixtures-unistyles'), - babelOptions: { - plugins: ['@babel/plugin-syntax-jsx'], - }, - formatResult: formatTestResult, -}); - -pluginTester({ - plugin: generateTestPlugin(textOptimizer, { unistyles: true }), - title: 'text unistyles typescript', - fixtures: path.resolve(import.meta.dirname, 'fixtures-unistyles-ts'), - babelOptions: { - plugins: ['@babel/plugin-syntax-jsx', ['@babel/plugin-syntax-typescript', { isTSX: true }]], - }, - formatResult: formatTestResult, -}); diff --git a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/accessibility-label/code.js b/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/accessibility-label/code.js deleted file mode 100644 index 9a4496af..00000000 --- a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/accessibility-label/code.js +++ /dev/null @@ -1,2 +0,0 @@ -import { View } from 'react-native'; -; diff --git a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/accessible/code.js b/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/accessible/code.js deleted file mode 100644 index 7a8cb132..00000000 --- a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/accessible/code.js +++ /dev/null @@ -1,2 +0,0 @@ -import { View } from 'react-native'; -; diff --git a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/accessible/output.js b/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/accessible/output.js deleted file mode 100644 index 3b87b94c..00000000 --- a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/accessible/output.js +++ /dev/null @@ -1,3 +0,0 @@ -import { NativeView as _NativeView } from 'react-native-boost/runtime'; -import { View } from 'react-native'; -<_NativeView accessible />; diff --git a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/aria-label-collision/output.js b/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/aria-label-collision/output.js deleted file mode 100644 index 6a4c402b..00000000 --- a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/aria-label-collision/output.js +++ /dev/null @@ -1,3 +0,0 @@ -import { NativeView as _NativeView } from 'react-native-boost/runtime'; -import { View } from 'react-native'; -<_NativeView accessibilityLabel="x" />; diff --git a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/basic/code.js b/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/basic/code.js deleted file mode 100644 index 39f20252..00000000 --- a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/basic/code.js +++ /dev/null @@ -1,2 +0,0 @@ -import { View } from 'react-native'; -; diff --git a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/basic/output.js b/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/basic/output.js deleted file mode 100644 index a027ffa2..00000000 --- a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/basic/output.js +++ /dev/null @@ -1,3 +0,0 @@ -import { NativeView as _NativeView } from 'react-native-boost/runtime'; -import { View } from 'react-native'; -<_NativeView />; diff --git a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/native-id/code.js b/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/native-id/code.js deleted file mode 100644 index 3c659857..00000000 --- a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/native-id/code.js +++ /dev/null @@ -1,2 +0,0 @@ -import { View } from 'react-native'; -; diff --git a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/native-id/output.js b/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/native-id/output.js deleted file mode 100644 index b2ea9b0e..00000000 --- a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/native-id/output.js +++ /dev/null @@ -1,3 +0,0 @@ -import { NativeView as _NativeView } from 'react-native-boost/runtime'; -import { View } from 'react-native'; -<_NativeView nativeID="x" />; diff --git a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/react-native-namespace-text-ancestor/output.js b/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/react-native-namespace-text-ancestor/output.js deleted file mode 100644 index 27f0935c..00000000 --- a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/react-native-namespace-text-ancestor/output.js +++ /dev/null @@ -1,7 +0,0 @@ -import * as ReactNative from 'react-native'; -import { View } from 'react-native'; - - - - -; diff --git a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/style-passthrough/code.js b/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/style-passthrough/code.js deleted file mode 100644 index cb5df641..00000000 --- a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/style-passthrough/code.js +++ /dev/null @@ -1,2 +0,0 @@ -import { View } from 'react-native'; -; diff --git a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/style/code.js b/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/style/code.js deleted file mode 100644 index 9e126bc4..00000000 --- a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/style/code.js +++ /dev/null @@ -1,2 +0,0 @@ -import { View } from 'react-native'; -; diff --git a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/style/output.js b/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/style/output.js deleted file mode 100644 index 7c8c70d9..00000000 --- a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/style/output.js +++ /dev/null @@ -1,7 +0,0 @@ -import { NativeView as _NativeView } from 'react-native-boost/runtime'; -import { View } from 'react-native'; -<_NativeView - style={{ - width: 1, - }} -/>; diff --git a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/text-ancestor/output.js b/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/text-ancestor/output.js deleted file mode 100644 index 182f110e..00000000 --- a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/text-ancestor/output.js +++ /dev/null @@ -1,12 +0,0 @@ -import { NativeView as _NativeView } from 'react-native-boost/runtime'; -import { Text, View } from 'react-native'; -<> - <_NativeView> - - - - - - - -; diff --git a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/unknown-imported-ancestor/code.js b/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/unknown-imported-ancestor/code.js deleted file mode 100644 index ce4c99fa..00000000 --- a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/unknown-imported-ancestor/code.js +++ /dev/null @@ -1,12 +0,0 @@ -import { View } from 'react-native'; -import { ExternalWrapper } from './ExternalWrapper'; -<> - - - - - - - - -; diff --git a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/unknown-imported-ancestor/dangerous-output.js b/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/unknown-imported-ancestor/dangerous-output.js deleted file mode 100644 index c8df3262..00000000 --- a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/unknown-imported-ancestor/dangerous-output.js +++ /dev/null @@ -1,13 +0,0 @@ -import { NativeView as _NativeView } from 'react-native-boost/runtime'; -import { View } from 'react-native'; -import { ExternalWrapper } from './ExternalWrapper'; -<> - <_NativeView> - - - - <_NativeView> - - - -; diff --git a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/unresolvable-spread-props/code.js b/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/unresolvable-spread-props/code.js deleted file mode 100644 index ff614bb1..00000000 --- a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/unresolvable-spread-props/code.js +++ /dev/null @@ -1,8 +0,0 @@ -import { View } from 'react-native'; -function MyComponent(props) { - return ( - - - - ); -} diff --git a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/unresolvable-spread-props/output.js b/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/unresolvable-spread-props/output.js deleted file mode 100644 index ff614bb1..00000000 --- a/packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/unresolvable-spread-props/output.js +++ /dev/null @@ -1,8 +0,0 @@ -import { View } from 'react-native'; -function MyComponent(props) { - return ( - - - - ); -} diff --git a/packages/react-native-boost/src/plugin/types/index.ts b/packages/react-native-boost/src/plugin/types/index.ts index 27501cc9..b2e87091 100644 --- a/packages/react-native-boost/src/plugin/types/index.ts +++ b/packages/react-native-boost/src/plugin/types/index.ts @@ -1,108 +1,111 @@ -import { NodePath, types as t } from '@babel/core'; +import type { NodePath, PluginObj, PluginPass } from '@babel/core'; +import { types as t } from '@babel/core'; +import type { ComponentAncestorClassification, ModuleAncestorAnalysis, SpreadKeys } from '../../ancestor-types'; + +export type OptimizationState = 'on' | 'off'; + +export type OptimizationSetting = OptimizationState | [state: OptimizationState, options: Options]; export interface PluginOptimizationOptions { /** - * Whether to optimize the `Text` component. - * @default true - */ - text?: boolean; - /** - * Whether to optimize the `View` component. - * @default true + * Replaces the React Native `Text` wrapper with its native host. + * @default 'on' */ - view?: boolean; + 'native-text'?: OptimizationSetting; /** - * Whether to optimize the `Image` component. - * @default true + * Replaces the React Native `View` wrapper with its native host. + * @default 'on' */ - image?: boolean; -} - -export interface PluginOptions { + 'native-view'?: OptimizationSetting; /** - * Paths to ignore from optimization. - * - * Patterns are resolved from Babel's current working directory. - * In nested monorepo apps, parent segments may be needed, for example `../../node_modules/**`. - * @default [] + * Replaces the React Native `Image` wrapper with its native host. + * @default 'on' */ - ignores?: string[]; + 'native-image'?: OptimizationSetting; /** - * Enables verbose logging. - * - * With `silent: false`, optimized components are logged by default. - * When enabled, skipped components and their skip reasons are also logged. - * @default false + * Replaces the React Native `ActivityIndicator` wrapper with its native hosts. + * @default 'on' */ - verbose?: boolean; + 'native-activity-indicator'?: OptimizationSetting; + /** Creates persistent `Animated` values lazily instead of recreating unused values on each render. @default 'on' for detected RN 0.83+ */ + 'animated-value-initialization'?: OptimizationSetting; + /** Removes built-in `Animated` wrappers whose props contain no animated values. @default 'on' for RN 0.83–0.86, 'off' otherwise */ + 'animated-wrapper-removal'?: OptimizationSetting; + /** Evaluates static React Native `StyleSheet` operations at build time. @default 'on' */ + 'stylesheet-operations'?: OptimizationSetting; + /** Folds React Native `Platform.select` calls and `Platform.OS` branches at build time. @default 'on' */ + 'platform-folding'?: OptimizationSetting; +} + +export interface PluginAssumptions { /** - * Disables all plugin logs. + * Assume unresolved ancestors and runtime parents do not provide a React Native `Text` context. * - * When set to `true`, this overrides `verbose`. + * This increases optimization coverage, but can emit the wrong native host when the assumption is false. + * Enable it only after you verify this behavior across the project. * @default false */ - silent?: boolean; - /** - * Toggle individual optimizers. - * - * If omitted, all optimizers are enabled. - */ - optimizations?: PluginOptimizationOptions; - /** - * Enables "Unistyles mode": keep `react-native-unistyles` reactivity working on optimized elements. - * - * When enabled, a `Text`/`View` whose `style` resolves to a Unistyles `StyleSheet.create` style is - * rewritten to Unistyles' own lean host (so its shadow-tree registration survives and theme/breakpoint/ - * variant updates keep working) instead of Boost's raw host; a `Text`/`View` with an unresolvable - * direct `style` (e.g. `style={props.style}`, a function call) is left untouched, because it could be a - * Unistyles style arriving from elsewhere; plain/RN styles still optimize to Boost's host as usual. - * - * Style origin is resolved within a single file only (cross-file stylesheets count as unresolvable). - * - * When omitted, the plugin auto-detects whether `react-native-unistyles` is installed and enables this - * if so (and logs a one-time hint to set the flag explicitly). Set to `false` to force it off even when - * Unistyles is installed. - * @default undefined (auto-detected) - */ - unistyles?: boolean; + unknownAncestorsDoNotRenderText?: boolean; +} + +export type IntegrationState = 'auto' | 'on' | 'off'; + +export interface PluginIntegrationOptions { /** - * Opt-in flag that allows View optimization when ancestor components cannot be statically resolved. - * - * This increases optimization coverage, but may introduce behavioral differences - * when unresolved ancestors render React Native `Text` wrappers. - * Prefer targeted ignores first, and enable this only after verifying affected screens. - * @default false + * Preserves free Uniwind 1.12.x styles on optimized components. Requires Metro. + * `auto` detects an active `withUniwindConfig`. Apply `withBoostConfig` after it. + * `on` requires that configuration. Use `off` only when transformed code does not use Uniwind. + * @default 'auto' */ - dangerouslyOptimizeViewWithUnknownAncestors?: boolean; + uniwind?: IntegrationState; /** - * Opt-in flag that allows Text optimization when ancestors or runtime parents cannot be statically resolved. + * Keeps `react-native-unistyles` reactivity working on optimized elements. * - * This increases optimization coverage, but may introduce behavioral differences when an unresolved - * ancestor or runtime parent renders a React Native `Text` wrapper: a nested `Text` must render as the - * inline `NativeVirtualText` host rather than `NativeText`, and optimizing it would emit the wrong host. - * Prefer targeted `@boost-force` first, and enable this only after verifying affected screens. - * @default false + * `auto` detects an installed `react-native-unistyles` package and logs a warning when found. Use `on` + * when the project uses Unistyles, or `off` when the package is installed but not used in transformed code. + * @default 'auto' */ - dangerouslyOptimizeTextWithUnknownAncestors?: boolean; + unistyles?: IntegrationState; +} + +export type LogLevel = 'silent' | 'warn' | 'info' | 'debug'; + +export interface PluginOptions { /** - * Opt-in flag that allows Image optimization when ancestor components cannot be statically resolved. + * Resolves imported component ancestors through Metro's module graph. This uses private/internal Metro APIs until Metro supports cross-file transform inputs (https://github.com/react/metro/issues/1902). + * Set this to `false` to use same-file ancestor analysis only. * - * This increases optimization coverage, but may introduce behavioral differences when an unresolved - * ancestor renders a React Native `Text` wrapper: Android images under text render through the inline - * image host rather than the normal image view, and optimizing them would emit the wrong host. - * Prefer targeted `@boost-force` first, and enable this only after verifying affected screens. - * @default false + * @default true in supported Metro versions */ - dangerouslyOptimizeImageWithUnknownAncestors?: boolean; + crossFileAncestorResolution?: boolean; + /** Configures individual optimizations. */ + optimizations?: PluginOptimizationOptions; + /** Declares project-wide facts that enable additional optimizations. */ + assumptions?: PluginAssumptions; + /** Configures supported third-party libraries. */ + integrations?: PluginIntegrationOptions; + /** Paths to ignore from optimization. */ + ignores?: string[]; + /** Controls plugin logging. */ + logLevel?: LogLevel; + /** Supplies the React Native target when automatic detection is unsuitable. */ + target?: { + reactNative?: ReactNativeTargetOption; + }; } -export type OptimizableComponent = 'Text' | 'View' | 'Image'; +export type ReactNativeTargetOption = { packageJson: string }; + +export type OptimizationName = keyof PluginOptimizationOptions; export type TargetPlatform = 'ios' | 'android' | 'web'; +export type ReactNativeColorNormalizer = (color: string | number) => number | null | undefined; + export interface OptimizationLogPayload { - component: OptimizableComponent; - path: NodePath; + target: string; + path: NodePath; + note?: string; } export interface SkippedOptimizationLogPayload extends OptimizationLogPayload { @@ -111,8 +114,8 @@ export interface SkippedOptimizationLogPayload extends OptimizationLogPayload { export interface WarningLogPayload { message: string; - component?: OptimizableComponent; - path?: NodePath; + target?: string; + path?: NodePath; } export interface PluginLogger { @@ -122,26 +125,50 @@ export interface PluginLogger { warning: (payload: WarningLogPayload) => void; } -export type Optimizer = ( - path: NodePath, - logger: PluginLogger, - options?: PluginOptions, +export interface OptimizerContext { + logger: PluginLogger; + options: PluginOptions; /** Target platform from Babel's caller (e.g. Metro sets `'ios'`/`'android'`). Lets optimizers resolve platform-specific defaults at build time. */ - platform?: TargetPlatform, + platform?: TargetPlatform; /** - * Whether "Unistyles mode" is active for this build (resolved once at plugin init from the `unistyles` - * option + install auto-detection). When `true`, optimizers classify each element's `style` origin and + * Whether "Unistyles mode" is active for this build (resolved once at plugin init from the integration + * setting and install auto-detection). When `true`, optimizers classify each element's `style` origin and * route Unistyles styles to Unistyles' lean host instead of Boost's raw host. */ - unistylesEnabled?: boolean -) => void; + unistylesEnabled: boolean; + /** Installed React Native minor version, when it can be resolved at build time. */ + reactNativeMinor?: number; + /** Color normalizer from the installed React Native package. */ + normalizeColor?: ReactNativeColorNormalizer; +} + +export interface OptimizerState extends PluginPass { + enabledOptimizations: Set; + optimizerContext: OptimizerContext; + ancestorSources?: Map; +} + +export interface Optimizer { + name: OptimizationName; + defaultState?: OptimizationState | ((context: OptimizerContext) => OptimizationState); + visitor: PluginObj['visitor']; +} + +export type JSXOptimizer = (path: NodePath, context: OptimizerContext) => void; export type HubFile = t.File & { opts: { filename: string; + caller?: { platform?: string }; }; + __ancestorAnalysis?: ModuleAncestorAnalysis; + __spreadImports?: Record>; + __spreadReferences?: Map[number]>; + __ancestorImports?: Record>; + __ancestorReferences?: Map; __hasImports?: Record; __optimized?: boolean; + __staticImageSourceDeclaration?: t.VariableDeclaration; }; /** diff --git a/packages/react-native-boost/src/plugin/utils/__tests__/logger.test.ts b/packages/react-native-boost/src/plugin/utils/__tests__/logger.test.ts index 2740a29d..6ffdc789 100644 --- a/packages/react-native-boost/src/plugin/utils/__tests__/logger.test.ts +++ b/packages/react-native-boost/src/plugin/utils/__tests__/logger.test.ts @@ -1,4 +1,4 @@ -import { NodePath, types as t } from '@babel/core'; +import type { NodePath } from '@babel/core'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { createLogger } from '../logger'; @@ -7,51 +7,79 @@ describe('logger', () => { vi.restoreAllMocks(); }); - it('logs optimized components by default', () => { + it('logs optimized targets at the default info level', () => { const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); - const logger = createLogger({ verbose: false, silent: false }); + const logger = createLogger(); const path = createMockPath('/app/screens/LoginScreen.tsx', 42); - logger.optimized({ component: 'Text', path }); - logger.skipped({ component: 'Text', path, reason: 'contains non-primitive children' }); + logger.optimized({ target: 'Text', path }); + logger.skipped({ target: 'Text', path, reason: 'contains non-primitive children' }); expect(consoleSpy).toHaveBeenCalledTimes(1); expect(String(consoleSpy.mock.calls[0][0])).toContain('Optimized Text in /app/screens/LoginScreen.tsx:42'); }); - it('logs skipped components and reasons when verbose is enabled', () => { + it('identifies context-preserving optimizations without changing the target', () => { const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); - const logger = createLogger({ verbose: true, silent: false }); + createLogger().optimized({ + target: 'View', + path: createMockPath('/app/Box.tsx', 2), + note: 'preserves runtime Text context', + }); + expect(String(consoleSpy.mock.calls[0][0])).toContain( + 'Optimized View in /app/Box.tsx:2 (preserves runtime Text context)' + ); + }); + + it('logs skipped targets and reasons at the debug level', () => { + const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + const logger = createLogger('debug'); const path = createMockPath('/app/screens/Settings.tsx', 10); logger.skipped({ - component: 'View', + target: 'View', path, - reason: 'has unresolved ancestor and dangerous optimization is disabled', + reason: 'has unresolved ancestor that may render Text', }); expect(consoleSpy).toHaveBeenCalledTimes(1); expect(String(consoleSpy.mock.calls[0][0])).toContain( - 'Skipped View in /app/screens/Settings.tsx:10 (has unresolved ancestor and dangerous optimization is disabled)' + 'Skipped View in /app/screens/Settings.tsx:10 (has unresolved ancestor that may render Text)' ); }); - it('disables all logs when silent is enabled', () => { + it('logs only warnings and forced optimizations at the warn level', () => { + const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + const logger = createLogger('warn'); + const path = createMockPath('/app/screens/Profile.tsx', 7); + + logger.optimized({ target: 'Text', path }); + logger.skipped({ target: 'View', path, reason: 'line is marked with @boost-ignore' }); + logger.warning({ target: 'Text', path, message: 'numberOfLines is invalid' }); + logger.forced({ target: 'View', path, reason: 'contains unsupported props' }); + + expect(consoleSpy.mock.calls.map(([message]) => String(message))).toEqual([ + expect.stringContaining('numberOfLines is invalid'), + expect.stringContaining('Force-optimized View'), + ]); + }); + + it('disables all logs at the silent level', () => { const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); - const logger = createLogger({ verbose: true, silent: true }); + const logger = createLogger('silent'); const path = createMockPath('/app/screens/Profile.tsx', 7); - logger.optimized({ component: 'Text', path }); - logger.skipped({ component: 'View', path, reason: 'line is marked with @boost-ignore' }); - logger.warning({ component: 'Text', path, message: 'numberOfLines is invalid' }); + logger.optimized({ target: 'Text', path }); + logger.skipped({ target: 'View', path, reason: 'line is marked with @boost-ignore' }); + logger.warning({ target: 'Text', path, message: 'numberOfLines is invalid' }); expect(consoleSpy).not.toHaveBeenCalled(); }); }); -function createMockPath(filename: string, lineNumber: number): NodePath { +function createMockPath(filename: string, lineNumber: number): NodePath { return { hub: { file: { opts: { filename } } }, node: { loc: { start: { line: lineNumber } } }, - } as unknown as NodePath; + } as unknown as NodePath; } diff --git a/packages/react-native-boost/src/plugin/utils/__tests__/options.test.ts b/packages/react-native-boost/src/plugin/utils/__tests__/options.test.ts new file mode 100644 index 00000000..88931483 --- /dev/null +++ b/packages/react-native-boost/src/plugin/utils/__tests__/options.test.ts @@ -0,0 +1,103 @@ +import { transformSync, type TransformCaller } from '@babel/core'; +import { describe, expect, it, vi } from 'vitest'; +import boostPlugin from '../../index'; +import { validatePluginOptions } from '../options'; + +describe('plugin options', () => { + it('accepts the current configuration', () => { + const options = { + crossFileAncestorResolution: false, + optimizations: { + 'native-image': 'off' as const, + 'animated-value-initialization': 'on' as const, + 'animated-wrapper-removal': 'on' as const, + 'stylesheet-operations': 'on' as const, + 'platform-folding': 'on' as const, + }, + assumptions: { unknownAncestorsDoNotRenderText: true }, + integrations: { unistyles: 'on' as const }, + ignores: ['node_modules/**'], + logLevel: 'info' as const, + target: { reactNative: { packageJson: '/react-native/package.json' } }, + }; + + expect(validatePluginOptions(options)).toEqual(options); + }); + + it('ignores an unmigrated Metro plugin and asks the user to move its old options', () => { + const log = vi.spyOn(console, 'log').mockImplementation(() => {}); + const output = transformSync(`import { Text } from 'react-native'; Hello;`, { + configFile: false, + babelrc: false, + caller: { name: 'metro', bundler: 'metro', platform: 'ios' } as TransformCaller, + plugins: ['@babel/plugin-syntax-jsx', [boostPlugin, { verbose: true, optimizations: { text: false } }]], + })!.code!; + + expect(output).not.toContain('NativeText'); + expect(log).toHaveBeenCalledWith(expect.stringContaining('Move your options to withBoostConfig')); + log.mockRestore(); + }); + + it('runs only the Metro-injected plugin when a manual copy is also configured', () => { + const output = transformSync(`import { Text } from 'react-native'; Hello;`, { + configFile: false, + babelrc: false, + caller: { name: 'metro', bundler: 'metro', platform: 'ios' } as TransformCaller, + plugins: [ + '@babel/plugin-syntax-jsx', + [boostPlugin, {}, 'manual'], + [boostPlugin, { logLevel: 'silent', __reactNativeBoost: 'injected' }, 'injected'], + ], + })!.code!; + + expect(output).toContain('NativeText'); + }); + + it('accepts a React Native package target', () => { + expect(validatePluginOptions({ target: { reactNative: { packageJson: '/react-native/package.json' } } })).toEqual({ + target: { reactNative: { packageJson: '/react-native/package.json' } }, + }); + }); + + it('disables an optimization with its public name', () => { + const output = transformSync(`import { Text } from 'react-native'; Hello;`, { + configFile: false, + babelrc: false, + plugins: [ + '@babel/plugin-syntax-jsx', + [boostPlugin, { logLevel: 'silent', optimizations: { 'native-text': 'off' } }], + ], + })!.code!; + + expect(output).toContain('Hello'); + expect(output).not.toContain('NativeText'); + }); + + it.each([ + [{ silent: true }, 'Use `logLevel` instead.'], + [{ unistyles: true }, 'Use `integrations.unistyles` instead.'], + [ + { dangerouslyOptimizeTextWithUnknownAncestors: true }, + 'Use `assumptions.unknownAncestorsDoNotRenderText` instead.', + ], + [{ optimizations: { text: false } }, 'Use `native-text` instead.'], + ])('rejects removed configuration %#', (options, message) => { + expect(() => validatePluginOptions(options)).toThrow(message); + }); + + it.each([ + [{ unknown: true }, 'Unknown plugin option `unknown`.'], + [{ optimizations: { unknown: 'on' } }, 'Unknown optimization `unknown`.'], + [{ assumptions: { unknown: true } }, 'Unknown assumption `unknown`.'], + [{ integrations: { unknown: 'on' } }, 'Unknown integration `unknown`.'], + [{ crossFileAncestorResolution: 'on' }, '`crossFileAncestorResolution` must be a boolean'], + [{ logLevel: 'verbose' }, '`logLevel` must be one of'], + [{ optimizations: { 'native-text': true } }, 'must be `on` or `off`'], + [{ integrations: { unistyles: true } }, 'must be `auto`, `on`, or `off`'], + [{ target: { reactNative: {} } }, '`target.reactNative.packageJson` must be a non-empty string'], + [{ target: { reactNative: { packageJson: '' } } }, '`target.reactNative.packageJson` must be a non-empty string'], + [{ target: { reactNative: { version: '0.87.1' } } }, 'Unknown `target.reactNative` option `version`'], + ])('rejects invalid configuration %#', (options, message) => { + expect(() => validatePluginOptions(options)).toThrow(message); + }); +}); diff --git a/packages/react-native-boost/src/plugin/utils/__tests__/react-native-target.test.ts b/packages/react-native-boost/src/plugin/utils/__tests__/react-native-target.test.ts new file mode 100644 index 00000000..e68acbb8 --- /dev/null +++ b/packages/react-native-boost/src/plugin/utils/__tests__/react-native-target.test.ts @@ -0,0 +1,67 @@ +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { afterEach, describe, expect, it } from 'vitest'; +import { getReactNativeMinor, loadReactNativeColorNormalizer, resolveReactNativeTarget } from '../react-native-target'; + +const temporaryDirectories: string[] = []; + +afterEach(() => { + for (const directory of temporaryDirectories.splice(0)) fs.rmSync(directory, { recursive: true, force: true }); +}); + +describe('React Native target resolution', () => { + it('uses the version resolved from the requiring project', () => { + const project = createProject('0.87.1'); + const resolution = resolveReactNativeTarget(undefined, [path.join(project, 'package.json')]); + + expect(resolution.target?.version).toBe('0.87.1'); + expect(resolution.target?.packageJson).toBe( + fs.realpathSync(path.join(project, 'node_modules/react-native/package.json')) + ); + expect(getReactNativeMinor(resolution.target?.version)).toBe(87); + }); + + it('uses the most common version when candidates disagree', () => { + const first = createProject('0.86.0'); + const second = createProject('0.87.1'); + const third = createProject('0.87.1'); + const resolution = resolveReactNativeTarget(undefined, [ + path.join(first, 'package.json'), + path.join(second, 'package.json'), + path.join(third, 'package.json'), + ]); + + expect(resolution.target?.version).toBe('0.87.1'); + expect(resolution.versions).toEqual(['0.86.0', '0.87.1']); + }); + + it('loads the color normalizer from the selected React Native package', () => { + const project = createProject('0.83.0'); + const packageJson = path.join(project, 'node_modules/react-native/package.json'); + const normalizerDirectory = path.join( + project, + 'node_modules/react-native/node_modules/@react-native/normalize-colors' + ); + fs.mkdirSync(normalizerDirectory, { recursive: true }); + fs.writeFileSync( + path.join(normalizerDirectory, 'index.js'), + 'module.exports = color => color === "red" ? 42 : null;' + ); + + const normalizeColor = loadReactNativeColorNormalizer(packageJson); + + expect(normalizeColor?.('red')).toBe(42); + expect(normalizeColor?.('invalid')).toBeNull(); + }); +}); + +function createProject(version: string): string { + const project = fs.mkdtempSync(path.join(os.tmpdir(), 'react-native-boost-target-')); + temporaryDirectories.push(project); + const packageDirectory = path.join(project, 'node_modules/react-native'); + fs.mkdirSync(packageDirectory, { recursive: true }); + fs.writeFileSync(path.join(project, 'package.json'), '{}'); + fs.writeFileSync(path.join(packageDirectory, 'package.json'), JSON.stringify({ name: 'react-native', version })); + return project; +} diff --git a/packages/react-native-boost/src/plugin/utils/__tests__/returned-props.test.ts b/packages/react-native-boost/src/plugin/utils/__tests__/returned-props.test.ts new file mode 100644 index 00000000..5602d78d --- /dev/null +++ b/packages/react-native-boost/src/plugin/utils/__tests__/returned-props.test.ts @@ -0,0 +1,89 @@ +import { transformSync, type PluginObj } from '@babel/core'; +import { describe, expect, it } from 'vitest'; +import { analyzeSpreadExports } from '../returned-props'; + +function keys(source: string) { + let result: ReturnType | undefined; + transformSync(source, { + configFile: false, + babelrc: false, + plugins: [ + '@babel/plugin-syntax-jsx', + ['@babel/plugin-syntax-typescript', { isTSX: true }], + (): PluginObj => ({ + visitor: { + Program(path) { + result = analyzeSpreadExports(path); + }, + }, + }), + ], + }); + return result; +} + +describe('returned prop keys', () => { + it('unions fresh object returns and fixed-key writes without reading types', () => { + expect( + keys(` + export function props(enabled: boolean, index?: number): Record { + if (!enabled) return {}; + const result = {role: 'cell'}; + if (index !== undefined) result['aria-colindex'] = index; + return result; + } + `) + ).toEqual({ props: ['aria-colindex', 'role'] }); + }); + + it('supports conditional, empty, default, and imported exports', () => { + expect( + keys(` + export const props = (flag) => flag ? {testID: 'a'} : null; + export default function empty() {} + export {props as forwarded} from './props'; + import other from './other'; export {other}; + `) + ).toEqual({ + props: ['testID'], + default: [], + forwarded: { kind: 'import', source: './props', imported: 'props' }, + other: { kind: 'import', source: './other', imported: 'default' }, + }); + }); + + it('keeps namespace exports from falling through to export-star helpers', () => { + expect(keys(`export * as props from './other'; export * from './fallback';`)).toEqual({ props: null }); + }); + + it.each([ + `return input;`, + `return external();`, + `return { ...input };`, + `return { [input]: true };`, + `return { get testID() { return input; } };`, + `return { __proto__: input };`, + `const result = {}; result[input] = true; return result;`, + `const result = {}; result.__proto__ = input; return result;`, + `const result = {}; Object.assign(result, input); return result;`, + `const result = {}; const alias = result; alias.ref = input; return result;`, + `const result = {}; external(result); return result;`, + `const result = {}; const mutate = () => { result.ref = input; }; mutate(); return result;`, + `const result = {}; eval('result.ref = input'); return result;`, + `const result = {}; function mutate() { eval('result.ref = input'); } mutate(); return result;`, + `let result = {}; result = input; return result;`, + `try { return {}; } finally { return input; }`, + `return shared;`, + ])('rejects an unproved return: %s', (body) => { + expect(keys(`const shared = {}; export function props(input) { ${body} }`)).toEqual({ props: null }); + }); + + it.each([ + `export async function props() { return {}; }`, + `export function* props() { return {}; }`, + `export function props() { return {}; } props = external;`, + `export default function props() { return {}; } props = external;`, + ])('rejects a non-plain or reassigned helper: %s', (source) => { + expect(Object.values(keys(source)!)).toEqual([null]); + }); +}); diff --git a/packages/react-native-boost/src/plugin/utils/ancestor-snapshot.ts b/packages/react-native-boost/src/plugin/utils/ancestor-snapshot.ts new file mode 100644 index 00000000..3e4e74f8 --- /dev/null +++ b/packages/react-native-boost/src/plugin/utils/ancestor-snapshot.ts @@ -0,0 +1,34 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import type { AncestorSnapshot } from '../../ancestor-types'; + +let cachedMtime: bigint | undefined; +let cachedPath: string | undefined; +let cachedSnapshot: AncestorSnapshot | undefined; + +export function readModuleImports( + snapshotPath: string | undefined, + projectRoot: string | undefined, + filename: string, + platform: string | undefined +) { + if (!snapshotPath || !projectRoot) return; + + try { + const mtime = fs.statSync(snapshotPath, { bigint: true }).mtimeNs; + if (cachedPath !== snapshotPath || cachedMtime !== mtime) { + cachedPath = snapshotPath; + cachedMtime = mtime; + cachedSnapshot = JSON.parse(fs.readFileSync(snapshotPath, 'utf8')) as AncestorSnapshot; + } + } catch { + return; + } + + if (cachedSnapshot?.version !== 1) return; + const absoluteFilename = path.resolve(projectRoot, filename); + return { + ancestors: cachedSnapshot.platforms[platform ?? '']?.[absoluteFilename], + spreads: cachedSnapshot.spreadPlatforms?.[platform ?? '']?.[absoluteFilename], + }; +} diff --git a/packages/react-native-boost/src/plugin/utils/common/__tests__/attributes.test.ts b/packages/react-native-boost/src/plugin/utils/common/__tests__/attributes.test.ts index 6bbeebf0..b9eb39b3 100644 --- a/packages/react-native-boost/src/plugin/utils/common/__tests__/attributes.test.ts +++ b/packages/react-native-boost/src/plugin/utils/common/__tests__/attributes.test.ts @@ -37,37 +37,17 @@ describe('buildPropertiesFromAttributes', () => { expect(t.isStringLiteral(property.key, { value: 'aria-label' })).toBe(true); }); - it('preserves source order of the attributes', () => { - const node = buildPropertiesFromAttributes([ - attribute('a', t.stringLiteral('1')), - attribute('b', t.stringLiteral('2')), - ]); - const keys = ((node as t.ObjectExpression).properties as t.ObjectProperty[]).map( - (property) => (property.key as t.Identifier).name - ); - expect(keys).toEqual(['a', 'b']); - }); - - it('returns an Object.assign call when any attribute is a spread', () => { + it('preserves spreads and plain attributes in an object literal', () => { const node = buildPropertiesFromAttributes([ spread('props'), attribute('accessibilityRole', t.stringLiteral('button')), ]); - expect(t.isCallExpression(node)).toBe(true); - const call = node as t.CallExpression; - const callee = call.callee as t.MemberExpression; - expect(t.isIdentifier(callee.object, { name: 'Object' })).toBe(true); - expect(t.isIdentifier(callee.property, { name: 'assign' })).toBe(true); - - // Args: {} target, the bare spread argument, then one single-key object per plain attribute. - expect(call.arguments).toHaveLength(3); - const target = call.arguments[0] as t.ObjectExpression; - expect(t.isObjectExpression(target)).toBe(true); - expect(target.properties).toHaveLength(0); - expect(t.isIdentifier(call.arguments[1], { name: 'props' })).toBe(true); - const lastArg = call.arguments[2] as t.ObjectExpression; - expect(t.isObjectExpression(lastArg)).toBe(true); - expect(lastArg.properties).toHaveLength(1); + expect(node.properties).toHaveLength(2); + const [spreadProperty, roleProperty] = node.properties; + expect(t.isSpreadElement(spreadProperty)).toBe(true); + expect(t.isIdentifier((spreadProperty as t.SpreadElement).argument, { name: 'props' })).toBe(true); + expect(t.isObjectProperty(roleProperty)).toBe(true); + expect(t.isIdentifier((roleProperty as t.ObjectProperty).key, { name: 'accessibilityRole' })).toBe(true); }); }); diff --git a/packages/react-native-boost/src/plugin/utils/common/__tests__/ignore.test.ts b/packages/react-native-boost/src/plugin/utils/common/__tests__/ignore.test.ts new file mode 100644 index 00000000..bfc30154 --- /dev/null +++ b/packages/react-native-boost/src/plugin/utils/common/__tests__/ignore.test.ts @@ -0,0 +1,32 @@ +import path from 'node:path'; +import { describe, expect, it } from 'vitest'; +import { createFileIgnoreMatcher } from '../validation'; + +const file = (cwd: string, filename: string) => ({ opts: { cwd, filename } }); + +describe('compiled file ignores', () => { + it('preserves glob rules and resolves relative patterns against each file working directory', () => { + const first = path.resolve('/first'); + const second = path.resolve('/second'); + const matches = createFileIgnoreMatcher([ + 'node_modules/**', + '**/*.unoptimized.tsx', + path.join(first, 'absolute/**'), + ]); + for (const cwd of [first, second, first]) { + expect(matches(file(cwd, path.join(cwd, 'node_modules/.pnpm/package/index.js')))).toBe(true); + expect(matches(file(cwd, path.join(cwd, 'src/file.unoptimized.tsx')))).toBe(true); + expect(matches(file(cwd, path.join(first, 'absolute/file.js')))).toBe(true); + expect(matches(file(cwd, path.join(cwd, 'src/file.tsx')))).toBe(false); + const other = cwd === first ? second : first; + expect(matches(file(cwd, path.join(other, 'node_modules/package/index.js')))).toBe(false); + } + }); + + it('does not share patterns between plugin configurations', () => { + const cwd = process.cwd(); + const source = file(cwd, path.join(cwd, 'source.js')); + expect(createFileIgnoreMatcher(['**/*.js'])(source)).toBe(true); + expect(createFileIgnoreMatcher([])(source)).toBe(false); + }); +}); diff --git a/packages/react-native-boost/src/plugin/utils/common/__tests__/intrinsic-ancestors.test.ts b/packages/react-native-boost/src/plugin/utils/common/__tests__/intrinsic-ancestors.test.ts new file mode 100644 index 00000000..30f40321 --- /dev/null +++ b/packages/react-native-boost/src/plugin/utils/common/__tests__/intrinsic-ancestors.test.ts @@ -0,0 +1,200 @@ +import { transformSync, type PluginObj, type TransformCaller } from '@babel/core'; +import { describe, expect, it } from 'vitest'; +import type { ComponentAncestorClassification, ModuleAncestorAnalysis } from '../../../../ancestor-types'; +import { analyzeAncestorModule } from '../validation'; + +function analyze(source: string, platform?: string): ModuleAncestorAnalysis { + let analysis: ModuleAncestorAnalysis | undefined; + transformSync(source, { + filename: 'ancestors.tsx', + configFile: false, + babelrc: false, + caller: { name: 'test', ...(platform ? { platform } : {}) } as TransformCaller, + parserOpts: { plugins: ['jsx', 'typescript'] }, + plugins: [ + (): PluginObj => ({ + visitor: { + Program(path) { + analysis = analyzeAncestorModule(path); + }, + }, + }), + ], + }); + return analysis!; +} + +function classify(imports: string, component: string, platform?: string): ComponentAncestorClassification { + return analyze( + `${imports}\nexport const Wrapper = ({ children }) => <${component}>{children};`, + platform + ).exports.Wrapper as ComponentAncestorClassification; +} + +const nativeComponents: Record = { + ActivityIndicator: 'safe', + Button: 'unknown', + DrawerLayoutAndroid: 'safe', + FlatList: 'unknown', + Image: 'unknown', + ImageBackground: 'safe', + InputAccessoryView: 'transparent', + KeyboardAvoidingView: 'safe', + Modal: 'safe', + Pressable: 'safe', + ProgressBarAndroid: 'unknown', + RefreshControl: 'transparent', + SafeAreaView: 'unknown', + ScrollView: 'unknown', + SectionList: 'unknown', + StatusBar: 'unknown', + Switch: 'transparent', + Text: 'text', + TextInput: 'text', + TouchableHighlight: 'safe', + TouchableNativeFeedback: 'unknown', + TouchableOpacity: 'safe', + TouchableWithoutFeedback: 'unknown', + View: 'safe', + VirtualizedList: 'unknown', + VirtualizedSectionList: 'unknown', + experimental_LayoutConformance: 'transparent', + unstable_NativeText: 'unknown', + unstable_NativeView: 'unknown', + unstable_VirtualView: 'unknown', + unstable_VirtualArray: 'unknown', + unstable_VirtualColumn: 'unknown', + unstable_VirtualColumnGenerator: 'unknown', + unstable_VirtualRow: 'unknown', +}; + +describe('intrinsic ancestor summaries', () => { + it.each(Object.entries(nativeComponents))('%s supplies %s', (name, expected) => { + expect(classify(`import { ${name} as Component } from 'react-native';`, 'Component')).toBe(expected); + expect(classify(`import * as RN from 'react-native';`, `RN.${name}`)).toBe(expected); + expect(analyze(`export { ${name} as Component } from 'react-native';`).exports.Component).toBe(expected); + }); + + it.each([ + ['ios', 'transparent', 'safe'], + ['android', 'safe', 'transparent'], + ['web', 'unknown', 'unknown'], + ])('resolves platform-dependent hosts on %s', (platform, safeArea, progress) => { + expect(classify(`import { SafeAreaView } from 'react-native';`, 'SafeAreaView', platform)).toBe(safeArea); + expect(classify(`import * as RN from 'react-native';`, 'RN.ProgressBarAndroid', platform)).toBe(progress); + expect(analyze(`export { SafeAreaView } from 'react-native';`, platform).exports.SafeAreaView).toBe(safeArea); + }); + + it.each(['react-native', 'react-native-reanimated'])('preserves %s Animated host context', (source) => { + const imports = + source === 'react-native' ? `import { Animated as Motion } from '${source}';` : `import Motion from '${source}';`; + for (const [component, expected] of Object.entries({ + View: 'safe', + Text: 'text', + Image: 'unknown', + ScrollView: 'unknown', + FlatList: 'unknown', + SectionList: 'unknown', + })) { + expect(classify(imports, `Motion.${component}`)).toBe(expected); + const namespace = source === 'react-native' ? 'Library.Animated' : 'Library.default'; + expect(classify(`import * as Library from '${source}';`, `${namespace}.${component}`)).toBe(expected); + expect(classify(`${imports} const Component = Motion.${component};`, 'Component')).toBe(expected); + } + }); + + it.each([ + [`import { Animated } from 'react-native';`, 'Animated.createAnimatedComponent'], + [`import * as RN from 'react-native';`, 'RN.Animated.createAnimatedComponent'], + [`import Animated from 'react-native-reanimated';`, 'Animated.createAnimatedComponent'], + [`import { createAnimatedComponent as animate } from 'react-native-reanimated';`, 'animate'], + [`import * as Reanimated from 'react-native-reanimated';`, 'Reanimated.createAnimatedComponent'], + ])('follows a known Animated factory: %s', (imports, factory) => { + for (const [component, expected] of Object.entries({ View: 'safe', Text: 'text', ScrollView: 'unknown' })) { + expect( + classify( + `${imports} import * as Native from 'react-native'; const Component = ${factory}(Native.${component});`, + 'Component' + ) + ).toBe(expected); + } + expect( + classify( + `${imports} const Pass = ({children}) => <>{children}; const Component = ${factory}(Pass);`, + 'Component' + ) + ).toBe('transparent'); + expect(classify(`${imports} const Component = ${factory}(Unknown);`, 'Component')).toBe('unknown'); + }); + + it('separates variable context from unsafe child handling', () => { + expect( + analyze(` + import React from 'react'; + import { Text, View } from 'react-native'; + export function Mixed({children, inline}) { + if (inline) return {children}; + return children; + } + export function ResetOrPass({children, reset}) { + if (reset) return {children}; + return children; + } + export function UnsafeLast({children, inline}) { + if (inline) return {children}; + return React.cloneElement(children, {'aria-label': 'Name'}); + } + export function UnsafeFirst({children, inline}) { + if (!inline) return React.cloneElement(children, {'aria-label': 'Name'}); + return {children}; + } + `).exports + ).toMatchObject({ + Mixed: 'context', + ResetOrPass: 'context', + UnsafeLast: 'unknown', + UnsafeFirst: 'unknown', + }); + }); + + it('follows a reset behind a transparent intrinsic', () => { + expect( + analyze(` + import { Text, View, RefreshControl } from 'react-native'; + export const Safe = ({children}) => {children}; + export const Inline = ({children}) => {children}; + `).exports + ).toMatchObject({ Safe: 'safe', Inline: 'text' }); + }); + + it('keeps customized and child-cloning wrappers unknown even behind a View', () => { + for (const component of ['ScrollView', 'FlatList', 'TouchableWithoutFeedback', 'TouchableNativeFeedback']) { + expect( + analyze( + `import { View, ${component} } from 'react-native'; export const Wrapper = ({children}) => <${component}>{children};` + ).exports.Wrapper + ).toBe('unknown'); + } + }); + + it('does not confuse names, namespace exports, or type imports with runtime components', () => { + expect(classify(`import * as Reanimated from 'react-native-reanimated';`, 'Reanimated.View')).toBe('unknown'); + expect(classify(`import { View } from 'react-native-reanimated';`, 'View')).toBe('unknown'); + expect(classify(`import Animated from 'other-library';`, 'Animated.View')).toBe('unknown'); + expect(classify(`import type { Pressable } from 'react-native';`, 'Pressable')).toBe('unknown'); + expect(classify(`import type * as RN from 'react-native';`, 'RN.View')).toBe('unknown'); + expect(classify(`const Animated = { View: Unknown };`, 'Animated.View')).toBe('unknown'); + expect( + classify( + `import { createAnimatedComponent } from 'other-library'; import { View } from 'react-native'; const Component = createAnimatedComponent(View);`, + 'Component' + ) + ).toBe('unknown'); + expect( + classify( + `import Animated from 'react-native-reanimated'; import { View } from 'react-native'; let Component = Animated.createAnimatedComponent(View); Component = Unknown;`, + 'Component' + ) + ).toBe('unknown'); + }); +}); diff --git a/packages/react-native-boost/src/plugin/utils/common/attributes.ts b/packages/react-native-boost/src/plugin/utils/common/attributes.ts index 2a006554..65d23da1 100644 --- a/packages/react-native-boost/src/plugin/utils/common/attributes.ts +++ b/packages/react-native-boost/src/plugin/utils/common/attributes.ts @@ -1,4 +1,5 @@ import { NodePath, types as t } from '@babel/core'; +import { getCallReturnKeys } from '../returned-props'; import { ACCESSIBILITY_PROPERTIES } from '../constants'; import { USER_SELECT_STYLE_TO_SELECTABLE_PROP, VERTICAL_ALIGN_TO_TEXT_ALIGN_VERTICAL } from '../constants'; @@ -20,31 +21,7 @@ export const hasBlacklistedProperty = (path: NodePath, blac return true; } - // Check spread attributes (e.g., {...props}) - if (t.isJSXSpreadAttribute(attribute)) { - if (t.isIdentifier(attribute.argument)) { - const binding = path.scope.getBinding(attribute.argument.name); - let objectExpression: t.ObjectExpression | undefined; - if (binding) { - // If the binding node is a VariableDeclarator, use its initializer - if (t.isVariableDeclarator(binding.path.node)) { - objectExpression = binding.path.node.init as t.ObjectExpression; - } else if (t.isObjectExpression(binding.path.node)) { - objectExpression = binding.path.node; - } - } - if (objectExpression && t.isObjectExpression(objectExpression)) { - return objectExpression.properties.some((property) => { - if (t.isObjectProperty(property) && t.isIdentifier(property.key)) { - return blacklist.has(property.key.name); - } - return false; - }); - } - } - // Bail if we can't resolve the spread attribute - return true; - } + if (t.isJSXSpreadAttribute(attribute)) return spreadMayContainProperty(path, attribute.argument, blacklist); // For other attribute types, assume no blacklisting return false; @@ -56,43 +33,46 @@ export const hasBlacklistedProperty = (path: NodePath, blac * ignored. Callers that can rewrite a direct attribute (e.g. the `id` β†’ `nativeID` rename) handle it * separately, but still need the conservative spread bail: an unresolvable spread, or a resolvable * spread whose object contains a guarded key, could smuggle the prop through untranslated. The spread - * resolution semantics mirror {@link hasBlacklistedProperty} exactly. + * guard accepts proved helper returns; the Text/Image blacklist checks still reject those calls. * * @param path - The path to the JSXOpeningElement. * @param keys - The set of guarded keys. * @returns true if any spread attribute could contribute one of `keys`. */ -export const hasBlacklistedPropertyInSpread = (path: NodePath, keys: Set): boolean => { - return path.node.attributes.some((attribute) => { +export const hasBlacklistedPropertyInSpread = (path: NodePath, keys: Set): boolean => + path.node.attributes.some((attribute) => { if (!t.isJSXSpreadAttribute(attribute)) return false; + if (!t.isCallExpression(attribute.argument)) return spreadMayContainProperty(path, attribute.argument, keys); + const returnedKeys = getCallReturnKeys(path, attribute.argument); + return returnedKeys === null || returnedKeys.some((key) => keys.has(key)); + }); - if (t.isIdentifier(attribute.argument)) { - const binding = path.scope.getBinding(attribute.argument.name); - let objectExpression: t.ObjectExpression | undefined; - if (binding) { - if (t.isVariableDeclarator(binding.path.node)) { - objectExpression = binding.path.node.init as t.ObjectExpression; - } else if (t.isObjectExpression(binding.path.node)) { - objectExpression = binding.path.node; - } - } - if (objectExpression && t.isObjectExpression(objectExpression)) { - return objectExpression.properties.some((property) => { - if (t.isObjectProperty(property) && t.isIdentifier(property.key)) { - return keys.has(property.key.name); - } - if (t.isObjectProperty(property) && t.isStringLiteral(property.key)) { - return keys.has(property.key.value); - } - return false; - }); - } - } - - // Bail if we can't resolve the spread attribute. +function spreadMayContainProperty( + path: NodePath, + argument: t.Expression, + keys: Set +): boolean { + if (!t.isIdentifier(argument)) return true; + const binding = path.scope.getBinding(argument.name); + if ( + !binding?.constant || + !binding.path.isVariableDeclarator() || + !t.isObjectExpression(binding.path.node.init) || + binding.referencePaths.some((reference) => !reference.parentPath?.isJSXSpreadAttribute()) + ) { return true; - }); -}; + } + return binding.path.node.init.properties.some( + (property) => + !t.isObjectProperty(property) || + property.computed || + (t.isIdentifier(property.key) + ? keys.has(property.key.name) + : t.isStringLiteral(property.key) + ? keys.has(property.key.value) + : true) + ); +} /** * Mirrors the `Text` wrapper's `id ?? nativeID` precedence: renames a direct `id` JSX attribute to @@ -229,11 +209,13 @@ export const addDefaultProperty = (path: NodePath, key: str }; /** - * Builds a JSX attribute, emitting a string literal directly as the value (`name="x"`) and wrapping - * any other expression in a container (`name={expr}`). + * Builds a JSX attribute. Strings with line breaks stay in containers to avoid JSX whitespace normalization. */ export function makeAttribute(name: string, value: t.Expression): t.JSXAttribute { - return t.jsxAttribute(t.jsxIdentifier(name), t.isStringLiteral(value) ? value : t.jsxExpressionContainer(value)); + return t.jsxAttribute( + t.jsxIdentifier(name), + t.isStringLiteral(value) && !/[\r\n]/.test(value.value) ? value : t.jsxExpressionContainer(value) + ); } /** @@ -261,38 +243,21 @@ const buildObjectPropertyFromAttribute = (attribute: t.JSXAttribute): t.ObjectPr }; /** - * Builds an expression that merges the given JSX attributes into a single props object. When none of - * the attributes is a spread (the common case) the result is a plain `ObjectExpression` literal β€” every - * attribute becomes one property, including the empty case which yields `{}`. When at least one spread - * is present the result is `Object.assign({}, …)` (one single-key object per plain attribute, each - * spread's argument as a bare merge source), because reproducing object-spread merge semantics requires - * the call. Either shape produces a structurally-equal object; only the construction differs. + * Builds an object expression from JSX attributes while preserving spread order. * * @param attributes - The attributes to build the expression from. - * @returns An `ObjectExpression` (no spread) or an `Object.assign` `CallExpression` (spread present). + * @returns An object expression containing direct properties and spread elements. */ -export const buildPropertiesFromAttributes = (attributes: (t.JSXAttribute | t.JSXSpreadAttribute)[]): t.Expression => { - if (!attributes.some((attribute) => t.isJSXSpreadAttribute(attribute))) { - const properties = attributes - .filter((attribute): attribute is t.JSXAttribute => t.isJSXAttribute(attribute)) - .map((attribute) => buildObjectPropertyFromAttribute(attribute)); - return t.objectExpression(properties); - } - - const arguments_: t.Expression[] = []; - for (const attribute of attributes) { - if (t.isJSXSpreadAttribute(attribute)) { - arguments_.push(attribute.argument); - } else if (t.isJSXAttribute(attribute)) { - arguments_.push(t.objectExpression([buildObjectPropertyFromAttribute(attribute)])); - } - } - - return t.callExpression(t.memberExpression(t.identifier('Object'), t.identifier('assign')), [ - t.objectExpression([]), - ...arguments_, - ]); -}; +export const buildPropertiesFromAttributes = ( + attributes: (t.JSXAttribute | t.JSXSpreadAttribute)[] +): t.ObjectExpression => + t.objectExpression( + attributes.map((attribute) => + t.isJSXSpreadAttribute(attribute) + ? t.spreadElement(attribute.argument) + : buildObjectPropertyFromAttribute(attribute) + ) + ); /** * Checks if the JSX element has an accessibility property. @@ -359,6 +324,7 @@ export function extractStyleAttribute(attributes: Array - t.isIdentifier(property.key, { name: 'userSelect' }) || - (t.isStringLiteral(property.key) && property.key.value === 'userSelect'); - -const isNullishExpression = (expression: t.Expression): boolean => - t.isNullLiteral(expression) || t.isIdentifier(expression, { name: 'undefined' }); - -const canResolveUserSelect = (expression: t.Expression): boolean => - t.isStringLiteral(expression) || t.isNumericLiteral(expression) || t.isBooleanLiteral(expression); - -const removeUserSelectProperties = (objectExpr: t.ObjectExpression) => { - objectExpr.properties = objectExpr.properties.filter( - (property) => !t.isObjectProperty(property) || !isUserSelectProperty(property) - ); -}; - -/** - * Attempts to statically extract the final flattened `userSelect` style value from a style expression. - * - * A non-null `userSelect` always overrides the direct `selectable` prop in RN, even when the value is - * unknown to RN's map and therefore resolves to `undefined`. Dynamic/nullish values are left for the - * runtime helper so it can preserve RN's `processedStyle.userSelect != null` check. - */ -export function extractSelectableAndUpdateStyle(styleExpr: t.Expression): SelectableExtraction | undefined { - const candidates: Array<{ object: t.ObjectExpression; value: t.Expression }> = []; - let hasUnresolvedStylePart = false; - - const collect = (objectExpr: t.ObjectExpression) => { - for (const property of objectExpr.properties) { - if (!t.isObjectProperty(property) || property.computed) { - hasUnresolvedStylePart = true; - continue; - } - if (isUserSelectProperty(property) && t.isExpression(property.value)) { - candidates.push({ object: objectExpr, value: property.value }); - } - } - }; - - const visitStyle = (expr: t.Expression | t.SpreadElement | null) => { - if (expr == null) return; - if (t.isObjectExpression(expr)) { - collect(expr); - return; - } - if (t.isArrayExpression(expr)) { - for (const element of expr.elements) visitStyle(element); - return; - } - if (!isSkippableFalsyElement(expr)) hasUnresolvedStylePart = true; - }; - - visitStyle(styleExpr); - if (hasUnresolvedStylePart) return undefined; - - const last = candidates.at(-1); - if (!last || isNullishExpression(last.value) || !canResolveUserSelect(last.value)) return undefined; - - for (const { object } of candidates) removeUserSelectProperties(object); - - return { - value: t.isStringLiteral(last.value) ? USER_SELECT_STYLE_TO_SELECTABLE_PROP[last.value.value] : undefined, - }; +/** Reads the final static userSelect without removing values observed by style preprocessors. */ +export function extractStaticTextSelectable(styleExpr: t.Expression): SelectableExtraction | undefined { + const value = tryFlattenStaticStyle(styleExpr, false)?.get('userSelect'); + if (!value || t.isNullLiteral(value)) return undefined; + return { value: t.isStringLiteral(value) ? USER_SELECT_STYLE_TO_SELECTABLE_PROP[value.value] : undefined }; } /** @@ -524,94 +427,67 @@ const staticPropertyName = (property: t.ObjectProperty): string => * literal forms that provably flatten to falsy (`null` / `false` / `undefined` / `0`); any other * primitive literal element is left for the caller to bail on rather than guessing. */ -const isSkippableFalsyElement = (element: t.Expression | t.SpreadElement): boolean => +const isSkippableFalsyElement = (element: t.Expression | t.SpreadElement, allowUndefined = true): boolean => t.isNullLiteral(element) || (t.isBooleanLiteral(element) && element.value === false) || - t.isIdentifier(element, { name: 'undefined' }) || + (allowUndefined && t.isIdentifier(element, { name: 'undefined' })) || (t.isNumericLiteral(element) && element.value === 0); -/** - * Attempts to reproduce, at build time, exactly what the runtime `processTextStyle` helper would - * compute for a `style` value β€” but only when the value is fully static. On success it returns a - * single normalized `ObjectExpression` (the merged, converted style) the caller emits as a direct - * `style={...}` attribute, dropping the per-render helper call. On any dynamic or uncertain input it - * returns `undefined`, leaving the caller to fall back to `{...processTextStyle(styleExpr)}`. - * - * The merge mirrors `StyleSheet.flatten` (top-level array flattened left-to-right, last key wins; - * nested value arrays/objects kept verbatim; falsy literal elements skipped) followed by the helper's - * three conversions (numeric `fontWeight` β†’ string, `verticalAlign` β†’ `textAlignVertical`, - * `userSelect` already lifted to `selectable` upstream). It biases toward bailing: any case it cannot - * prove equivalent to the runtime result yields `undefined`. It never mutates `styleExpr` (it builds - * fresh nodes), so a late bail still hands the original expression to the helper. - */ -export function tryBuildStaticTextStyle(styleExpr: t.Expression): t.ObjectExpression | undefined { - const collected: t.ObjectExpression[] = []; - - // Flatten the top-level style into an ordered list of fully-static object literals, replicating - // `StyleSheet.flatten`'s recursion. Returns false to bail the whole style. - const collect = (expr: t.Expression | t.SpreadElement): boolean => { - if (t.isObjectExpression(expr)) { - if (!isStaticLiteralTree(expr)) return false; - collected.push(expr); - return true; - } - - if (t.isArrayExpression(expr)) { - for (const element of expr.elements) { - if (element == null) continue; // hole β†’ flattens to falsy β†’ skipped - if (t.isObjectExpression(element) || t.isArrayExpression(element)) { - if (!collect(element)) return false; - } else if (isSkippableFalsyElement(element)) { - continue; - } else { - return false; // identifier, logical, member, call, non-falsy literal, spread β†’ bail - } - } - return true; +/** Flattens a static object or array style with last-key-wins semantics. */ +export function tryFlattenStaticStyle( + styleExpression: t.Expression, + allowUndefined = true +): Map | undefined { + if (!t.isObjectExpression(styleExpression) && !t.isArrayExpression(styleExpression)) return undefined; + + const flattened = new Map(); + const collect = (expression: t.Expression | t.SpreadElement | null): boolean => { + if (expression === null) return true; + if (t.isArrayExpression(expression)) return expression.elements.every(collect); + if (isSkippableFalsyElement(expression, allowUndefined)) return true; + if (!t.isObjectExpression(expression) || !isStaticLiteralTree(expression)) return false; + + for (const property of expression.properties) { + if (!t.isObjectProperty(property) || !t.isExpression(property.value)) return false; + const name = staticPropertyName(property); + if (name === '__proto__') return false; + flattened.set(name, t.cloneNode(property.value, true)); } - - return false; // top-level identifier / member / call / conditional / etc. β†’ bail + return true; }; - if (!collect(styleExpr)) return undefined; - - // Merge left-to-right (last key wins), exactly as `flattenStyle` does. - const merged = new Map(); - for (const object of collected) { - for (const property of object.properties) { - const objectProperty = property as t.ObjectProperty; - merged.set(staticPropertyName(objectProperty), t.cloneNode(objectProperty.value as t.Expression, true)); - } - } - - // `userSelect` is removed from literals by `extractSelectableAndUpdateStyle` before this runs. If a - // key remains, the extractor could not resolve its value, so the style is not fully static β€” bail. - if (merged.has('userSelect')) return undefined; + return collect(styleExpression) ? flattened : undefined; +} +/** Appends RN's overrides to a static style, preserving renderer traversal and reset keys. */ +export function tryBuildStaticTextStyle(styleExpr: t.Expression): t.Expression | undefined { + const merged = tryFlattenStaticStyle(styleExpr, false); + if (!merged) return undefined; + const overrides: t.ObjectProperty[] = []; const fontWeight = merged.get('fontWeight'); - if (fontWeight) { - if (t.isNumericLiteral(fontWeight)) { - merged.set('fontWeight', t.stringLiteral(String(fontWeight.value))); - } else if (t.isUnaryExpression(fontWeight) && fontWeight.operator === '-') { - return undefined; // negative weight is implausible; bail rather than guess - } - // A string-literal `fontWeight` is already in its native form and is left untouched. + if (t.isNumericLiteral(fontWeight)) { + overrides.push(t.objectProperty(t.identifier('fontWeight'), t.stringLiteral(String(fontWeight.value)))); + } else if (t.isUnaryExpression(fontWeight)) { + return undefined; + } + const userSelect = merged.get('userSelect'); + if (userSelect && !t.isNullLiteral(userSelect)) { + overrides.push(t.objectProperty(t.identifier('userSelect'), t.unaryExpression('void', t.numericLiteral(0)))); } - const verticalAlign = merged.get('verticalAlign'); - if (verticalAlign !== undefined) { - if (!t.isStringLiteral(verticalAlign)) return undefined; - const mapped = VERTICAL_ALIGN_TO_TEXT_ALIGN_VERTICAL[verticalAlign.value]; - if (mapped === undefined) return undefined; // unknown key β†’ defer to the runtime map - merged.delete('verticalAlign'); - merged.set('textAlignVertical', t.stringLiteral(mapped)); + if (verticalAlign && !t.isNullLiteral(verticalAlign)) { + const mapped = t.isStringLiteral(verticalAlign) + ? VERTICAL_ALIGN_TO_TEXT_ALIGN_VERTICAL[verticalAlign.value] + : undefined; + overrides.push( + t.objectProperty( + t.identifier('textAlignVertical'), + mapped === undefined ? t.unaryExpression('void', t.numericLiteral(0)) : t.stringLiteral(mapped) + ), + t.objectProperty(t.identifier('verticalAlign'), t.unaryExpression('void', t.numericLiteral(0))) + ); } - - const properties = [...merged].map(([key, value]) => - t.objectProperty(VALID_JS_IDENTIFIER.test(key) ? t.identifier(key) : t.stringLiteral(key), value) - ); - - return t.objectExpression(properties); + return overrides.length > 0 ? t.arrayExpression([styleExpr, t.objectExpression(overrides)]) : styleExpr; } /** diff --git a/packages/react-native-boost/src/plugin/utils/common/base.ts b/packages/react-native-boost/src/plugin/utils/common/base.ts index 5b183397..522cf7d2 100644 --- a/packages/react-native-boost/src/plugin/utils/common/base.ts +++ b/packages/react-native-boost/src/plugin/utils/common/base.ts @@ -2,7 +2,7 @@ import { NodePath, types as t } from '@babel/core'; import { addDefault, addNamed } from '@babel/helper-module-imports'; import { FileImportOptions, HubFile } from '../../types'; import { RUNTIME_MODULE_NAME, UNISTYLES_NATIVE_TEXT_MODULE, UNISTYLES_NATIVE_VIEW_MODULE } from '../constants'; -import { markOptimizedHost, OptimizedHostKind } from './optimized-host'; +import { markReactNativeComponent, ReactNativeComponentName } from './optimized-host'; /** * Adds a hint to the file object to ensure that a specific import is added only once and cached on the file object. @@ -50,13 +50,14 @@ export interface NativeComponentSource { } /** The native hosts Boost rewrites elements into; the local-name basis for each injected import. */ -type NativeComponentName = 'NativeText' | 'NativeView' | 'NativeImage'; +type NativeComponentName = 'NativeText' | 'NativeView' | 'NativeImage' | 'NativeActivityIndicator'; -/** Which context each optimized host establishes for the ancestor walk. Total over every host name. */ -const HOST_KIND_BY_NAME: Record = { - NativeText: 'text', - NativeView: 'view', - NativeImage: 'view', +/** The public component whose render context each native host preserves. */ +const COMPONENT_BY_HOST: Record = { + NativeText: 'Text', + NativeView: 'View', + NativeImage: 'Image', + NativeActivityIndicator: 'View', }; /** @@ -108,7 +109,7 @@ export const replaceWithNativeComponent = ( // Record what this element was optimized into so the ancestor walk can classify it once it becomes a // descendant's ancestor (the injected import is not yet resolvable via scope this traversal). - markOptimizedHost(path.node, HOST_KIND_BY_NAME[nativeComponentName]); + markReactNativeComponent(path.node, COMPONENT_BY_HOST[nativeComponentName]); // Replace the component with its native counterpart const jsxName = path.node.name as t.JSXIdentifier; diff --git a/packages/react-native-boost/src/plugin/utils/common/intrinsic-ancestors.ts b/packages/react-native-boost/src/plugin/utils/common/intrinsic-ancestors.ts new file mode 100644 index 00000000..de12d64c --- /dev/null +++ b/packages/react-native-boost/src/plugin/utils/common/intrinsic-ancestors.ts @@ -0,0 +1,45 @@ +import type { ComponentAncestorClassification } from '../../../ancestor-types'; + +/** Text context supplied to rendered children, not the native host's layout context. */ +export function classifyReactNativeAncestor(name: string, platform?: string): ComponentAncestorClassification { + switch (name) { + case 'View': + case 'ActivityIndicator': + case 'DrawerLayoutAndroid': + case 'ImageBackground': + case 'KeyboardAvoidingView': + case 'Modal': + case 'Pressable': + case 'TouchableHighlight': + case 'TouchableOpacity': { + return 'safe'; + } + case 'Text': + case 'TextInput': { + return 'text'; + } + case 'InputAccessoryView': + case 'RefreshControl': + case 'Switch': + case 'experimental_LayoutConformance': { + return 'transparent'; + } + case 'SafeAreaView': { + return platform === 'ios' ? 'transparent' : platform === 'android' ? 'safe' : 'unknown'; + } + case 'ProgressBarAndroid': { + return platform === 'android' ? 'transparent' : platform === 'ios' ? 'safe' : 'unknown'; + } + // Scroll/list customization and Touchable child-prop injection need more than a context summary. + default: { + return 'unknown'; + } + } +} + +/** Animated wrappers retain their wrapped component; they do not establish their own Text context. */ +export function classifyAnimatedAncestor(name: string): ComponentAncestorClassification { + if (name === 'View') return 'safe'; + if (name === 'Text') return 'text'; + return 'unknown'; +} diff --git a/packages/react-native-boost/src/plugin/utils/common/optimized-host.ts b/packages/react-native-boost/src/plugin/utils/common/optimized-host.ts index df223dcc..8d69e122 100644 --- a/packages/react-native-boost/src/plugin/utils/common/optimized-host.ts +++ b/packages/react-native-boost/src/plugin/utils/common/optimized-host.ts @@ -1,19 +1,17 @@ import { types as t } from '@babel/core'; -export type OptimizedHostKind = 'text' | 'view'; +export type ReactNativeComponentName = 'Image' | 'ScrollView' | 'Text' | 'View'; -/** - * Records, per `JSXOpeningElement` node, which kind of host Boost rewrote it into. The ancestor walk - * reads this to classify an already-optimized ancestor by what it renders, without depending on the - * just-injected import being resolvable via scope (a fresh `addNamed` import is not yet crawled into - * scope during the same traversal, so `getBinding` returns `undefined` for it). A `WeakMap` keyed on the - * node avoids mutating the AST and is collected with the file's nodes after the transform. - */ -const optimizedHosts = new WeakMap(); +// Babel cannot resolve an import added during the current visitor without another scope crawl. +const reactNativeComponents = new WeakMap(); -export const markOptimizedHost = (openingElement: t.JSXOpeningElement, kind: OptimizedHostKind): void => { - optimizedHosts.set(openingElement, kind); +export const markReactNativeComponent = ( + openingElement: t.JSXOpeningElement, + component: ReactNativeComponentName +): void => { + reactNativeComponents.set(openingElement, component); }; -export const getOptimizedHostKind = (openingElement: t.JSXOpeningElement): OptimizedHostKind | undefined => - optimizedHosts.get(openingElement); +export const getMarkedReactNativeComponent = ( + openingElement: t.JSXOpeningElement +): ReactNativeComponentName | undefined => reactNativeComponents.get(openingElement); diff --git a/packages/react-native-boost/src/plugin/utils/common/validation.ts b/packages/react-native-boost/src/plugin/utils/common/validation.ts index 1e28e6c0..c7abf1f8 100644 --- a/packages/react-native-boost/src/plugin/utils/common/validation.ts +++ b/packages/react-native-boost/src/plugin/utils/common/validation.ts @@ -1,70 +1,62 @@ import { NodePath, types as t } from '@babel/core'; +import type { + AncestorClassification, + AncestorSummary, + ComponentAncestorClassification, + ModuleAncestorAnalysis, +} from '../../../ancestor-types'; import { ensureArray, BailoutCheck } from '../helpers'; import { HubFile } from '../../types'; -import { minimatch } from 'minimatch'; +import { Minimatch } from 'minimatch'; import nodePath from 'node:path'; -import PluginError from '../plugin-error'; import { UNISTYLES_MODULE_NAME, UNISTYLES_NATIVE_TEXT_MODULE, UNISTYLES_NATIVE_VIEW_MODULE, RUNTIME_MODULE_NAME, } from '../constants'; -import { getOptimizedHostKind } from './optimized-host'; +import { getMarkedReactNativeComponent } from './optimized-host'; import { extractStyleAttribute } from './attributes'; +import { analyzeSpreadExports } from '../returned-props'; +import { classifyAnimatedAncestor, classifyReactNativeAncestor } from './intrinsic-ancestors'; + +/** Compiles ignore patterns once per Babel working directory. */ +export function createFileIgnoreMatcher(ignores: string[]): (file: Pick) => boolean { + let cachedDirectory: string | undefined; + let matchers: Minimatch[] = []; + return (file) => { + if (ignores.length === 0) return false; + const directory = 'cwd' in file.opts ? (file.opts.cwd as string) : process.cwd(); + if (directory !== cachedDirectory) { + matchers = ignores.map( + (pattern) => + new Minimatch(nodePath.isAbsolute(pattern) ? pattern : nodePath.join(directory, pattern), { dot: true }) + ); + cachedDirectory = directory; + } + return matchers.some((matcher) => matcher.match(file.opts.filename)); + }; +} -/** - * Checks if the file is in the list of ignored files. - * - * @param path - The path to the JSXOpeningElement. - * @param ignores - List of glob paths (absolute or relative to import.meta.dirname). - * @returns true if the file matches any of the ignore patterns. - */ -export const isIgnoredFile = (path: NodePath, ignores: string[]): boolean => { - const hub = path.hub as unknown; - const file = typeof hub === 'object' && hub !== null && 'file' in hub ? (hub.file as HubFile) : undefined; - - if (!file) { - throw new PluginError('No file found in Babel hub'); - } - - const fileName = file.opts.filename; +export const isForcedLine = (path: NodePath): boolean => hasDecoratorComment(path, '@boost-force'); - // Use the current working directory which typically corresponds to the user's project root. - const baseDirectory = 'cwd' in file.opts ? (file.opts.cwd as string) : process.cwd(); +export const isIgnoredLine = (path: NodePath): boolean => hasDecoratorComment(path, '@boost-ignore'); - // Iterate through the ignore patterns. - for (const pattern of ignores) { - // If the pattern is not absolute, join it with the baseDir - const absolutePattern = nodePath.isAbsolute(pattern) ? pattern : nodePath.join(baseDirectory, pattern); +function hasDecoratorComment(path: NodePath, decorator: string): boolean { + if (path.isJSXOpeningElement()) return hasJSXDecoratorComment(path, decorator); - // Check if the file name matches the glob pattern. - if (minimatch(fileName, absolutePattern, { dot: true })) { - return true; - } + let currentPath: NodePath | null = path; + while (currentPath) { + if (currentPath.node.leadingComments?.some((comment) => comment.value.includes(decorator))) return true; + if (currentPath.isStatement()) break; + currentPath = currentPath.parentPath; } - return false; -}; - -export const isForcedLine = (path: NodePath): boolean => { - return hasDecoratorComment(path, '@boost-force'); -}; - -export const isIgnoredLine = (path: NodePath): boolean => { - return hasDecoratorComment(path, '@boost-ignore'); -}; +} -/** - * Checks if the JSX element has a preceding comment containing the given decorator string. - * - * Scans the JSXOpeningElement's own leading comments, the parent element's comments, - * ObjectProperty containers, and backward siblings. - */ -function hasDecoratorComment(path: NodePath, decorator: string): boolean { - if (path.node.leadingComments?.some((comment) => comment.value.includes(decorator))) { - return true; - } +/** Finds JSX decorators attached to the element or its preceding JSX comment. */ +function hasJSXDecoratorComment(path: NodePath, decorator: string): boolean { + if (path.node.leadingComments?.some((comment) => comment.value.includes(decorator))) return true; const jsxElementPath = path.parentPath; if (jsxElementPath.node.leadingComments?.some((comment) => comment.value.includes(decorator))) { @@ -117,7 +109,26 @@ function hasDecoratorComment(path: NodePath, decorator: str return false; } +export function isModuleImportBinding( + binding: ReturnType, + source: string, + isSpecifier: (node: t.Node) => boolean, + importedName?: string +): boolean { + if (!binding || binding.kind !== 'module' || !isSpecifier(binding.path.node)) return false; + if (!t.isImportDeclaration(binding.path.parent) || binding.path.parent.source.value !== source) return false; + if (binding.path.parent.importKind === 'type') return false; + if (t.isImportSpecifier(binding.path.node)) { + return ( + binding.path.node.importKind !== 'type' && + (importedName === undefined || t.isIdentifier(binding.path.node.imported, { name: importedName })) + ); + } + return importedName === undefined; +} + export const isReactNativeComponent = (path: NodePath, expectedImportedName: string): boolean => { + if (getMarkedReactNativeComponent(path.node) === expectedImportedName) return true; if (!t.isJSXIdentifier(path.node.name) || !t.isJSXElement(path.parent)) return false; const localName = path.node.name.name; @@ -132,87 +143,85 @@ export const isReactNativeComponent = (path: NodePath, expe return t.isImportDefaultSpecifier(binding.path.node) && localName === expectedImportedName; }; -export type AncestorClassification = 'safe' | 'text' | 'unknown'; +export type { AncestorClassification } from '../../../ancestor-types'; type ScopeBinding = NonNullable['scope']['getBinding']>>; type AncestorAnalysisContext = { - componentCache: WeakMap; + componentCache: WeakMap; componentInProgress: WeakSet; - renderExpressionInProgress: WeakSet; + imports?: Record>; + references?: HubFile['__ancestorReferences']; + symbolic?: boolean; + platform?: string; }; -export const getAncestorClassification = (path: NodePath): AncestorClassification => { +type TextContextSource = AncestorClassification | 'runtime'; + +/** Resolves once during an element's safety checks, before its AST can change. */ +export function createTextContextSourceResolver(path: NodePath): () => TextContextSource { + let source: TextContextSource | undefined; + return () => (source ??= getTextContextSource(path)); +} + +function getTextContextSource(path: NodePath): TextContextSource { + const file = (path.hub as unknown as { file: HubFile }).file; const context: AncestorAnalysisContext = { - componentCache: new WeakMap(), + componentCache: new WeakMap(), componentInProgress: new WeakSet(), - renderExpressionInProgress: new WeakSet(), + platform: file.opts.caller?.platform, + imports: file.__ancestorImports, + references: file.__ancestorReferences, }; - - let classification: AncestorClassification = 'safe'; - let ancestorPath: NodePath | null = path.parentPath.parentPath; - - while (ancestorPath) { - if (ancestorPath.isJSXElement()) { - const ancestorClassification = classifyJSXElementAsAncestor(ancestorPath, context); - classification = mergeAncestorClassification(classification, ancestorClassification); - - if (classification === 'text') return classification; - } - - ancestorPath = ancestorPath.parentPath; - } - - return classification; -}; - -/** Whether a Text has no JSX parent that determines its runtime text context. */ -export const inheritsTextContextFromRuntimeParent = (path: NodePath): boolean => { let childPath: NodePath = path.parentPath; let ancestorPath = childPath.parentPath; while (ancestorPath) { if (ancestorPath.isJSXElement() || ancestorPath.isJSXFragment()) { - if (childPath.listKey !== 'children') return true; - if (ancestorPath.isJSXElement() && !isReactFragmentElement(ancestorPath)) return false; + if (childPath.listKey !== 'children') return 'unknown'; + + if (ancestorPath.isJSXElement()) { + const classification = classifyJSXElementAsAncestor(ancestorPath, context); + if (classification !== 'transparent') return typeof classification === 'string' ? classification : 'unknown'; + } } if (ancestorPath.isExpressionStatement() && ancestorPath.parentPath?.isProgram()) { - // A bare module-level JSX expression has no runtime parent. const expressionPath = ancestorPath.get('expression'); - return !(expressionPath.isJSXElement() || expressionPath.isJSXFragment()); + return expressionPath.isJSXElement() || expressionPath.isJSXFragment() ? 'safe' : 'runtime'; } + if (ancestorPath.isFunction()) return 'runtime'; + childPath = ancestorPath; ancestorPath = ancestorPath.parentPath; } - return true; -}; + return 'runtime'; +} /** - * The ancestor-safety bailout checks shared by the Text and View optimizers. An element nested under a - * `Text` renders as the inline `NativeVirtualText` host (`RCTVirtualText`) instead of the block - * `NativeText`/`NativeView` host, so optimizing it would emit the wrong host; an `'unknown'` ancestor - * chain cannot be proven free of such a `Text`, so it bails too unless the caller opts into the risk. - * - * The ancestor walk is lazy (it runs only if these checks are reached) and memoized across the two - * checks. Each optimizer passes its own resolved `dangerouslyOptimize*WithUnknownAncestors` flag. + * The ancestor-safety bailout checks shared by the host-component optimizers. An element nested under + * a `Text` can need a different native host, so optimization would be incorrect. An `'unknown'` + * ancestor chain also bails unless the project asserts that unknown ancestors do not render `Text`. */ export const ancestorBailoutChecks = ( path: NodePath, - dangerousOptimizationEnabled: boolean + unknownAncestorsDoNotRenderText: boolean ): BailoutCheck[] => { - let classification: AncestorClassification | undefined; - const classify = () => (classification ??= getAncestorClassification(path)); + const classify = createTextContextSourceResolver(path); return [ + { + reason: 'has unresolved runtime parent that may render Text', + shouldBail: () => !unknownAncestorsDoNotRenderText && classify() === 'runtime', + }, { reason: 'has Text ancestor', - shouldBail: () => classify() === 'text', + shouldBail: () => classify() === 'text' || classify() === 'context', }, { - reason: 'has unresolved ancestor and dangerous optimization is disabled', - shouldBail: () => classify() === 'unknown' && !dangerousOptimizationEnabled, + reason: 'has unresolved ancestor that may render Text', + shouldBail: () => classify() === 'unknown' && !unknownAncestorsDoNotRenderText, }, ]; }; @@ -223,10 +232,9 @@ function isReactFragmentElement(path: NodePath): boolean { if (t.isJSXIdentifier(name)) { const binding = path.scope.getBinding(name.name); return ( - name.name === 'Fragment' || - (isReactImportBinding(binding) && - t.isImportSpecifier(binding.path.node) && - getImportSpecifierImportedName(binding.path.node) === 'Fragment') + isReactImportBinding(binding) && + t.isImportSpecifier(binding.path.node) && + getImportSpecifierImportedName(binding.path.node) === 'Fragment' ); } @@ -238,17 +246,11 @@ function isReactFragmentElement(path: NodePath): boolean { ); } -function classifyJSXElementAsAncestor( - path: NodePath, - context: AncestorAnalysisContext -): AncestorClassification { - if (isReactFragmentElement(path)) return 'safe'; +function classifyJSXElementAsAncestor(path: NodePath, context: AncestorAnalysisContext): AncestorSummary { + if (isReactFragmentElement(path)) return 'transparent'; - // An ancestor Boost already rewrote earlier in this same traversal: classify it by the host it - // became (Text β†’ inline-text context, View β†’ normal context). This is checked before the import-based - // paths because the freshly-injected host import is not yet resolvable via scope. - const optimizedHostKind = getOptimizedHostKind(path.node.openingElement); - if (optimizedHostKind) return optimizedHostKind === 'text' ? 'text' : 'safe'; + const markedComponent = getMarkedReactNativeComponent(path.node.openingElement); + if (markedComponent) return classifyReactNativeAncestor(markedComponent, context.platform); const openingElementName = path.node.openingElement.name; @@ -257,7 +259,7 @@ function classifyJSXElementAsAncestor( } if (t.isJSXMemberExpression(openingElementName)) { - return classifyJSXMemberExpressionAsAncestor(path, openingElementName); + return classifyJSXMemberExpressionAsAncestor(path, openingElementName, context); } return 'unknown'; @@ -267,7 +269,7 @@ function classifyJSXIdentifierAsAncestor( path: NodePath, identifierName: string, context: AncestorAnalysisContext -): AncestorClassification { +): AncestorSummary { const binding = path.scope.getBinding(identifierName); if (!binding) return 'unknown'; @@ -276,53 +278,31 @@ function classifyJSXIdentifierAsAncestor( function classifyJSXMemberExpressionAsAncestor( path: NodePath, - expression: t.JSXMemberExpression -): AncestorClassification { - if (!t.isJSXIdentifier(expression.object) || !t.isJSXIdentifier(expression.property)) { - return 'unknown'; - } - - const binding = path.scope.getBinding(expression.object.name); - if (!binding || binding.kind !== 'module' || !t.isImportNamespaceSpecifier(binding.path.node)) { - return 'unknown'; - } - - const importDeclaration = binding.path.parent; - if (!t.isImportDeclaration(importDeclaration)) return 'unknown'; - - if (importDeclaration.source.value === 'react-native') { - return expression.property.name === 'Text' ? 'text' : 'safe'; - } - - return 'unknown'; + expression: t.JSXMemberExpression, + context: AncestorAnalysisContext +): AncestorSummary { + const reference = getModuleReference(path, expression); + return reference ? classifyModuleReference(reference, context) : 'unknown'; } -function classifyBindingAsAncestor(binding: ScopeBinding, context: AncestorAnalysisContext): AncestorClassification { +function classifyBindingAsAncestor(binding: ScopeBinding, context: AncestorAnalysisContext): AncestorSummary { if (binding.kind === 'module') { - return classifyModuleBindingAsAncestor(binding); + return classifyModuleBindingAsAncestor(binding, context); } return classifyLocalBindingAsAncestor(binding, context); } -function classifyModuleBindingAsAncestor(binding: ScopeBinding): AncestorClassification { +function classifyModuleBindingAsAncestor(binding: ScopeBinding, context: AncestorAnalysisContext): AncestorSummary { const importDeclaration = binding.path.parent; if (!t.isImportDeclaration(importDeclaration)) return 'unknown'; const source = importDeclaration.source.value; - if (source === 'react-native') { - if (t.isImportSpecifier(binding.path.node)) { - const importedName = getImportSpecifierImportedName(binding.path.node); - if (!importedName) return 'unknown'; - return importedName === 'Text' ? 'text' : 'safe'; - } - - if (t.isImportNamespaceSpecifier(binding.path.node)) { - return 'safe'; - } - + if ( + importDeclaration.importKind === 'type' || + (t.isImportSpecifier(binding.path.node) && binding.path.node.importKind === 'type') + ) return 'unknown'; - } // An ancestor Boost itself already rewrote (its own runtime host, or a Unistyles lean host in // Unistyles mode) is a *known* host: a View establishes a normal context ('safe'), a Text an @@ -334,10 +314,11 @@ function classifyModuleBindingAsAncestor(binding: ScopeBinding): AncestorClassif if (source === 'react' && t.isImportSpecifier(binding.path.node)) { const importedName = getImportSpecifierImportedName(binding.path.node); - if (importedName === 'Fragment') return 'safe'; + if (importedName === 'Fragment') return 'transparent'; } - return 'unknown'; + const imported = getBindingImportedName(binding); + return imported ? classifyImportedAncestor(source, imported, context) : 'unknown'; } /** @@ -351,19 +332,20 @@ function classifyOptimizedHostAncestor(source: string, binding: ScopeBinding): A if (source === UNISTYLES_NATIVE_TEXT_MODULE) return 'text'; if (source === UNISTYLES_NATIVE_VIEW_MODULE) return 'safe'; - if (source === RUNTIME_MODULE_NAME && t.isImportSpecifier(binding.path.node)) { + if ( + (source === RUNTIME_MODULE_NAME || source === 'react-native-boost/uniwind') && + t.isImportSpecifier(binding.path.node) + ) { const importedName = getImportSpecifierImportedName(binding.path.node); if (importedName === 'NativeText') return 'text'; - if (importedName === 'NativeView') return 'safe'; + if (importedName === 'NativeView' || importedName === 'NativeViewWithContext') return 'safe'; } return undefined; } -function classifyLocalBindingAsAncestor( - binding: ScopeBinding, - context: AncestorAnalysisContext -): AncestorClassification { +function classifyLocalBindingAsAncestor(binding: ScopeBinding, context: AncestorAnalysisContext): AncestorSummary { + if (!binding.constant) return 'unknown'; const cacheKey = binding.path.node; const cached = context.componentCache.get(cacheKey); if (cached) return cached; @@ -374,7 +356,7 @@ function classifyLocalBindingAsAncestor( context.componentInProgress.add(cacheKey); - let classification: AncestorClassification; + let classification: AncestorSummary; if (binding.path.isFunctionDeclaration()) { classification = analyzeFunctionComponent(binding.path, context); } else if (binding.path.isVariableDeclarator()) { @@ -392,7 +374,7 @@ function classifyLocalBindingAsAncestor( function analyzeVariableDeclaratorComponent( path: NodePath, context: AncestorAnalysisContext -): AncestorClassification { +): AncestorSummary { const initPath = path.get('init'); if (!initPath.node) return 'unknown'; @@ -404,6 +386,11 @@ function analyzeVariableDeclaratorComponent( return analyzeCallWrappedComponent(initPath, context); } + if (initPath.isMemberExpression()) { + const reference = getModuleReference(initPath, initPath.node); + return reference ? classifyModuleReference(reference, context) : 'unknown'; + } + if (initPath.isIdentifier()) { const aliasBinding = path.scope.getBinding(initPath.node.name); if (!aliasBinding) return 'unknown'; @@ -417,8 +404,15 @@ function analyzeVariableDeclaratorComponent( function analyzeCallWrappedComponent( path: NodePath, context: AncestorAnalysisContext -): AncestorClassification { - if (!isReactMemoOrForwardRefCall(path)) return 'unknown'; +): AncestorSummary { + const factory = getModuleReference(path, path.node.callee); + const animated = + factory && + ((factory.source === 'react-native' && factory.members.join('.') === 'Animated.createAnimatedComponent') || + (factory.source === 'react-native-reanimated' && + (factory.members.join('.') === 'createAnimatedComponent' || + factory.members.join('.') === 'default.createAnimatedComponent'))); + if (!animated && !isReactMemoOrForwardRefCall(path)) return 'unknown'; const [firstArgumentPath] = path.get('arguments'); if (!firstArgumentPath?.node) return 'unknown'; @@ -438,6 +432,11 @@ function analyzeCallWrappedComponent( return analyzeCallWrappedComponent(firstArgumentPath, context); } + if (firstArgumentPath.isMemberExpression()) { + const reference = getModuleReference(firstArgumentPath, firstArgumentPath.node); + return reference ? classifyModuleReference(reference, context) : 'unknown'; + } + return 'unknown'; } @@ -476,157 +475,183 @@ function isReactImportBinding(binding: ScopeBinding | undefined): binding is Sco return t.isImportDeclaration(importDeclaration) && importDeclaration.source.value === 'react'; } +// Classify direct `children` insertion points; other forms stay unknown. function analyzeFunctionComponent( path: NodePath, context: AncestorAnalysisContext -): AncestorClassification { - const bodyPath = path.get('body'); +): AncestorSummary { + const references = getChildrenReferences(path); + if (!references) return 'unknown'; + if (references.length === 0) return 'safe'; - if (!bodyPath.isBlockStatement()) { - return analyzeRenderExpression(bodyPath as NodePath, context); + let classification = classifyChildrenReference(references[0]!, path, context); + for (const reference of references.slice(1)) { + classification = mergeChildrenSummaries(classification, classifyChildrenReference(reference, path, context)); } - - let classification: AncestorClassification = 'safe'; - - for (const statementPath of bodyPath.get('body')) { - if (!statementPath.isReturnStatement()) continue; - - const argumentPath = statementPath.get('argument'); - if (!argumentPath.node) continue; - - const returnClassification = analyzeRenderExpression(argumentPath as NodePath, context); - classification = mergeAncestorClassification(classification, returnClassification); - - if (classification === 'text') return classification; - } - return classification; } -function analyzeRenderExpression(path: NodePath, context: AncestorAnalysisContext): AncestorClassification { - if (path.isJSXFragment()) { - return analyzeJSXChildren(path.get('children'), context); - } - - let classification: AncestorClassification = 'safe'; - let hasJSX = false; - - path.traverse({ - JSXOpeningElement(jsxPath) { - hasJSX = true; - - const jsxElementPath = jsxPath.parentPath; - if (!jsxElementPath.isJSXElement()) { - classification = mergeAncestorClassification(classification, 'unknown'); - return; - } - - const jsxClassification = classifyJSXElementAsAncestor(jsxElementPath, context); - classification = mergeAncestorClassification(classification, jsxClassification); - - if (classification === 'text') { - jsxPath.stop(); - } - }, - }); - - if (hasJSX) return classification; - - if (path.isIdentifier()) { - return analyzeIdentifierRenderExpression(path, context); +function getChildrenReferences( + path: NodePath +): NodePath[] | undefined { + const parameter = path.node.params[0]; + if (!parameter) return path.isArrowFunctionExpression() ? [] : undefined; + + if (t.isIdentifier(parameter)) { + const binding = path.scope.getBinding(parameter.name); + if (!binding?.constant) return; + + const references: NodePath[] = []; + for (const reference of binding.referencePaths) { + const parent = reference.parentPath; + if (!parent?.isMemberExpression() && !parent?.isOptionalMemberExpression()) return; + if (parent.node.object !== reference.node) return; + if (isChildrenProperty(parent.node)) references.push(parent); + else if (parent.node.computed) return; + } + return references; } - if (path.isMemberExpression() && isPropsChildrenMemberExpression(path.node)) { - return 'safe'; + if (!t.isObjectPattern(parameter)) return; + const property = parameter.properties.find( + (item): item is t.ObjectProperty => + t.isObjectProperty(item) && + !item.computed && + (t.isIdentifier(item.key, { name: 'children' }) || t.isStringLiteral(item.key, { value: 'children' })) + ); + if (!property) { + return parameter.properties.some((item) => t.isRestElement(item) || (t.isObjectProperty(item) && item.computed)) + ? undefined + : []; } - if ( - path.isNullLiteral() || - path.isBooleanLiteral() || - path.isNumericLiteral() || - path.isStringLiteral() || - path.isBigIntLiteral() - ) { - return 'safe'; - } + const value = t.isAssignmentPattern(property.value) ? property.value.left : property.value; + if (!t.isIdentifier(value)) return; + const binding = path.scope.getBinding(value.name); + return binding?.constant ? binding.referencePaths : undefined; +} - return 'unknown'; +function isChildrenProperty(expression: t.MemberExpression | t.OptionalMemberExpression): boolean { + return expression.computed + ? t.isStringLiteral(expression.property, { value: 'children' }) + : t.isIdentifier(expression.property, { name: 'children' }); } -function analyzeJSXChildren( - children: Array>, +function classifyChildrenReference( + reference: NodePath, + component: NodePath, context: AncestorAnalysisContext -): AncestorClassification { - let classification: AncestorClassification = 'safe'; - - for (const childPath of children) { - if (childPath.isJSXElement()) { - const childClassification = classifyJSXElementAsAncestor(childPath, context); - classification = mergeAncestorClassification(classification, childClassification); - } else if (childPath.isJSXFragment()) { - const fragmentClassification = analyzeJSXChildren(childPath.get('children'), context); - classification = mergeAncestorClassification(classification, fragmentClassification); - } else if (childPath.isJSXExpressionContainer()) { - const expressionPath = childPath.get('expression'); - if (!expressionPath.node || expressionPath.isJSXEmptyExpression()) continue; - - const expressionClassification = analyzeRenderExpression(expressionPath as NodePath, context); - classification = mergeAncestorClassification(classification, expressionClassification); - } else if (childPath.isJSXSpreadChild()) { - classification = mergeAncestorClassification(classification, 'unknown'); +): AncestorSummary { + if (reference.parentPath?.isReturnStatement() && reference.key === 'argument') return 'transparent'; + if (component.isArrowFunctionExpression() && component.get('body').node === reference.node) return 'transparent'; + + const container = reference.parentPath; + if (!container?.isJSXExpressionContainer() || reference.key !== 'expression') return 'unknown'; + + let jsxPath = container.parentPath; + if (jsxPath?.isJSXAttribute()) { + if (!t.isJSXIdentifier(jsxPath.node.name, { name: 'children' })) return 'unknown'; + const elementPath = jsxPath.parentPath?.parentPath; + if (!elementPath) return 'unknown'; + jsxPath = elementPath; + } + if (!jsxPath?.isJSXElement() && !jsxPath?.isJSXFragment()) return 'unknown'; + + const ancestors: AncestorSummary[] = []; + while (jsxPath.isJSXElement() || jsxPath.isJSXFragment()) { + if (jsxPath.isJSXElement()) { + const classification = classifyJSXElementAsAncestor(jsxPath, context); + if (typeof classification === 'string') { + if (classification !== 'transparent') { + return ancestors.length === 0 + ? classification + : { kind: 'ancestors', values: [...ancestors, classification] }; + } + } else { + ancestors.push(classification); + } } - if (classification === 'text') { - return classification; + const parent = jsxPath.parentPath; + if (parent?.isJSXElement() || parent?.isJSXFragment()) { + if (jsxPath.listKey !== 'children') return 'unknown'; + jsxPath = parent; + continue; } + + const fallback = + parent?.node === component.node || + (parent?.isReturnStatement() && jsxPath.key === 'argument' && parent.getFunctionParent()?.node === component.node) + ? 'transparent' + : 'unknown'; + return ancestors.length === 0 ? fallback : { kind: 'ancestors', values: [...ancestors, fallback] }; } - return classification; + return 'unknown'; } -function analyzeIdentifierRenderExpression( - path: NodePath, - context: AncestorAnalysisContext -): AncestorClassification { - if (path.node.name === 'children') return 'safe'; - - const binding = path.scope.getBinding(path.node.name); - if (!binding) return 'unknown'; - - if (binding.kind === 'param') { - return binding.identifier.name === 'children' ? 'safe' : 'unknown'; - } +function mergeChildrenSummaries(current: AncestorSummary, next: AncestorSummary): AncestorSummary { + if (current === 'unknown' || next === 'unknown') return 'unknown'; + if (typeof current === 'string' && typeof next === 'string') return current === next ? current : 'context'; + return { kind: 'branches', values: [current, next] }; +} - if (!binding.path.isVariableDeclarator()) return 'unknown'; +type ModuleReference = { source: string; members: string[] }; + +function getModuleReference(path: NodePath, expression: t.Node): ModuleReference | undefined { + const members: string[] = []; + while (t.isJSXMemberExpression(expression) || t.isMemberExpression(expression)) { + if (t.isMemberExpression(expression) && expression.computed) return; + if (!t.isIdentifier(expression.property) && !t.isJSXIdentifier(expression.property)) return; + members.unshift(expression.property.name); + expression = expression.object; + } + if (!t.isIdentifier(expression) && !t.isJSXIdentifier(expression)) return; + const binding = path.scope.getBinding(expression.name); + if (!binding?.constant || binding.kind !== 'module') return; + const declaration = binding.path.parent; + if (!t.isImportDeclaration(declaration) || declaration.importKind === 'type') return; + if (t.isImportSpecifier(binding.path.node) && binding.path.node.importKind === 'type') return; + const imported = getBindingImportedName(binding); + if (imported) members.unshift(imported); + else if (!t.isImportNamespaceSpecifier(binding.path.node)) return; + return { source: declaration.source.value, members }; +} - const cacheKey = binding.path.node; - if (context.renderExpressionInProgress.has(cacheKey)) { - return 'unknown'; +function classifyModuleReference(reference: ModuleReference, context: AncestorAnalysisContext): AncestorSummary { + const { source, members } = reference; + if ( + (source === 'react-native' && members[0] === 'Animated') || + (source === 'react-native-reanimated' && members[0] === 'default') + ) { + return members.length === 2 ? classifyAnimatedAncestor(members[1]!) : 'unknown'; } - - const initPath = binding.path.get('init'); - if (!initPath.node) return 'unknown'; - - context.renderExpressionInProgress.add(cacheKey); - const classification = analyzeRenderExpression(initPath as NodePath, context); - context.renderExpressionInProgress.delete(cacheKey); - - return classification; + return members.length === 1 ? classifyImportedAncestor(source, members[0]!, context) : 'unknown'; } -function isPropsChildrenMemberExpression(expression: t.MemberExpression): boolean { - if (!t.isIdentifier(expression.object, { name: 'props' })) return false; - if (!t.isIdentifier(expression.property, { name: 'children' })) return false; - return !expression.computed; +function classifyImportedAncestor(source: string, imported: string, context: AncestorAnalysisContext): AncestorSummary { + if (source === 'react-native') return classifyReactNativeAncestor(imported, context.platform); + // Reanimated's named exports are not members of its default Animated object. + if (source === 'react-native-reanimated') return 'unknown'; + if (source === 'uniwind' && ['ScopedTheme', 'ScopedVariables', 'LayoutDirection'].includes(imported)) + return 'transparent'; + if (source === 'uniwind/components' || source.startsWith('uniwind/components/')) { + const component = source === 'uniwind/components' ? imported : source.slice('uniwind/components/'.length); + if (component === 'Text') return 'text'; + if (component === 'View') return 'safe'; + } + if (context.symbolic) return { kind: 'import', source, imported }; + + const key = `${source}\0${imported}`; + const classification = context.imports?.[source]?.[imported] ?? 'unknown'; + context.references?.set(key, { source, imported, classification }); + return classification; } -function mergeAncestorClassification( - current: AncestorClassification, - next: AncestorClassification -): AncestorClassification { - if (current === 'text' || next === 'text') return 'text'; - if (current === 'unknown' || next === 'unknown') return 'unknown'; - return 'safe'; +function getBindingImportedName(binding: ScopeBinding): string | undefined { + if (t.isImportDefaultSpecifier(binding.path.node)) return 'default'; + if (t.isImportSpecifier(binding.path.node)) return getImportSpecifierImportedName(binding.path.node); + return undefined; } function getImportSpecifierImportedName(specifier: t.ImportSpecifier): string | undefined { @@ -641,6 +666,95 @@ function getImportSpecifierImportedName(specifier: t.ImportSpecifier): string | return undefined; } +export function analyzeAncestorModule(path: NodePath): ModuleAncestorAnalysis { + const file = (path.hub as unknown as { file: HubFile }).file; + const context: AncestorAnalysisContext = { + platform: file.opts.caller?.platform, + componentCache: new WeakMap(), + componentInProgress: new WeakSet(), + symbolic: true, + }; + const exports = Object.create(null) as Record; + const exportAll: string[] = []; + + for (const statementPath of path.get('body')) { + if (statementPath.isExportAllDeclaration()) { + if (statementPath.node.exportKind !== 'type') { + exportAll.push(statementPath.node.source.value); + } + continue; + } + + if (statementPath.isExportDefaultDeclaration()) { + exports.default = classifyDefaultExport(statementPath.get('declaration'), path, context); + continue; + } + + if (!statementPath.isExportNamedDeclaration() || statementPath.node.exportKind === 'type') continue; + const declarationPath = statementPath.get('declaration'); + if (declarationPath.isFunctionDeclaration() && declarationPath.node.id) { + const name = declarationPath.node.id.name; + exports[name] = path.scope.getBinding(name)?.constant + ? analyzeFunctionComponent(declarationPath, context) + : 'unknown'; + } else if (declarationPath.isVariableDeclaration()) { + for (const declarator of declarationPath.get('declarations')) { + for (const name of Object.keys(t.getBindingIdentifiers(declarator.node.id))) { + const binding = path.scope.getBinding(name); + exports[name] = binding ? classifyBindingAsAncestor(binding, context) : 'unknown'; + } + } + } else if (declarationPath.isClassDeclaration() && declarationPath.node.id) { + exports[declarationPath.node.id.name] = 'unknown'; + } + + for (const specifier of statementPath.node.specifiers) { + if (!t.isExportSpecifier(specifier) || specifier.exportKind === 'type') continue; + const exported = getModuleExportName(specifier.exported); + const local = getModuleExportName(specifier.local); + if (!exported || !local) continue; + if (statementPath.node.source) { + exports[exported] = classifyImportedAncestor(statementPath.node.source.value, local, context); + } else { + const binding = path.scope.getBinding(local); + exports[exported] = binding ? classifyBindingAsAncestor(binding, context) : 'unknown'; + } + } + } + + return { + exports, + exportAll, + spreadExports: analyzeSpreadExports(path), + references: [...(file.__ancestorReferences?.values() ?? [])], + }; +} + +function classifyDefaultExport( + declarationPath: NodePath, + programPath: NodePath, + context: AncestorAnalysisContext +): AncestorSummary { + if (declarationPath.isFunctionDeclaration()) { + return !declarationPath.node.id || programPath.scope.getBinding(declarationPath.node.id.name)?.constant + ? analyzeFunctionComponent(declarationPath, context) + : 'unknown'; + } + if (declarationPath.isFunctionExpression() || declarationPath.isArrowFunctionExpression()) { + return analyzeFunctionComponent(declarationPath, context); + } + if (declarationPath.isIdentifier()) { + const binding = programPath.scope.getBinding(declarationPath.node.name); + return binding ? classifyBindingAsAncestor(binding, context) : 'unknown'; + } + if (declarationPath.isCallExpression()) return analyzeCallWrappedComponent(declarationPath, context); + return 'unknown'; +} + +function getModuleExportName(node: t.Identifier | t.StringLiteral): string { + return t.isIdentifier(node) ? node.name : node.value; +} + /** * Checks whether the closest JSX element ancestor is expo-router Link with a truthy asChild prop. * diff --git a/packages/react-native-boost/src/plugin/utils/generate-test-plugin.ts b/packages/react-native-boost/src/plugin/utils/generate-test-plugin.ts index 1d61f3e7..af5edce6 100644 --- a/packages/react-native-boost/src/plugin/utils/generate-test-plugin.ts +++ b/packages/react-native-boost/src/plugin/utils/generate-test-plugin.ts @@ -1,25 +1,33 @@ +import type { PluginObj } from '@babel/core'; import { declare } from '@babel/helper-plugin-utils'; -import { Optimizer, PluginOptions, TargetPlatform } from '../types'; +import type { Optimizer, OptimizerState, PluginOptions, TargetPlatform } from '../types'; import { createLogger } from './logger'; -export const generateTestPlugin = (optimizer: Optimizer, options: PluginOptions = {}, platform?: TargetPlatform) => { - const logger = createLogger({ - verbose: false, - silent: true, - }); +export const generateTestPlugin = ( + optimizer: Optimizer, + options: PluginOptions = {}, + platform?: TargetPlatform, + reactNativeMinor?: number +) => { + const logger = createLogger('silent'); return declare((api) => { api.assertVersion(7); - return { + const plugin: PluginObj = { name: 'react-native-boost', - visitor: { - JSXOpeningElement(path) { - // Mirror the real plugin's explicit-flag resolution for Unistyles mode (auto-detection is not - // exercised in fixtures); a fixture opts in with `{ unistyles: true }`. - optimizer(path, logger, options, platform, options.unistyles === true); - }, + pre() { + this.enabledOptimizations = new Set([optimizer.name]); + this.optimizerContext = { + logger, + options, + platform, + unistylesEnabled: options.integrations?.unistyles === 'on', + reactNativeMinor, + }; }, + visitor: optimizer.visitor, }; + return plugin as unknown as PluginObj; }); }; diff --git a/packages/react-native-boost/src/plugin/utils/literal-spreads.ts b/packages/react-native-boost/src/plugin/utils/literal-spreads.ts new file mode 100644 index 00000000..22af176b --- /dev/null +++ b/packages/react-native-boost/src/plugin/utils/literal-spreads.ts @@ -0,0 +1,54 @@ +import { type NodePath, types as t } from '@babel/core'; +import { isStaticLiteralTree } from './common/attributes'; + +const reservedKeys = new Set(['__proto__', 'key', 'ref']); +const attributeName = /^[a-zA-Z_$][a-zA-Z0-9_$-]*$/; + +export function openLiteralSpreads(path: NodePath): void { + if (!path.node.attributes.some((attribute) => t.isJSXSpreadAttribute(attribute))) return; + const names = new Set(); + const attributes: t.JSXAttribute[] = []; + for (const attribute of path.node.attributes) { + if (t.isJSXAttribute(attribute)) { + if (!t.isJSXIdentifier(attribute.name)) return; + const name = attribute.name.name; + if (reservedKeys.has(name) || names.has(name)) return; + if (t.isJSXElement(attribute.value) || t.isJSXFragment(attribute.value)) return; + if (t.isJSXExpressionContainer(attribute.value)) { + const value = attribute.value.expression; + if ( + !path.scope.isPure(value) && + !(t.isIdentifier(value, { name: 'undefined' }) && !path.scope.getBinding('undefined')) + ) + return; + } + names.add(name); + attributes.push(attribute); + continue; + } + if (!t.isObjectExpression(attribute.argument) || !isSafeLiteral(attribute.argument)) return; + for (const property of attribute.argument.properties as t.ObjectProperty[]) { + const name = t.isIdentifier(property.key) ? property.key.name : (property.key as t.StringLiteral).value; + if (!attributeName.test(name) || names.has(name)) return; + names.add(name); + // Keep string expressions in containers; JSX text normalization changes whitespace and entities. + attributes.push(t.jsxAttribute(t.jsxIdentifier(name), t.jsxExpressionContainer(property.value as t.Expression))); + } + } + path.node.attributes = attributes; +} + +function isSafeLiteral(expression: t.ObjectExpression): boolean { + if (!isStaticLiteralTree(expression)) return false; + let safe = true; + t.traverseFast(expression, (node) => { + if (!t.isObjectExpression(node)) return; + const names = new Set(); + for (const property of node.properties as t.ObjectProperty[]) { + const name = t.isIdentifier(property.key) ? property.key.name : (property.key as t.StringLiteral).value; + if (reservedKeys.has(name) || names.has(name)) safe = false; + names.add(name); + } + }); + return safe; +} diff --git a/packages/react-native-boost/src/plugin/utils/logger.ts b/packages/react-native-boost/src/plugin/utils/logger.ts index f349af54..9c439a9a 100644 --- a/packages/react-native-boost/src/plugin/utils/logger.ts +++ b/packages/react-native-boost/src/plugin/utils/logger.ts @@ -1,6 +1,7 @@ import { styleText } from 'node:util'; import { HubFile, + LogLevel, OptimizationLogPayload, PluginLogger, SkippedOptimizationLogPayload, @@ -16,21 +17,23 @@ export const noopLogger: PluginLogger = { warning() {}, }; -export const createLogger = ({ verbose, silent }: { verbose: boolean; silent: boolean }): PluginLogger => { - if (silent) return noopLogger; +export const createLogger = (logLevel: LogLevel = 'info'): PluginLogger => { + if (logLevel === 'silent') return noopLogger; return { optimized(payload) { - writeLog('optimized', `Optimized ${payload.component} in ${formatPathLocation(payload.path)}`); + if (logLevel === 'warn') return; + const note = payload.note ? ` (${payload.note})` : ''; + writeLog('optimized', `Optimized ${payload.target} in ${formatPathLocation(payload.path)}${note}`); }, skipped(payload) { - if (!verbose) return; - writeLog('skipped', `Skipped ${payload.component} in ${formatPathLocation(payload.path)} (${payload.reason})`); + if (logLevel !== 'debug') return; + writeLog('skipped', `Skipped ${payload.target} in ${formatPathLocation(payload.path)} (${payload.reason})`); }, forced(payload) { writeLog( 'forced', - `Force-optimized ${payload.component} in ${formatPathLocation(payload.path)} (skipped bailout: ${payload.reason})` + `Force-optimized ${payload.target} in ${formatPathLocation(payload.path)} (skipped bailout: ${payload.reason})` ); }, warning(payload) { @@ -44,25 +47,25 @@ export const createLogger = ({ verbose, silent }: { verbose: boolean; silent: bo function formatWarningContext(payload: WarningLogPayload): string { const location = payload.path ? formatPathLocation(payload.path) : ''; - if (payload.component && location.length > 0) { - return `${payload.component} in ${location}`; + if (payload.target && location.length > 0) { + return `${payload.target} in ${location}`; } - if (payload.component) { - return payload.component; + if (payload.target) { + return payload.target; } return location; } -type LogLevel = 'optimized' | 'skipped' | 'forced' | 'warning'; +type LogCategory = 'optimized' | 'skipped' | 'forced' | 'warning'; -function writeLog(level: LogLevel, message: string): void { +function writeLog(level: LogCategory, message: string): void { const levelTag = formatLevel(level); console.log(`${LOG_PREFIX} ${levelTag} ${message}`); } -function formatLevel(level: LogLevel): string { +function formatLevel(level: LogCategory): string { if (level === 'optimized') return styleText('green', '[optimized]'); if (level === 'skipped') return styleText('yellow', '[skipped]'); if (level === 'forced') return styleText('red', '[forced]'); diff --git a/packages/react-native-boost/src/plugin/utils/optimizer.ts b/packages/react-native-boost/src/plugin/utils/optimizer.ts new file mode 100644 index 00000000..d60bef81 --- /dev/null +++ b/packages/react-native-boost/src/plugin/utils/optimizer.ts @@ -0,0 +1,45 @@ +import type { JSXOptimizer, OptimizationName, Optimizer, OptimizerContext, OptimizationState } from '../types'; +import { isIgnoredLine, isReactNativeComponent } from './common/validation'; +import { openLiteralSpreads } from './literal-spreads'; + +const spreadComponents: Partial> = { + 'native-text': 'Text', + 'native-view': 'View', + 'native-image': 'Image', + 'native-activity-indicator': 'ActivityIndicator', +}; + +export function createJSXOptimizer( + name: OptimizationName, + optimize: JSXOptimizer, + defaultState?: OptimizationState | ((context: OptimizerContext) => OptimizationState) +): Optimizer { + return { + name, + defaultState, + visitor: { + JSXOpeningElement(path, state) { + if (!state.enabledOptimizations.has(name)) return; + const component = spreadComponents[name]; + if (!component || !isReactNativeComponent(path, component) || isIgnoredLine(path)) { + optimize(path, state.optimizerContext); + return; + } + const originalAttributes = path.node.attributes; + const originalElement = path.parentPath.node; + const originalTag = 'name' in path.node.name ? path.node.name.name : undefined; + const { platform } = state.optimizerContext; + if (platform === 'ios' || platform === 'android') openLiteralSpreads(path); + optimize(path, state.optimizerContext); + // Restore rejected sites, including in the pre-Compiler pass; its disabled set prevents a retry. + if ( + path.parentPath.node === originalElement && + 'name' in path.node.name && + path.node.name.name === originalTag + ) { + path.node.attributes = originalAttributes; + } + }, + }, + }; +} diff --git a/packages/react-native-boost/src/plugin/utils/options.ts b/packages/react-native-boost/src/plugin/utils/options.ts new file mode 100644 index 00000000..c632ddab --- /dev/null +++ b/packages/react-native-boost/src/plugin/utils/options.ts @@ -0,0 +1,136 @@ +import type { IntegrationState, LogLevel, OptimizationState, PluginOptions } from '../types'; +import PluginError from './plugin-error'; + +const optionKeys = [ + 'crossFileAncestorResolution', + 'optimizations', + 'assumptions', + 'integrations', + 'ignores', + 'logLevel', + 'target', +]; +const optimizationKeys = [ + 'native-text', + 'native-view', + 'native-image', + 'native-activity-indicator', + 'animated-value-initialization', + 'animated-wrapper-removal', + 'stylesheet-operations', + 'platform-folding', +]; +const assumptionKeys = ['unknownAncestorsDoNotRenderText']; +const integrationKeys = ['unistyles', 'uniwind']; +const optimizationStates: OptimizationState[] = ['on', 'off']; +const integrationStates: IntegrationState[] = ['auto', 'on', 'off']; +const logLevels: LogLevel[] = ['silent', 'warn', 'info', 'debug']; + +const legacyPluginOptions: Record = { + verbose: 'Use `logLevel` instead.', + silent: 'Use `logLevel` instead.', + unistyles: 'Use `integrations.unistyles` instead.', + dangerouslyOptimizeViewWithUnknownAncestors: 'Use `assumptions.unknownAncestorsDoNotRenderText` instead.', + dangerouslyOptimizeTextWithUnknownAncestors: 'Use `assumptions.unknownAncestorsDoNotRenderText` instead.', + dangerouslyOptimizeImageWithUnknownAncestors: 'Use `assumptions.unknownAncestorsDoNotRenderText` instead.', + dangerouslyOptimizeActivityIndicatorWithUnknownAncestors: + 'Use `assumptions.unknownAncestorsDoNotRenderText` instead.', +}; + +const legacyOptimizations: Record = { + text: 'Use `native-text` instead.', + view: 'Use `native-view` instead.', + image: 'Use `native-image` instead.', + activityIndicator: 'Use `native-activity-indicator` instead.', +}; + +export function validatePluginOptions(rawOptions: unknown): PluginOptions { + const options = validateOptions(rawOptions, optionKeys); + if (options.crossFileAncestorResolution !== undefined && typeof options.crossFileAncestorResolution !== 'boolean') { + throw new PluginError('`crossFileAncestorResolution` must be a boolean.'); + } + if (options.target !== undefined) validateTarget(options.target); + return options as PluginOptions; +} + +function validateOptions(rawOptions: unknown, keys: string[]): Record { + const options = readObject(rawOptions, 'Plugin options'); + validateKeys(options, keys, 'plugin option', legacyPluginOptions); + + if (options.logLevel !== undefined && !logLevels.some((level) => level === options.logLevel)) { + throw new PluginError('`logLevel` must be one of `silent`, `warn`, `info`, or `debug`.'); + } + + if ( + options.ignores !== undefined && + (!Array.isArray(options.ignores) || options.ignores.some((pattern) => typeof pattern !== 'string')) + ) { + throw new PluginError('`ignores` must be an array of strings.'); + } + + if (options.optimizations !== undefined) { + const optimizations = readObject(options.optimizations, '`optimizations`'); + validateKeys(optimizations, optimizationKeys, 'optimization', legacyOptimizations); + for (const [name, state] of Object.entries(optimizations)) { + if (state !== undefined && !optimizationStates.some((candidate) => candidate === state)) { + throw new PluginError(`Optimization \`${name}\` must be \`on\` or \`off\`.`); + } + } + } + + if (options.assumptions !== undefined) { + const assumptions = readObject(options.assumptions, '`assumptions`'); + validateKeys(assumptions, assumptionKeys, 'assumption'); + if ( + assumptions.unknownAncestorsDoNotRenderText !== undefined && + typeof assumptions.unknownAncestorsDoNotRenderText !== 'boolean' + ) { + throw new PluginError('`assumptions.unknownAncestorsDoNotRenderText` must be a boolean.'); + } + } + + if (options.integrations !== undefined) { + const integrations = readObject(options.integrations, '`integrations`'); + validateKeys(integrations, integrationKeys, 'integration'); + for (const name of integrationKeys) { + if (integrations[name] !== undefined && !integrationStates.some((state) => state === integrations[name])) { + throw new PluginError(`\`integrations.${name}\` must be \`auto\`, \`on\`, or \`off\`.`); + } + } + } + + return options; +} + +function validateTarget(value: unknown): void { + const target = readObject(value, '`target`'); + validateKeys(target, ['reactNative'], 'target'); + if (target.reactNative === undefined) return; + + const reactNative = readObject(target.reactNative, '`target.reactNative`'); + validateKeys(reactNative, ['packageJson'], '`target.reactNative` option'); + if (typeof reactNative.packageJson !== 'string' || reactNative.packageJson.length === 0) { + throw new PluginError('`target.reactNative.packageJson` must be a non-empty string.'); + } +} + +function readObject(value: unknown, name: string): Record { + if (typeof value !== 'object' || value === null || Array.isArray(value)) { + throw new PluginError(`${name} must be an object.`); + } + return value as Record; +} + +function validateKeys( + value: Record, + allowedKeys: string[], + label: string, + replacements: Record = {} +): void { + for (const key of Object.keys(value)) { + if (allowedKeys.includes(key)) continue; + const replacement = replacements[key]; + if (replacement) throw new PluginError(`The \`${key}\` ${label} was removed. ${replacement}`); + throw new PluginError(`Unknown ${label} \`${key}\`.`); + } +} diff --git a/packages/react-native-boost/src/plugin/utils/plugin-error.ts b/packages/react-native-boost/src/plugin/utils/plugin-error.ts index 0c2695d8..1230de59 100644 --- a/packages/react-native-boost/src/plugin/utils/plugin-error.ts +++ b/packages/react-native-boost/src/plugin/utils/plugin-error.ts @@ -1,6 +1,6 @@ export default class PluginError extends Error { constructor(message: string) { - super(`[react-native-boost] Babel plugin exception: ${message}`); + super(`[react-native-boost] ${message}`); this.name = 'PluginError'; } } diff --git a/packages/react-native-boost/src/plugin/utils/react-native-target.ts b/packages/react-native-boost/src/plugin/utils/react-native-target.ts new file mode 100644 index 00000000..7729ea17 --- /dev/null +++ b/packages/react-native-boost/src/plugin/utils/react-native-target.ts @@ -0,0 +1,101 @@ +import { createHash } from 'node:crypto'; +import fs from 'node:fs'; +import { createRequire } from 'node:module'; +import path from 'node:path'; +import type { ReactNativeColorNormalizer, ReactNativeTargetOption } from '../types'; + +export interface ResolvedReactNativeTarget { + version: string; + packageJson: string; + fingerprint: string; +} + +export interface ReactNativeTargetResolution { + target?: ResolvedReactNativeTarget; + versions: string[]; +} + +export function resolveReactNativeTarget( + configuredTarget: ReactNativeTargetOption | undefined, + locations: Array +): ReactNativeTargetResolution { + if (configuredTarget) { + const target = readReactNativeTarget(resolvePath(configuredTarget.packageJson, locations)); + return { target, versions: target ? [target.version] : [] }; + } + + const packageJsonPaths = new Set(); + const targets: ResolvedReactNativeTarget[] = []; + for (const location of locations) { + const packageJson = resolveReactNativePackageJson(location); + if (!packageJson || packageJsonPaths.has(packageJson)) continue; + packageJsonPaths.add(packageJson); + const target = readReactNativeTarget(packageJson); + if (target) targets.push(target); + } + + if (targets.length === 0) return { versions: [] }; + + const counts = new Map(); + for (const target of targets) counts.set(target.version, (counts.get(target.version) ?? 0) + 1); + let selected = targets[0]!; + for (const target of targets.slice(1)) { + if ((counts.get(target.version) ?? 0) > (counts.get(selected.version) ?? 0)) selected = target; + } + + return { target: selected, versions: [...new Set(targets.map((target) => target.version))] }; +} + +export function getReactNativeMinor(version: string | undefined): number | undefined { + const match = version ? /^(\d+)\.(\d+)\./.exec(version) : null; + if (!match) return undefined; + return Number(match[1]) === 0 ? Number(match[2]) : Number.MAX_SAFE_INTEGER; +} + +export function loadReactNativeColorNormalizer( + packageJson: string | undefined +): ReactNativeColorNormalizer | undefined { + if (!packageJson) return undefined; + + try { + const normalizeColor = createRequire(packageJson)('@react-native/normalize-colors') as unknown; + return typeof normalizeColor === 'function' ? (normalizeColor as ReactNativeColorNormalizer) : undefined; + } catch { + return undefined; + } +} + +function resolvePath(packageJson: string, locations: Array): string { + if (path.isAbsolute(packageJson)) return packageJson; + const origin = locations.find(Boolean); + return path.resolve(origin ? path.dirname(origin) : process.cwd(), packageJson); +} + +function resolveReactNativePackageJson(origin: string | undefined): string | undefined { + if (!origin) return undefined; + + try { + return fs.realpathSync(createRequire(path.resolve(origin)).resolve('react-native/package.json')); + } catch { + return undefined; + } +} + +function readReactNativeTarget(packageJson: string): ResolvedReactNativeTarget | undefined { + try { + const contents = fs.readFileSync(packageJson, 'utf8'); + const { version } = JSON.parse(contents) as { version?: unknown }; + if (typeof version !== 'string' || !isReactNativeVersion(version)) return undefined; + return { version, packageJson: fs.realpathSync(packageJson), fingerprint: hash(contents) }; + } catch { + return undefined; + } +} + +function isReactNativeVersion(version: string): boolean { + return /^\d+\.\d+\.\d+(?:-|$)/.test(version); +} + +function hash(value: string): string { + return createHash('sha256').update(value).digest('hex'); +} diff --git a/packages/react-native-boost/src/plugin/utils/returned-props.ts b/packages/react-native-boost/src/plugin/utils/returned-props.ts new file mode 100644 index 00000000..baf2eb69 --- /dev/null +++ b/packages/react-native-boost/src/plugin/utils/returned-props.ts @@ -0,0 +1,139 @@ +import { type NodePath, types as t } from '@babel/core'; +import type { SpreadKeys, SpreadSummary } from '../../ancestor-types'; +import type { HubFile } from '../types'; + +type Binding = NonNullable>; + +export function getCallReturnKeys(path: NodePath, call: t.CallExpression): SpreadKeys { + if (!t.isIdentifier(call.callee)) return null; + const summary = summarizeBinding(path.scope.getBinding(call.callee.name)); + if (summary === null || Array.isArray(summary)) return summary; + const file = (path.hub as unknown as { file: HubFile }).file; + const { source, imported } = summary; + const value = file.__spreadImports?.[source]?.[imported]; + const keys = Array.isArray(value) ? value : null; + file.__spreadReferences?.set(`${source}\0${imported}`, { source, imported, keys }); + return keys; +} + +export function analyzeSpreadExports(path: NodePath): Record { + const exports: Record = Object.create(null); + for (const statement of path.get('body')) { + if (statement.isExportDefaultDeclaration()) { + const declaration = statement.get('declaration'); + exports.default = declaration.isIdentifier() + ? summarizeBinding(declaration.scope.getBinding(declaration.node.name)) + : declaration.isFunctionDeclaration() && declaration.node.id + ? summarizeBinding(declaration.scope.getBinding(declaration.node.id.name)) + : getReturnedKeys(declaration); + } + if (!statement.isExportNamedDeclaration() || statement.node.exportKind === 'type') continue; + const declaration = statement.get('declaration'); + if (declaration.node) { + for (const name of Object.keys(t.getOuterBindingIdentifiers(declaration.node))) { + exports[name] = summarizeBinding(path.scope.getBinding(name)); + } + } + for (const specifier of statement.node.specifiers) { + if (t.isExportSpecifier(specifier) && specifier.exportKind === 'type') continue; + const exported = t.isIdentifier(specifier.exported) ? specifier.exported.name : specifier.exported.value; + exports[exported] = null; + if (!t.isExportSpecifier(specifier)) continue; + const local = specifier.local.name; + exports[exported] = statement.node.source + ? { kind: 'import', source: statement.node.source.value, imported: local } + : summarizeBinding(path.scope.getBinding(local)); + } + } + return exports; +} + +function summarizeBinding(binding: Binding | undefined): SpreadSummary { + if (!binding?.constant) return null; + const path = binding.path; + if (path.isImportSpecifier() || path.isImportDefaultSpecifier()) { + const declaration = path.parent; + if (!t.isImportDeclaration(declaration) || declaration.importKind === 'type') return null; + if (path.isImportSpecifier() && path.node.importKind === 'type') return null; + const imported = path.isImportDefaultSpecifier() + ? 'default' + : t.isIdentifier(path.node.imported) + ? path.node.imported.name + : path.node.imported.value; + return { kind: 'import', source: declaration.source.value, imported }; + } + return getReturnedKeys(path.isVariableDeclarator() ? path.get('init') : path); +} + +function getReturnedKeys(path: NodePath): SpreadKeys { + if (!path.isFunctionDeclaration() && !path.isFunctionExpression() && !path.isArrowFunctionExpression()) return null; + if (path.node.async || path.node.generator) return null; + const keys = new Set(); + let valid = true; + const body = (path as NodePath).get('body'); + if (!body.isBlockStatement()) return expressionKeys(body, path); + path.traverse({ + CallExpression(call) { + // Direct eval can access a local object without a binding reference in the AST. + if (t.isIdentifier(call.node.callee, { name: 'eval' })) { + valid = false; + call.stop(); + } + }, + ReturnStatement(statement) { + if (statement.getFunctionParent() !== path) return; + const returned = expressionKeys(statement.get('argument'), path); + if (returned === null) { + valid = false; + statement.stop(); + } else for (const key of returned) keys.add(key); + }, + }); + return valid ? [...keys].sort() : null; +} + +function expressionKeys(expression: NodePath, owner: NodePath): SpreadKeys { + if (!expression.node || expression.isNullLiteral()) return []; + if (expression.isConditionalExpression()) { + const left = expressionKeys(expression.get('consequent'), owner); + const right = expressionKeys(expression.get('alternate'), owner); + return left && right ? [...new Set([...left, ...right])].sort() : null; + } + if (expression.isObjectExpression()) return objectKeys(expression.node); + if (!expression.isIdentifier()) return null; + const binding = expression.scope.getBinding(expression.node.name); + if (!binding?.constant || !binding.path.isVariableDeclarator() || binding.path.getFunctionParent() !== owner) + return null; + const initial = binding.path.node.init; + if (!t.isObjectExpression(initial)) return null; + const keys = objectKeys(initial); + if (keys === null) return null; + for (const reference of binding.referencePaths) { + if (reference.getFunctionParent() !== owner) return null; + const parent = reference.parentPath; + if (parent?.isReturnStatement() && reference.key === 'argument') continue; + if (!parent?.isMemberExpression() || reference.key !== 'object') return null; + const assignment = parent.parentPath; + if (!assignment?.isAssignmentExpression({ operator: '=' }) || parent.key !== 'left') return null; + const key = propertyKey(parent.node.property, parent.node.computed); + if (key === null) return null; + keys.push(key); + } + return [...new Set(keys)].sort(); +} + +function objectKeys(object: t.ObjectExpression): SpreadKeys { + const keys: string[] = []; + for (const property of object.properties) { + if (!t.isObjectProperty(property)) return null; + const key = propertyKey(property.key, property.computed); + if (key === null) return null; + keys.push(key); + } + return [...new Set(keys)].sort(); +} + +function propertyKey(node: t.Node, computed: boolean): string | null { + const key = t.isStringLiteral(node) ? node.value : !computed && t.isIdentifier(node) ? node.name : null; + return key === '__proto__' ? null : key; +} diff --git a/packages/react-native-boost/src/plugin/utils/static-accessibility.ts b/packages/react-native-boost/src/plugin/utils/static-accessibility.ts new file mode 100644 index 00000000..d3698ece --- /dev/null +++ b/packages/react-native-boost/src/plugin/utils/static-accessibility.ts @@ -0,0 +1,230 @@ +import { type NodePath, types as t } from '@babel/core'; +import { buildPropertiesFromAttributes, makeAttribute } from './common/attributes'; + +type Primitive = string | number | boolean | null | undefined; +type Literal = Primitive | { [key: string]: Primitive } | string[]; +type Props = Record; +const unknown = Symbol('unknown'); +const stateKeys = ['busy', 'checked', 'disabled', 'expanded', 'selected']; +const valueKeys = ['max', 'min', 'now', 'text']; + +// Only syntax-owned scalar fields qualify. Never follow bindings or invoke Babel's evaluator. +function readLiteral(node: t.Node, path: NodePath, object = true): Literal | typeof unknown { + if (t.isNullLiteral(node)) return null; + if (t.isStringLiteral(node) || t.isBooleanLiteral(node) || t.isNumericLiteral(node)) return node.value; + if (t.isIdentifier(node, { name: 'undefined' }) && !path.scope.getBinding('undefined')) return undefined; + if (t.isUnaryExpression(node) && t.isNumericLiteral(node.argument)) { + if (node.operator === '-') return -node.argument.value; + if (node.operator === 'void') return undefined; + } + if (object && t.isArrayExpression(node) && node.elements.every((element) => t.isStringLiteral(element))) + return node.elements.map((element) => (element as t.StringLiteral).value); + if (!object || !t.isObjectExpression(node)) return unknown; + const result: Record = {}; + for (const property of node.properties) { + if (!t.isObjectProperty(property) || property.computed) return unknown; + const key = t.isIdentifier(property.key) + ? property.key.name + : t.isStringLiteral(property.key) + ? property.key.value + : undefined; + if (key === undefined || key === '__proto__' || Object.hasOwn(result, key)) return unknown; + const value = readLiteral(property.value, path, false); + if (value === unknown || (value !== null && typeof value === 'object')) return unknown; + result[key] = value; + } + return result; +} + +function expression(value: Literal): t.Expression { + if (value === undefined) return t.unaryExpression('void', t.numericLiteral(0)); + if (Array.isArray(value)) return t.arrayExpression(value.map((item) => t.stringLiteral(item))); + if (value !== null && typeof value === 'object') { + return t.objectExpression( + Object.entries(value).map(([key, field]) => t.objectProperty(t.stringLiteral(key), expression(field))) + ); + } + return t.valueToNode(value); +} + +/** Returns direct props only when the entire helper input is closed and its native target is known. */ +export function foldAccessibility( + path: NodePath, + attributes: t.JSXAttribute[], + component: 'Text' | 'View' | 'Image', + platform?: string, + minor?: number +): t.JSXAttribute[] | undefined { + if ((platform !== 'ios' && platform !== 'android') || minor === undefined || minor < 83 || minor > 88) return; + const names = new Set(); + for (const attribute of path.node.attributes) { + if (!t.isJSXAttribute(attribute) || !t.isJSXIdentifier(attribute.name) || names.has(attribute.name.name)) return; + names.add(attribute.name.name); + } + if ( + attributes.some( + (attribute) => + attribute.value !== null && + !t.isStringLiteral(attribute.value) && + !(t.isJSXExpressionContainer(attribute.value) && t.isExpression(attribute.value.expression)) + ) + ) + return; + const props: Props = {}; + for (const property of buildPropertiesFromAttributes(attributes).properties) { + if (!t.isObjectProperty(property)) return; + const key = t.isIdentifier(property.key) ? property.key.name : (property.key as t.StringLiteral).value; + const value = readLiteral(property.value, path); + if (value === unknown) return; + if (Array.isArray(value)) { + if (key !== 'accessibilityLabelledBy') return; + } else if ( + value !== null && + typeof value === 'object' && + key !== 'accessibilityState' && + key !== 'accessibilityValue' + ) + return; + if ((key === 'accessibilityState' || key === 'accessibilityValue') && value != null && typeof value !== 'object') + return; + props[key] = value; + } + const result = + component === 'Text' + ? textProps(props, platform, minor) + : component === 'Image' + ? imageProps(props, platform, minor) + : viewProps(props); + if (!result) return; + return Object.entries(result).map(([key, value]) => makeAttribute(key, expression(value))); +} + +function merge(props: Props, name: string, keys: string[], prefix = 'aria-') { + const base = props[name] as Record | null | undefined; + return Object.fromEntries(keys.map((key) => [key, props[prefix + key] ?? base?.[key]])) as Record; +} + +function textProps(props: Props, platform: string, minor: number): Props { + const result = { ...props }; + for (const key of [ + 'accessibilityLabel', + 'accessibilityState', + 'accessible', + 'disabled', + 'aria-label', + 'aria-hidden', + ...stateKeys.map((key) => 'aria-' + key), + ]) + delete result[key]; + let state = props.accessibilityState as Record | null | undefined; + const stateDisabled = props['aria-disabled'] ?? state?.disabled; + const resolvedDisabled = props.disabled ?? stateDisabled; + const needsStateUpdate = + resolvedDisabled !== stateDisabled && + ((resolvedDisabled != null && resolvedDisabled !== false) || (stateDisabled != null && stateDisabled !== false)); + if ((minor >= 88 && needsStateUpdate) || stateKeys.some((key) => props['aria-' + key] != null)) { + state = + state == null + ? (Object.fromEntries(stateKeys.map((key) => [key, props['aria-' + key]])) as Record) + : merge(props, 'accessibilityState', stateKeys); + if (minor >= 88) state.disabled = resolvedDisabled as Primitive; + } + const disabled = (minor >= 88 ? resolvedDisabled : (props.disabled ?? state?.disabled)) as Primitive; + if ( + minor < 88 && + disabled !== state?.disabled && + ((disabled != null && disabled !== false) || (state?.disabled != null && state.disabled !== false)) + ) + state = { ...state, disabled }; + if (minor >= 84) { + delete result.accessibilityRole; + delete result.role; + if (minor === 84 || props.accessibilityRole != null) + result.accessibilityRole = props.accessibilityRole ?? undefined; + if (minor === 84 || props.role !== undefined) result.role = props.role; + } + const label = props['aria-label'] ?? props.accessibilityLabel; + if (minor < 85 || label !== undefined) result.accessibilityLabel = label; + if (minor < 85 || state !== undefined) result.accessibilityState = state; + // The renderer can flatten state.disabled onto disabled; Text writes its reconciled prop last. + result.accessible = platform === 'ios' ? props.accessible !== false : (props.accessible ?? false); + result.disabled = disabled; + const hidden = props['aria-hidden']; + if (minor < 85 || hidden !== undefined) + result.accessibilityElementsHidden = minor < 85 ? (hidden ?? props.accessibilityElementsHidden) : hidden; + if (minor < 85 || hidden === true) + result.importantForAccessibility = hidden === true ? 'no-hide-descendants' : props.importantForAccessibility; + return result; +} + +function viewProps(props: Props): Props | undefined { + const result = { ...props }; + for (const key of Object.keys(result)) + if (key.startsWith('aria-') || key === 'tabIndex' || key === 'accessibilityState' || key === 'accessibilityValue') + delete result[key]; + const labelledBy = props['aria-labelledby']; + // Array output belongs to View's existing single-alias fast path. + if (labelledBy != null) return; + if (props['aria-label'] !== undefined) result.accessibilityLabel = props['aria-label']; + if (props['aria-live'] !== undefined) + result.accessibilityLiveRegion = props['aria-live'] === 'off' ? 'none' : props['aria-live']; + if (props['aria-hidden'] !== undefined) result.accessibilityElementsHidden = props['aria-hidden']; + if (props['aria-hidden'] === true) result.importantForAccessibility = 'no-hide-descendants'; + if (props.tabIndex !== undefined) result.focusable = !props.tabIndex; + for (const [name, keys, prefix] of [ + ['accessibilityState', stateKeys, 'aria-'], + ['accessibilityValue', valueKeys, 'aria-value'], + ] as const) { + if (props[name] != null || keys.some((key) => props[prefix + key] != null)) + result[name] = merge(props, name, keys, prefix); + } + return result; +} + +function imageProps(props: Props, platform: string, minor: number): Props { + const result = { ...props }; + const legacy = minor < 85; + if (minor >= 88) { + for (const key of Object.keys(result)) + if (key.startsWith('aria-') || key === 'alt' || key === 'accessibilityState') delete result[key]; + if (props['aria-label'] != null) result.accessibilityLabel = props['aria-label']; + else if (props.alt != null && props.accessibilityLabel == null) result.accessibilityLabel = props.alt; + if (props['aria-labelledby'] != null) result.accessibilityLabelledBy = props['aria-labelledby']; + if (platform === 'ios' && props['aria-hidden'] === true) result.accessible = false; + else if (props.alt != null) result.accessible = true; + if (platform === 'android' && props['aria-hidden'] === true) + result.importantForAccessibility = 'no-hide-descendants'; + if (props.accessibilityState != null || stateKeys.some((key) => props['aria-' + key] != null)) + result.accessibilityState = merge(props, 'accessibilityState', stateKeys); + return result; + } + if (platform === 'android' && !legacy) { + for (const key of Object.keys(result)) + if ( + key.startsWith('aria-') || + ['alt', 'accessible', 'accessibilityLabel', 'accessibilityLabelledBy', 'accessibilityState'].includes(key) + ) + delete result[key]; + } else if (platform === 'ios') { + for (const key of ['aria-hidden', ...stateKeys.map((key) => 'aria-' + key)]) delete result[key]; + } + const label = props['aria-label'] ?? props.accessibilityLabel ?? props.alt; + const labelledBy = props['aria-labelledby'] ?? props.accessibilityLabelledBy; + if (platform === 'ios' || legacy || label != null) result.accessibilityLabel = label; + if (platform === 'android' && (legacy || labelledBy != null)) result.accessibilityLabelledBy = labelledBy; + if (platform === 'ios') + result.accessible = props['aria-hidden'] !== true && (props.alt === undefined ? props.accessible : true); + else if (legacy) result.accessible = props.alt === undefined ? props.accessible : true; + else if (props.alt != null || props.accessible != null) + result.accessible = props.alt == null ? props.accessible : true; + if (platform === 'android' && (legacy || props['aria-hidden'] === true)) + result.importantForAccessibility = + props['aria-hidden'] === true ? 'no-hide-descendants' : props.importantForAccessibility; + if (platform === 'ios') + result.accessibilityState = Object.hasOwn(props, 'accessibilityState') + ? props.accessibilityState + : merge(props, 'accessibilityState', stateKeys); + else if (legacy || props.accessibilityState != null || stateKeys.some((key) => props['aria-' + key] != null)) + result.accessibilityState = merge(props, 'accessibilityState', stateKeys); + return result; +} diff --git a/packages/react-native-boost/src/plugin/utils/unistyles.ts b/packages/react-native-boost/src/plugin/utils/unistyles.ts index 6588f52c..a53238aa 100644 --- a/packages/react-native-boost/src/plugin/utils/unistyles.ts +++ b/packages/react-native-boost/src/plugin/utils/unistyles.ts @@ -13,8 +13,8 @@ const DEPENDENCY_FIELDS = ['dependencies', 'devDependencies', 'peerDependencies' * `require.resolve` probe: in a monorepo (or with a hoisted/transitive copy) the package is often * resolvable from a project that does not actually use it, which would wrongly enable the mode and its * bailouts for unrelated packages. A declared dependency is the project's own signal that it uses - * Unistyles. The result still only auto-enables the mode and emits a one-time hint to set the `unistyles` - * flag explicitly, since a declared dependency does not prove Unistyles' own Babel plugin is active. + * Unistyles. The result still only auto-enables the mode and emits a one-time hint to set the integration + * to `on`, since a declared dependency does not prove Unistyles' own Babel plugin is active. */ export function isUnistylesInstalled(fromDirectory: string | undefined): boolean { const packageJson = readNearestPackageJson(fromDirectory ?? process.cwd()); diff --git a/packages/react-native-boost/src/runtime/__tests__/index.test.ts b/packages/react-native-boost/src/runtime/__tests__/index.test.ts index a407d1be..7c03d5c6 100644 --- a/packages/react-native-boost/src/runtime/__tests__/index.test.ts +++ b/packages/react-native-boost/src/runtime/__tests__/index.test.ts @@ -8,11 +8,22 @@ import { processImageSourceProps, getDefaultTextAccessible, clampNumberOfLines, - userSelectToSelectableMap, - verticalAlignToTextAlignVerticalMap, + processActivityIndicatorSize, + processActivityIndicatorStyle, + resolveActivityIndicatorDefault, } from '..'; import { Platform, StyleSheet, TextStyle } from 'react-native'; +function flattenStyle(style: unknown): unknown { + if (!Array.isArray(style)) return style; + const result: Record = {}; + for (const entry of style) { + const flat = flattenStyle(entry); + if (flat && typeof flat === 'object') Object.assign(result, flat); + } + return result; +} + vi.mock('../components/native-text', () => ({ NativeText: () => 'MockedNativeText', })); @@ -29,15 +40,6 @@ vi.mock('../components/native-image', () => ({ // reads the live `OS`, mirroring react-native's own implementation; tests flip `Platform.OS` and the // shared `afterEach` resets it. vi.mock('react-native', () => { - const flattenStyle = (style: unknown): unknown => { - if (!Array.isArray(style)) return style; - const result: Record = {}; - for (const entry of style) { - const flat = flattenStyle(entry); - if (flat && typeof flat === 'object') Object.assign(result, flat); - } - return result; - }; const Platform = { OS: 'ios' as 'ios' | 'android', // Backs the runtime's wrapper-version gates. Defaults to the RN this repo builds against; @@ -49,18 +51,21 @@ vi.mock('react-native', () => { }; return { View: () => 'View', + unstable_TextAncestorContext: undefined, Text: () => 'Text', Image: Object.assign(() => 'Image', { resolveAssetSource: (source: T): T => source, }), + ActivityIndicator: () => 'ActivityIndicator', Platform, StyleSheet: { flatten: flattenStyle, + compose: (first: unknown, second: unknown) => (second ? [first, second] : first), }, // Distinguishable stand-in for RN's `processColor` so `processSelectionColor` can be asserted to // actually call it (a named color β†’ packed int) rather than passing the value through unchanged. // `'invalid'` β†’ `undefined` models RN rejecting an unparseable color. - processColor: (color: unknown) => (color === 'red' ? 0xffff0000 : color === 'invalid' ? undefined : color), + processColor: (color: unknown) => (color === 'red' ? 0xff_ff_00_00 : color === 'invalid' ? undefined : color), }; }); @@ -79,7 +84,7 @@ const DEFAULT_MOCK_RN_VERSION = { major: 0, minor: 86, patch: 0 }; * is mutated in place and restored by {@link restorePlatformMock}. Pass `undefined` to model a host * that exposes no version at all. */ -const loadRuntime = async (minor: number | undefined, os: 'ios' | 'android' = 'android') => { +const loadRuntime = async (minor?: number, os: 'ios' | 'android' = 'android') => { vi.resetModules(); const { Platform: platformMock } = (await import('react-native')) as unknown as { Platform: PlatformMock }; platformMock.OS = os; @@ -96,15 +101,17 @@ const restorePlatformMock = async () => { describe('processTextStyle', () => { it('returns the RN 0.86 default style for falsy style', () => { - expect(processTextStyle(null)).toEqual({ style: { overflow: 'hidden' } }); - expect(processTextStyle()).toEqual({ style: { overflow: 'hidden' } }); + expect(processTextStyle(null)).toStrictEqual({ style: [{ overflow: 'hidden' }, null] }); + expect(processTextStyle()).toStrictEqual({ style: [{ overflow: 'hidden' }, undefined] }); }); - it('caches computed props', () => { + it('keeps the input identity without caching derived props', () => { const style = { color: 'red' }; const result1 = processTextStyle(style); const result2 = processTextStyle(style); - expect(result1).toBe(result2); + expect(result1).not.toBe(result2); + expect((result1.style as unknown[])[1]).toBe(style); + expect((result2.style as unknown[])[1]).toBe(style); }); it('converts numeric fontWeight to string', () => { @@ -117,7 +124,7 @@ describe('processTextStyle', () => { const style = { userSelect: 'none', color: 'blue' } as const; const result = processTextStyle(style); const resultStyle = StyleSheet.flatten(result.style) as TextStyle; - expect(result.selectable).toBe(userSelectToSelectableMap['none']); + expect(result.selectable).toBe(false); expect(resultStyle.userSelect).toBeUndefined(); expect(resultStyle.color).toBe('blue'); }); @@ -125,7 +132,7 @@ describe('processTextStyle', () => { it('maps verticalAlign to textAlignVertical and removes verticalAlign from style', () => { const style = { verticalAlign: 'top', fontSize: 16 } as const; const result = StyleSheet.flatten(processTextStyle(style).style) as TextStyle; - expect(result.textAlignVertical).toBe(verticalAlignToTextAlignVerticalMap['top']); + expect(result.textAlignVertical).toBe('top'); expect(result.verticalAlign).toBeUndefined(); }); @@ -139,8 +146,8 @@ describe('processTextStyle', () => { const result = processTextStyle(style); const resultStyle = StyleSheet.flatten(result.style) as TextStyle; expect(resultStyle.fontWeight).toBe('700'); - expect(result.selectable).toBe(userSelectToSelectableMap['auto']); - expect(resultStyle.textAlignVertical).toBe(verticalAlignToTextAlignVerticalMap['middle']); + expect(result.selectable).toBe(true); + expect(resultStyle.textAlignVertical).toBe('center'); expect(resultStyle.margin).toBe(10); expect(resultStyle.userSelect).toBeUndefined(); expect(resultStyle.verticalAlign).toBeUndefined(); @@ -148,19 +155,48 @@ describe('processTextStyle', () => { }); }); +describe('ActivityIndicator helpers', () => { + it('applies defaults only to undefined', () => { + expect(resolveActivityIndicatorDefault(undefined, true)).toBe(true); + expect(resolveActivityIndicatorDefault(null, true)).toBeNull(); + expect(resolveActivityIndicatorDefault(false, true)).toBe(false); + }); + + it('resolves built-in and numeric sizes', () => { + const small = processActivityIndicatorSize(); + const large = processActivityIndicatorSize('large'); + expect(small).toEqual({ style: { width: 20, height: 20 }, size: 'small' }); + expect(processActivityIndicatorSize()).toBe(small); + expect(large).toEqual({ style: { width: 36, height: 36 }, size: 'large' }); + expect(processActivityIndicatorSize('large')).toBe(large); + expect(processActivityIndicatorSize(24)).toEqual({ style: { height: 24, width: 24 }, size: undefined }); + expect(processActivityIndicatorSize(null as never)).toEqual({ + style: { height: null, width: null }, + size: undefined, + }); + }); + + it('composes the container and user styles', () => { + expect(processActivityIndicatorStyle(null)).toEqual({ alignItems: 'center', justifyContent: 'center' }); + expect(processActivityIndicatorStyle({ margin: 4 })).toEqual([ + { alignItems: 'center', justifyContent: 'center' }, + { margin: 4 }, + ]); + }); +}); + describe('processSelectionColor', () => { it('omits the prop for null/undefined input', () => { expect(processSelectionColor(null)).toEqual({}); - expect(processSelectionColor(undefined)).toEqual({}); expect(processSelectionColor()).toEqual({}); }); it('runs the value through processColor', () => { - expect(processSelectionColor('red')).toEqual({ selectionColor: 0xffff0000 }); + expect(processSelectionColor('red')).toEqual({ selectionColor: 0xff_ff_00_00 }); }); it('forwards the processColor result for an already-processed value', () => { - expect(processSelectionColor(0x12345678)).toEqual({ selectionColor: 0x12345678 }); + expect(processSelectionColor(0x12_34_56_78)).toEqual({ selectionColor: 0x12_34_56_78 }); }); it('omits the prop when processColor rejects the value', () => { @@ -183,33 +219,67 @@ describe('getDefaultTextAccessible', () => { describe('getDefaultTextStyle follows the installed RN version', () => { afterEach(restorePlatformMock); - it.each([83, 84])('returns no default on RN 0.%i', async (minor) => { - const runtime = await loadRuntime(minor); + it('returns no default before RN 0.85', async () => { + const runtime = await loadRuntime(84); expect(runtime.getDefaultTextStyle()).toBeUndefined(); expect(runtime.processTextStyle({ color: 'red' })).toEqual({ style: { color: 'red' } }); - expect(runtime.processTextStyle(null)).toEqual({}); + expect(runtime.processTextStyle(null)).toStrictEqual({ style: null }); }); - it.each([85, 86, 87])('prepends overflow hidden on RN 0.%i', async (minor) => { - const runtime = await loadRuntime(minor); + it('prepends overflow hidden from RN 0.85', async () => { + const runtime = await loadRuntime(85); expect(runtime.getDefaultTextStyle()).toEqual({ overflow: 'hidden' }); expect(runtime.processTextStyle({ color: 'red' })).toEqual({ style: [{ overflow: 'hidden' }, { color: 'red' }], }); - expect(runtime.processTextStyle(null)).toEqual({ style: { overflow: 'hidden' } }); + expect(runtime.processTextStyle(null)).toStrictEqual({ style: [{ overflow: 'hidden' }, null] }); + }); + + it('uses a build-time default when provided', async () => { + const runtime = await loadRuntime(86); + const style = { color: 'red' } as const; + expect(runtime.processTextStyle(style, false)).toEqual({ style }); + expect(runtime.processTextStyle(style, true)).toEqual({ + style: [{ overflow: 'hidden' }, style], + }); }); it('uses the current default when the version cannot be read', async () => { - const runtime = await loadRuntime(undefined); + const runtime = await loadRuntime(); expect(runtime.getDefaultTextStyle()).toEqual({ overflow: 'hidden' }); }); +}); - it('keeps the user overflow value', async () => { - const runtime = await loadRuntime(86); - expect(StyleSheet.flatten([runtime.getDefaultTextStyle(), { overflow: 'visible' }])).toEqual({ - overflow: 'visible', - }); - }); +describe('Text accessibility release defaults', () => { + afterEach(restorePlatformMock); + + it.each([83, 84, 85, 86, 87, 88, undefined])( + 'preserves null visibility and state ownership on RN 0.%s', + async (minor) => { + const runtime = await loadRuntime(minor); + const legacy = minor !== undefined && minor < 85; + const state = { disabled: true }; + const result = runtime.processTextAccessibilityProps({ + 'aria-hidden': null, + 'accessibilityElementsHidden': true, + 'accessibilityState': state, + 'disabled': false, + }); + expect(result.accessibilityElementsHidden).toBe(legacy ? true : null); + const mutatesState = minor !== undefined && minor >= 85 && minor < 88; + expect(result.accessibilityState === state).toBe(mutatesState); + expect(state.disabled).toBe(!mutatesState); + expect(Object.hasOwn(result, 'accessibilityLabel')).toBe(legacy); + if (legacy) { + expect( + runtime.processTextAccessibilityProps({ + accessibilityState: Object.freeze({ disabled: true }), + disabled: false, + }).accessibilityState + ).toStrictEqual({ disabled: false }); + } + } + ); }); describe('processTextAccessibilityProps', () => { @@ -479,6 +549,7 @@ describe('processImageAccessibilityProps', () => { it.each([ [84, { accessibilityLabel: 'Label', accessible: true }], [85, { accessibilityLabel: 'Label' }], + [88, { accessibilityLabel: 'Label' }], ])('handles a null alt on RN 0.%i', async (minor, expected) => { const runtime = await loadRuntime(minor); expect(runtime.processImageAccessibilityProps({ 'alt': null, 'aria-label': 'Label' })).toEqual(expected); @@ -487,6 +558,7 @@ describe('processImageAccessibilityProps', () => { it.each([ [84, { accessible: null }], [85, {}], + [88, { accessible: null }], ])('handles a null accessible prop on RN 0.%i', async (minor, expected) => { const runtime = await loadRuntime(minor); expect(runtime.processImageAccessibilityProps({ accessible: null })).toEqual(expected); @@ -647,7 +719,7 @@ describe('processImageSourceProps', () => { describe('object-source headers follow the installed RN version', () => { afterEach(restorePlatformMock); - it.each([83, 84, 87, 88])('lifts them on RN 0.%i', async (minor) => { + it.each([84, 87, 88])('lifts them on RN 0.%i', async (minor) => { const runtime = await loadRuntime(minor); const source = { uri: 'logo.png', headers: { Authorization: 'Bearer object' } }; expect(runtime.processImageSourceProps({ source }).headers).toEqual({ Authorization: 'Bearer object' }); @@ -656,23 +728,42 @@ describe('processImageSourceProps', () => { }); }); - it.each([85, 86])('drops them on RN 0.%i, matching the wrapper of that version', async (minor) => { - const runtime = await loadRuntime(minor); + it.each([83, 84, 85, 86, 87, 88, undefined])( + 'merges generated headers only from RN 0.88 (target %s)', + async (minor) => { + const runtime = await loadRuntime(minor); + const headers = { 'Authorization': 'Bearer token', 'Referrer-Policy': 'no-referrer' }; + const result = runtime.processImageSourceProps({ + source: { uri: 'logo.png', headers }, + referrerPolicy: 'origin', + }); + expect(result.source[0].headers).toStrictEqual( + minor === undefined || minor >= 88 ? headers : { 'Referrer-Policy': 'origin' } + ); + expect(result.headers).toStrictEqual(result.source[0].headers); + const nullHeaders = minor !== undefined && minor < 85 ? null : undefined; + expect(runtime.processImageObjectSourceHeaders(null)).toBe(nullHeaders); + expect(runtime.processImageSourceProps({ source: { uri: '', headers: null } }).headers).toBe(nullHeaders); + } + ); + + it('drops them on RN 0.85, matching the wrapper of that version', async () => { + const runtime = await loadRuntime(85); const source = { uri: 'logo.png', headers: { Authorization: 'Bearer object' } }; expect('headers' in runtime.processImageSourceProps({ source })).toBe(false); expect(runtime.processImageObjectSourceHeaders({ Authorization: 'Bearer object' })).toBeUndefined(); }); it('lifts them when the version cannot be read (the safe direction)', async () => { - const runtime = await loadRuntime(undefined); + const runtime = await loadRuntime(); expect( runtime.processImageSourceProps({ source: { uri: 'logo.png', headers: { Authorization: 'x' } } }).headers ).toEqual({ Authorization: 'x' }); }); // An ARRAY source's `source[0].headers` is lifted by every supported version, gate or not. - it.each([84, 86, 87])('always lifts array-source headers on RN 0.%i', async (minor) => { - const runtime = await loadRuntime(minor); + it('always lifts array-source headers', async () => { + const runtime = await loadRuntime(86); expect( runtime.processImageSourceProps({ source: [{ uri: 'logo.png', headers: { Authorization: 'Bearer first' } }] }) .headers @@ -687,14 +778,14 @@ describe('processImageSourceProps', () => { const source = [{ uri: 'logo.png', width: 16, height: 8 }]; - it.each([83, 84])('does not propagate on RN 0.%i', async (minor) => { - const runtime = await loadRuntime(minor); + it('does not propagate before RN 0.85', async () => { + const runtime = await loadRuntime(84); expect(runtime.processImageSourceProps({ source }).style).toEqual([false, { overflow: 'hidden' }, undefined]); expect(runtime.processImageArraySourceDimensions({ width: 16, height: 8 })).toBeUndefined(); }); - it.each([85, 86, 87])('propagates on RN 0.%i', async (minor) => { - const runtime = await loadRuntime(minor); + it('propagates from RN 0.85', async () => { + const runtime = await loadRuntime(85); expect(runtime.processImageSourceProps({ source }).style).toEqual([ { width: 16, height: 8 }, { overflow: 'hidden' }, @@ -719,7 +810,6 @@ describe('processImageSourceProps', () => { describe('clampNumberOfLines', () => { it('clamps negative values to 0', () => { expect(clampNumberOfLines(-1)).toBe(0); - expect(clampNumberOfLines(-10)).toBe(0); }); it('clamps NaN to 0 (RN uses !(value >= 0))', () => { @@ -733,6 +823,6 @@ describe('clampNumberOfLines', () => { it('passes null/undefined through untouched (RN guards with != null)', () => { expect(clampNumberOfLines(null)).toBeNull(); - expect(clampNumberOfLines(undefined)).toBeUndefined(); + expect(clampNumberOfLines()).toBeUndefined(); }); }); diff --git a/packages/react-native-boost/src/runtime/__tests__/mocks/react-native.ts b/packages/react-native-boost/src/runtime/__tests__/mocks/react-native.ts index 07f2236a..7f9cc26c 100644 --- a/packages/react-native-boost/src/runtime/__tests__/mocks/react-native.ts +++ b/packages/react-native-boost/src/runtime/__tests__/mocks/react-native.ts @@ -1,8 +1,10 @@ export const View = () => 'View'; +export const unstable_TextAncestorContext = undefined; export const Text = () => 'Text'; export const Image = Object.assign(() => 'Image', { resolveAssetSource: (source: T): T => source, }); +export const ActivityIndicator = () => 'ActivityIndicator'; // `constants.reactNativeVersion` backs the runtime's Image wrapper-version gates. Pinned rather than // read from the installed RN so unit expectations are deterministic; the version-dependent branches diff --git a/packages/react-native-boost/src/runtime/__tests__/native-activity-indicator.test.ts b/packages/react-native-boost/src/runtime/__tests__/native-activity-indicator.test.ts new file mode 100644 index 00000000..600c8caa --- /dev/null +++ b/packages/react-native-boost/src/runtime/__tests__/native-activity-indicator.test.ts @@ -0,0 +1,42 @@ +import type { ComponentType } from 'react'; +import type { ActivityIndicatorProps } from 'react-native'; +import { afterEach, describe, expect, it, vi } from 'vitest'; + +const reactNativeActivityIndicator = (() => null) as ComponentType; + +async function importNativeActivityIndicator(os: string) { + vi.resetModules(); + const loadActivityIndicator = vi.fn(() => reactNativeActivityIndicator); + vi.doMock('react-native', () => ({ + get ActivityIndicator() { + return loadActivityIndicator(); + }, + Platform: { OS: os }, + })); + + return { ...(await import('../components/native-activity-indicator')), loadActivityIndicator }; +} + +afterEach(() => { + vi.doUnmock('react-native'); + vi.resetModules(); +}); + +describe('NativeActivityIndicator', () => { + it.each([ + ['ios', 'RCTActivityIndicatorView'], + ['android', 'AndroidProgressBar'], + ])('loads React Native ActivityIndicator to register and use the %s host', async (os, host) => { + const { NativeActivityIndicator, loadActivityIndicator } = await importNativeActivityIndicator(os); + + expect(loadActivityIndicator).toHaveBeenCalledOnce(); + expect(NativeActivityIndicator).toBe(host); + }); + + it('uses React Native ActivityIndicator on other platforms', async () => { + const { NativeActivityIndicator, loadActivityIndicator } = await importNativeActivityIndicator('web'); + + expect(loadActivityIndicator).toHaveBeenCalledOnce(); + expect(NativeActivityIndicator).toBe(reactNativeActivityIndicator); + }); +}); diff --git a/packages/react-native-boost/src/runtime/__tests__/native-view-with-context.test.tsx b/packages/react-native-boost/src/runtime/__tests__/native-view-with-context.test.tsx new file mode 100644 index 00000000..e0a6c75e --- /dev/null +++ b/packages/react-native-boost/src/runtime/__tests__/native-view-with-context.test.tsx @@ -0,0 +1,91 @@ +import { readFileSync } from 'node:fs'; +import { runInNewContext } from 'node:vm'; +import { URL } from 'node:url'; +import * as React from 'react'; +import * as jsxRuntime from 'react/jsx-runtime'; +import { use } from 'react'; +import type { ReactNode } from 'react'; +import { renderToStaticMarkup } from 'react-dom/server'; +import { unstable_TextAncestorContext as TextAncestorContext } from 'react-native'; +import { afterEach, expect, it, vi } from 'vitest'; +import { NativeViewWithContext } from '../components/native-view-with-context'; +import { NativeView } from '../components/native-view'; + +const captures = vi.hoisted(() => [] as Array<{ props: Record; context: boolean }>); + +vi.mock('react-native', async () => { + const { createContext } = await import('react'); + return { View: () => null, unstable_TextAncestorContext: createContext(false) }; +}); +vi.mock('../components/native-view', () => ({ + NativeView: (props: Record) => { + captures.push({ props, context: use(TextAncestorContext) }); + return props.children as ReactNode; + }, +})); + +afterEach(() => { + captures.length = 0; + vi.doUnmock('react-native'); + vi.resetModules(); +}); + +it.each([false, true])('preserves props and resets incoming Text context %s', (insideText) => { + const child = label; + const props = { ref: vi.fn(), style: { width: 12 }, testID: 'box', children: child }; + renderToStaticMarkup( + + + + ); + expect(captures).toHaveLength(1); + expect(captures[0].context).toBe(false); + expect(captures[0].props).toEqual(props); + expect(captures[0].props.children).toBe(child); + expect(captures[0].props.style).toBe(props.style); + expect(captures[0].props.ref).toBe(props.ref); +}); + +it('renders the built CommonJS runtime without a global React variable', () => { + const runtime = {} as typeof import('../index'); + runInNewContext(readFileSync(new URL('../../../dist/runtime/index.js', import.meta.url), 'utf8'), { + exports: runtime, + require: (name: string) => { + if (name === 'react') return React; + if (name === 'react/jsx-runtime') return jsxRuntime; + if (name === 'react-native') + return { + View: () => null, + Image: () => null, + Platform: { OS: 'ios' }, + unstable_NativeView: NativeView, + unstable_TextAncestorContext: TextAncestorContext, + }; + throw new Error(`Unexpected runtime dependency: ${name}`); + }, + }); + renderToStaticMarkup( + + + label + + + ); + expect(captures).toHaveLength(1); + expect(captures[0].context).toBe(false); +}); + +it('falls back to View when the native context is unavailable', async () => { + const View = vi.fn(() => null); + vi.doMock('react-native', () => ({ View, unstable_TextAncestorContext: undefined })); + const runtime = await import('../components/native-view-with-context'); + expect(runtime.NativeViewWithContext).toBe(View); +}); + +it('falls back directly when NativeView already uses View', async () => { + const View = vi.fn(() => null); + vi.doMock('react-native', () => ({ View, unstable_TextAncestorContext: TextAncestorContext })); + vi.doMock('../components/native-view', () => ({ NativeView: View })); + const runtime = await import('../components/native-view-with-context'); + expect(runtime.NativeViewWithContext).toBe(View); +}); diff --git a/packages/react-native-boost/src/runtime/components/native-activity-indicator.ts b/packages/react-native-boost/src/runtime/components/native-activity-indicator.ts new file mode 100644 index 00000000..3347bf6c --- /dev/null +++ b/packages/react-native-boost/src/runtime/components/native-activity-indicator.ts @@ -0,0 +1,22 @@ +import type { ComponentType } from 'react'; +import type { ActivityIndicatorProps } from 'react-native'; +import * as reactNativeModule from 'react-native'; + +type ReactNativeActivityIndicatorModule = { + ActivityIndicator: ComponentType; + Platform: { + OS: string; + }; +}; + +const { ActivityIndicator, Platform } = reactNativeModule as ReactNativeActivityIndicatorModule; + +/** + * Native ActivityIndicator component with graceful fallback. + */ +export const NativeActivityIndicator: ComponentType = + Platform.OS === 'android' + ? ('AndroidProgressBar' as unknown as ComponentType) + : Platform.OS === 'ios' + ? ('RCTActivityIndicatorView' as unknown as ComponentType) + : ActivityIndicator; diff --git a/packages/react-native-boost/src/runtime/components/native-image.tsx b/packages/react-native-boost/src/runtime/components/native-image.ts similarity index 51% rename from packages/react-native-boost/src/runtime/components/native-image.tsx rename to packages/react-native-boost/src/runtime/components/native-image.ts index 03c2db41..cafd2240 100644 --- a/packages/react-native-boost/src/runtime/components/native-image.tsx +++ b/packages/react-native-boost/src/runtime/components/native-image.ts @@ -9,15 +9,10 @@ type ReactNativeImageModule = { }; }; -// Accessing Image loads its module, which registers the RCTImageView host before the string is used. const { Image, Platform } = reactNativeModule as ReactNativeImageModule; /** - * Native Image host registered by React Native's public `Image` module. - * - * React Native's component registry returns the host name as the component value. This avoids RN's - * deprecated `Libraries` deep import and its Babel warning. Keep the public `Image` access above: - * using the string without first loading `Image` leaves the host unregistered. + * Native Image component with graceful fallback. */ export const NativeImage: ComponentType = Platform.OS === 'web' ? Image : ('RCTImageView' as unknown as ComponentType); diff --git a/packages/react-native-boost/src/runtime/components/native-text.tsx b/packages/react-native-boost/src/runtime/components/native-text.ts similarity index 67% rename from packages/react-native-boost/src/runtime/components/native-text.tsx rename to packages/react-native-boost/src/runtime/components/native-text.ts index ca8e967b..fbca8403 100644 --- a/packages/react-native-boost/src/runtime/components/native-text.tsx +++ b/packages/react-native-boost/src/runtime/components/native-text.ts @@ -9,15 +9,10 @@ const isWeb = reactNative.Platform.OS === 'web'; let nativeText = reactNative.unstable_NativeText; if (isWeb || nativeText == null) { - // Fallback to regular Text component if unstable_NativeText is not available or we're on Web nativeText = reactNative.Text; } /** * Native Text component with graceful fallback. - * - * @remarks - * Uses `unstable_NativeText` on supported native runtimes and falls back to `Text` - * on web or when the unstable export is unavailable. */ export const NativeText: ComponentType = nativeText; diff --git a/packages/react-native-boost/src/runtime/components/native-view-with-context.ts b/packages/react-native-boost/src/runtime/components/native-view-with-context.ts new file mode 100644 index 00000000..5e6aad1a --- /dev/null +++ b/packages/react-native-boost/src/runtime/components/native-view-with-context.ts @@ -0,0 +1,16 @@ +import { use } from 'react'; +import { jsx } from 'react/jsx-runtime'; +import type { ComponentPropsWithRef, ComponentType } from 'react'; +import * as reactNativeModule from 'react-native'; +import { NativeView } from './native-view'; + +const { View, unstable_TextAncestorContext: TextAncestorContext } = reactNativeModule; + +export const NativeViewWithContext: ComponentType> = + TextAncestorContext == null || NativeView === View + ? View + : function NativeViewWithContext(props) { + const hasTextAncestor = use(TextAncestorContext); + const view = jsx(NativeView, props); + return hasTextAncestor ? jsx(TextAncestorContext, { value: false, children: view }) : view; + }; diff --git a/packages/react-native-boost/src/runtime/components/native-view.tsx b/packages/react-native-boost/src/runtime/components/native-view.ts similarity index 67% rename from packages/react-native-boost/src/runtime/components/native-view.tsx rename to packages/react-native-boost/src/runtime/components/native-view.ts index 6a66f277..ad0efdcc 100644 --- a/packages/react-native-boost/src/runtime/components/native-view.tsx +++ b/packages/react-native-boost/src/runtime/components/native-view.ts @@ -9,15 +9,10 @@ const isWeb = reactNative.Platform.OS === 'web'; let nativeView = reactNative.unstable_NativeView; if (isWeb || nativeView == null) { - // Fallback to regular View component if unstable_NativeView is not available or we're on Web nativeView = reactNative.View; } /** * Native View component with graceful fallback. - * - * @remarks - * Uses `unstable_NativeView` on supported native runtimes and falls back to `View` - * on web or when the unstable export is unavailable. */ export const NativeView: ComponentType = nativeView; diff --git a/packages/react-native-boost/src/runtime/index.ts b/packages/react-native-boost/src/runtime/index.ts index 70222f58..3e57d952 100644 --- a/packages/react-native-boost/src/runtime/index.ts +++ b/packages/react-native-boost/src/runtime/index.ts @@ -6,18 +6,23 @@ import { Image as RNImage, processColor as rnProcessColor, } from 'react-native'; -import type { ColorValue, ProcessedColorValue } from 'react-native'; +import type { ActivityIndicatorProps, ColorValue, ProcessedColorValue } from 'react-native'; import { GenericStyleProp } from './types'; import { userSelectToSelectableMap, verticalAlignToTextAlignVerticalMap } from './utils/constants'; -const propsCache = new WeakMap(); const imageBaseStyle = { overflow: 'hidden' } as const; -const textDefaultOverflowStyle = { overflow: 'hidden' } as const; +export const textDefaultOverflowStyle = { overflow: 'hidden' } as const; const emptyImageSource = { uri: undefined, width: undefined, height: undefined }; +export const activityIndicatorStyles = { + container: { alignItems: 'center', justifyContent: 'center' }, + small: { width: 20, height: 20 }, + large: { width: 36, height: 36 }, +} as const; +let activityIndicatorSmallProps: { style: (typeof activityIndicatorStyles)['small']; size: 'small' } | undefined; +let activityIndicatorLargeProps: { style: (typeof activityIndicatorStyles)['large']; size: 'large' } | undefined; // Resolve RN's `processColor` once. The `typeof` guard degrades to a passthrough on -// a non-RN host that lacks it (see {@link processSelectionColor}); the web build never reaches this β€” it -// uses the separate `index.web.ts` shim. +// a non-RN host that lacks it (see {@link processSelectionColor}). const processColor: ((color?: ColorValue | number) => ProcessedColorValue | null | undefined) | undefined = typeof rnProcessColor === 'function' ? rnProcessColor : undefined; const resolveImageAssetSource = @@ -41,8 +46,8 @@ let cachedReactNativeMinor: number | null | undefined; * Callers treat `null` as "the current wrapper behavior". * * @remarks - * `Platform.constants` is a synchronous native-constants read, so it happens lazily on first use and - * is memoized rather than run at module load. + * This fallback is used only when the Babel plugin cannot resolve the build target. The synchronous + * native-constants read happens lazily on first use and is memoized. * * Boost intentionally mirrors release defaults by version. Do not import RN's private feature flags: * their paths are unstable, and apps that override private flags must patch Boost or skip optimization. @@ -66,8 +71,7 @@ let cachedDefaultTextStyle: TextStyle | false | undefined; /** * The default style `Text` prepends to every element's `style` β€” `{ overflow: 'hidden' }` on RN β‰₯ - * 0.85, and `undefined` otherwise. The plugin prepends this as the first `style` array entry of every - * optimized `Text`, so the user's own `overflow` still wins. + * 0.85, and `undefined` otherwise. The plugin uses this fallback when the build target is unknown. */ export function getDefaultTextStyle(): TextStyle | undefined { const minor = getReactNativeMinor(); @@ -103,19 +107,19 @@ function propagatesArraySourceDimensions(): boolean { } /** - * Gates a plain OBJECT source's inline `headers` for the top-level Android `headers` prop. The - * plugin wraps the statically-extracted headers in this call so build-time output tracks the - * installed RN exactly. See {@link liftsObjectSourceHeaders}. + * Gates a plain OBJECT source's inline `headers` for the top-level Android `headers` prop when the + * build target is unknown. See {@link liftsObjectSourceHeaders}. */ export function processImageObjectSourceHeaders(headers: T): T | undefined { + const minor = getReactNativeMinor(); + if (headers === null && (minor === null || minor >= 85)) return undefined; return liftsObjectSourceHeaders() ? headers : undefined; } /** - * Gates the layout style entry synthesized from a single-entry ARRAY source's intrinsic dimensions. - * The plugin emits this as the first `style` array entry so build-time output tracks the installed - * RN exactly; an `undefined` entry is ignored by style flattening, exactly like the wrapper's - * `false`. See {@link propagatesArraySourceDimensions}. + * Gates a single-entry ARRAY source's intrinsic dimensions when the build target is unknown. An + * `undefined` entry is ignored by style flattening, exactly like the wrapper's `false`. + * See {@link propagatesArraySourceDimensions}. */ export function processImageArraySourceDimensions(dimensions: T): T | undefined { return propagatesArraySourceDimensions() ? dimensions : undefined; @@ -125,52 +129,48 @@ export function processImageArraySourceDimensions(dimensions: T): T | undefin * Normalizes `Text` style values for `NativeText`. * * @param style - Style prop passed to a text-like component. - * @returns Native-friendly text props. Returns an empty object when `style` is falsy or cannot be normalized. + * @param includesDefaultStyle - Build-time release default. Omit it to detect the runtime version. + * @returns Native text props with the authored style and wrapper overrides preserved. * @remarks - * - Flattens style arrays via `StyleSheet.flatten` + * - Inspects style arrays via `StyleSheet.flatten`, without replacing the authored style * - Converts numeric `fontWeight` values to string values * - Maps `userSelect` and `verticalAlign` to native-compatible props - * - Prepends {@link getDefaultTextStyle} so the versioned `overflow: 'hidden'` default applies to - * dynamically-styled (and falsy-styled) `Text` exactly as the wrapper applies it + * - Applies the build-time default-style setting, or uses {@link getDefaultTextStyle} as a fallback */ -export function processTextStyle(style: GenericStyleProp): Partial { - const defaultTextStyle = getDefaultTextStyle(); - - if (!style) return defaultTextStyle ? { style: defaultTextStyle } : {}; - - // Cache the computed props - let props = propsCache.get(style); - if (props) return props; - - props = {}; - propsCache.set(style, props); - - const flattenedStyle = StyleSheet.flatten(style) as TextStyle; - - if (!flattenedStyle) { - if (defaultTextStyle) props.style = defaultTextStyle; - return props; +export function processTextStyle( + style: GenericStyleProp, + includesDefaultStyle?: boolean +): Partial { + const defaultTextStyle = + includesDefaultStyle === undefined + ? getDefaultTextStyle() + : includesDefaultStyle + ? textDefaultOverflowStyle + : undefined; + const props: { style?: TextProps['style']; selectable?: boolean } = {}; + const flattenedStyle = StyleSheet.flatten(style) as TextStyle | undefined; + let overrides: { -readonly [Key in keyof TextStyle]: TextStyle[Key] } | undefined; + + if (typeof flattenedStyle?.fontWeight === 'number') { + overrides = { fontWeight: String(flattenedStyle.fontWeight) as TextStyle['fontWeight'] }; } - - const processedStyle = { ...flattenedStyle }; - - if (typeof processedStyle.fontWeight === 'number') { - processedStyle.fontWeight = processedStyle.fontWeight.toString() as TextStyle['fontWeight']; - } - - if (processedStyle.userSelect != null) { - props.selectable = userSelectToSelectableMap[processedStyle.userSelect]; - delete processedStyle.userSelect; + if (flattenedStyle?.userSelect != null) { + props.selectable = userSelectToSelectableMap[flattenedStyle.userSelect]; + (overrides ??= {}).userSelect = undefined; } - - if (processedStyle.verticalAlign != null) { - processedStyle.textAlignVertical = verticalAlignToTextAlignVerticalMap[ - processedStyle.verticalAlign + if (flattenedStyle?.verticalAlign != null) { + overrides ??= {}; + overrides.textAlignVertical = verticalAlignToTextAlignVerticalMap[ + flattenedStyle.verticalAlign ] as TextStyle['textAlignVertical']; - delete processedStyle.verticalAlign; + overrides.verticalAlign = undefined; } - props.style = defaultTextStyle ? [defaultTextStyle, processedStyle] : processedStyle; + // The renderer processes every authored entry, then RN's overrides. Do not flatten or cache this shape. + // Older RN types exclude recursive readonly arrays, although the renderer accepts them. + const normalizedStyle = (overrides ? [style, overrides] : style) as TextProps['style']; + if (defaultTextStyle) props.style = [defaultTextStyle, normalizedStyle]; + else if (normalizedStyle !== undefined) props.style = normalizedStyle; return props; } @@ -199,6 +199,24 @@ export function processSelectionColor(selectionColor?: ColorValue | number | nul return processed === undefined ? {} : { selectionColor: processed }; } +export function resolveActivityIndicatorDefault(value: T | undefined, fallback: T): T { + return value === undefined ? fallback : value; +} + +export function processActivityIndicatorStyle(style: ActivityIndicatorProps['style']) { + return StyleSheet.compose(activityIndicatorStyles.container, style); +} + +export function processActivityIndicatorSize(size: ActivityIndicatorProps['size'] = 'small') { + if (size === 'small') { + return (activityIndicatorSmallProps ??= { style: activityIndicatorStyles.small, size: 'small' }); + } + if (size === 'large') { + return (activityIndicatorLargeProps ??= { style: activityIndicatorStyles.large, size: 'large' }); + } + return { style: { height: size, width: size }, size: undefined }; +} + // eslint-disable-next-line @typescript-eslint/no-explicit-any type ImageSourceHelperProps = Record; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -219,7 +237,8 @@ function getImageSourcesFromProps(props: ImageSourceHelperProps): ImageSource | return [{ uri: props.src, headers, width: props.width, height: props.height }]; } if (source != null && source.uri && Object.keys(headers).length > 0) { - return [{ ...source, headers }]; + const minor = getReactNativeMinor(); + return [{ ...source, headers: minor === null || minor >= 88 ? { ...headers, ...source.headers } : headers }]; } return source; } @@ -276,11 +295,20 @@ export function processImageSourceProps(props: ImageSourceHelperProps): Record Platform.select({ ios: true, android: false }); /** - * Translates `aria-hidden` into its native counterparts for the `Text` helper, mirroring `Text.js`'s - * legacy path: `aria-hidden` supplies `accessibilityElementsHidden`, falling back to an explicit value - * when it is nullish (`??`), and forces `importantForAccessibility` to `'no-hide-descendants'` only when - * it is strictly `true` (otherwise the explicit value is preserved). The `??` fallback is the legacy - * superset of the two RN `Text` implementations, so this is correct regardless of which path a given RN - * version runs. - * - * The `View` helper deliberately does NOT reuse this: `View.js` assigns `accessibilityElementsHidden` - * directly under an `=== undefined` guard (no `??`), so the two diverge for a nullish `aria-hidden` and - * must stay separate. + * Translates Text's ARIA visibility props. The caller applies RN < 0.85's nullish fallback. */ function applyAriaHidden( ariaHidden: unknown, @@ -315,7 +334,7 @@ function applyAriaHidden( importantForAccessibility?: unknown ): { accessibilityElementsHidden: unknown; importantForAccessibility: unknown } { return { - accessibilityElementsHidden: ariaHidden ?? accessibilityElementsHidden, + accessibilityElementsHidden: ariaHidden === undefined ? accessibilityElementsHidden : ariaHidden, importantForAccessibility: ariaHidden === true ? 'no-hide-descendants' : importantForAccessibility, }; } @@ -346,8 +365,6 @@ export function processTextAccessibilityProps(props: Record): Recor ['aria-expanded']: ariaExpanded, ['aria-selected']: ariaSelected, ['aria-hidden']: ariaHidden, - accessibilityElementsHidden, - importantForAccessibility, accessible, disabled, ...restProperties @@ -356,44 +373,60 @@ export function processTextAccessibilityProps(props: Record): Recor // Merge label props: prefer the aria-label if defined. const normalizedLabel = ariaLabel ?? accessibilityLabel; - // Merge the accessibilityState with any provided ARIA properties. + const minor = getReactNativeMinor(); + const copiesState = minor === null || minor >= 88; + const stateDisabled = copiesState ? (ariaDisabled ?? accessibilityState?.disabled) : undefined; + const resolvedDisabled = disabled ?? stateDisabled; + const needsStateUpdate = + resolvedDisabled !== stateDisabled && + ((resolvedDisabled != null && resolvedDisabled !== false) || (stateDisabled != null && stateDisabled !== false)); let normalizedState = accessibilityState; - if (ariaBusy != null || ariaChecked != null || ariaDisabled != null || ariaExpanded != null || ariaSelected != null) { + if ( + (copiesState && needsStateUpdate) || + ariaBusy != null || + ariaChecked != null || + ariaDisabled != null || + ariaExpanded != null || + ariaSelected != null + ) { normalizedState = normalizedState == null ? { busy: ariaBusy, checked: ariaChecked, - disabled: ariaDisabled, + disabled: copiesState ? resolvedDisabled : ariaDisabled, expanded: ariaExpanded, selected: ariaSelected, } : { busy: ariaBusy ?? normalizedState.busy, checked: ariaChecked ?? normalizedState.checked, - disabled: ariaDisabled ?? normalizedState.disabled, + disabled: copiesState ? resolvedDisabled : (ariaDisabled ?? normalizedState.disabled), expanded: ariaExpanded ?? normalizedState.expanded, selected: ariaSelected ?? normalizedState.selected, }; } - // Reconcile `disabled` with `accessibilityState.disabled`. When the two are out of sync (and not - // both falsy) the explicit `disabled` prop wins and is mirrored back into the state object, so the - // native host receives a consistent value on both fields. - const stateDisabled = normalizedState?.disabled; - const normalizedDisabled = disabled ?? stateDisabled; + // RN 0.85–0.87 mutates conflicting state; 0.88 copies only the five native fields. + const legacy = minor !== null && minor < 85; + const mergedDisabled = normalizedState?.disabled; + const normalizedDisabled = copiesState ? resolvedDisabled : (disabled ?? mergedDisabled); if ( - normalizedDisabled !== stateDisabled && - ((normalizedDisabled != null && normalizedDisabled !== false) || (stateDisabled != null && stateDisabled !== false)) + !copiesState && + normalizedDisabled !== mergedDisabled && + ((normalizedDisabled != null && normalizedDisabled !== false) || + (mergedDisabled != null && mergedDisabled !== false)) ) { - normalizedState = { ...normalizedState, disabled: normalizedDisabled }; + if (legacy || normalizedState == null) normalizedState = { ...normalizedState, disabled: normalizedDisabled }; + else normalizedState.disabled = normalizedDisabled; } - // `aria-hidden` β†’ `accessibilityElementsHidden` / `importantForAccessibility`. The explicit native - // props are consumed (destructured out of `restProperties`) so `aria-hidden` wins over them, matching - // the wrapper; the plugin routes both into this call when `aria-hidden` is present. const { accessibilityElementsHidden: normalizedElementsHidden, importantForAccessibility: normalizedImportant } = - applyAriaHidden(ariaHidden, accessibilityElementsHidden, importantForAccessibility); + applyAriaHidden( + legacy && ariaHidden === null ? undefined : ariaHidden, + restProperties.accessibilityElementsHidden, + restProperties.importantForAccessibility + ); // Resolve `accessible` exactly as `Text` does: opt-out on iOS, off by default on Android. The // Android pressable case (`onPress`/`onLongPress`) never applies β€” press handlers bail out of @@ -404,15 +437,20 @@ export function processTextAccessibilityProps(props: Record): Recor default: accessible, }); - return { + const result: Record = { ...restProperties, - accessibilityLabel: normalizedLabel, - accessibilityState: normalizedState, - accessibilityElementsHidden: normalizedElementsHidden, - importantForAccessibility: normalizedImportant, accessible: normalizedAccessible, disabled: normalizedDisabled, }; + if (legacy || normalizedLabel !== undefined) result.accessibilityLabel = normalizedLabel; + if (legacy || normalizedState !== undefined) result.accessibilityState = normalizedState; + if (legacy || ariaHidden !== undefined) { + result.accessibilityElementsHidden = normalizedElementsHidden; + } + if (legacy || ariaHidden === true) { + result.importantForAccessibility = normalizedImportant; + } + return result; } /** @@ -463,9 +501,6 @@ export function processViewAccessibilityProps(props: Record): Recor if (parsedAriaLabelledBy !== undefined) result.accessibilityLabelledBy = parsedAriaLabelledBy; if (ariaLabel !== undefined) result.accessibilityLabel = ariaLabel; if (ariaLive !== undefined) result.accessibilityLiveRegion = ariaLive === 'off' ? 'none' : ariaLive; - // Direct assignment under an `!== undefined` guard, matching `View.js` exactly. This is NOT the - // `Text` helper's `applyAriaHidden` rule (which uses `??`): the two diverge for a nullish value, so - // they must stay separate. if (ariaHidden !== undefined) { result.accessibilityElementsHidden = ariaHidden; if (ariaHidden === true) result.importantForAccessibility = 'no-hide-descendants'; @@ -536,42 +571,55 @@ export function processImageAccessibilityProps(props: Record): Reco } = props; const result = restProperties; - const normalizedLabel = ariaLabel ?? accessibilityLabel ?? alt; - if (normalizedLabel !== undefined) result.accessibilityLabel = normalizedLabel; - - if (Platform.OS === 'android') { - const normalizedLabelledBy = ariaLabelledBy ?? accessibilityLabelledBy; - if (normalizedLabelledBy !== undefined) result.accessibilityLabelledBy = normalizedLabelledBy; - } else if (accessibilityLabelledBy !== undefined) { - result.accessibilityLabelledBy = accessibilityLabelledBy; - } - - // RN 0.85 changed Android from undefined checks to nullish checks. - if (Platform.OS === 'ios') { - if (ariaHidden === true) { - result.accessible = false; - } else if (alt !== undefined) { - result.accessible = true; - } else if (accessible !== undefined) { - result.accessible = accessible; + const minor = getReactNativeMinor(); + const modern = minor === null || minor >= 88; + if (modern) { + for (const key of ['accessible', 'accessibilityLabel', 'accessibilityLabelledBy', 'importantForAccessibility']) { + if (Object.hasOwn(props, key)) result[key] = props[key]; } + if (ariaLabel != null) result.accessibilityLabel = ariaLabel; + else if (alt != null && accessibilityLabel == null) result.accessibilityLabel = alt; + if (ariaLabelledBy != null) result.accessibilityLabelledBy = ariaLabelledBy; + if (Platform.OS === 'ios' && ariaHidden === true) result.accessible = false; + else if (alt != null) result.accessible = true; + if (Platform.OS !== 'ios' && ariaHidden === true) result.importantForAccessibility = 'no-hide-descendants'; } else { - const minor = getReactNativeMinor(); - const usesUndefinedChecks = minor !== null && minor <= 84; - if (usesUndefinedChecks ? alt !== undefined : alt != null) { - result.accessible = true; - } else if (usesUndefinedChecks ? accessible !== undefined : accessible != null) { - result.accessible = accessible; + const normalizedLabel = ariaLabel ?? accessibilityLabel ?? alt; + if (normalizedLabel !== undefined) result.accessibilityLabel = normalizedLabel; + + if (Platform.OS === 'android') { + const normalizedLabelledBy = ariaLabelledBy ?? accessibilityLabelledBy; + if (normalizedLabelledBy !== undefined) result.accessibilityLabelledBy = normalizedLabelledBy; + } else if (accessibilityLabelledBy !== undefined) { + result.accessibilityLabelledBy = accessibilityLabelledBy; } - } - if (ariaHidden === true && Platform.OS !== 'ios') { - result.importantForAccessibility = 'no-hide-descendants'; - } else if (importantForAccessibility !== undefined) { - result.importantForAccessibility = importantForAccessibility; + // RN 0.85 changed Android from undefined checks to nullish checks. + if (Platform.OS === 'ios') { + if (ariaHidden === true) { + result.accessible = false; + } else if (alt !== undefined) { + result.accessible = true; + } else if (accessible !== undefined) { + result.accessible = accessible; + } + } else { + const usesUndefinedChecks = minor !== null && minor <= 84; + if (usesUndefinedChecks ? alt !== undefined : alt != null) { + result.accessible = true; + } else if (usesUndefinedChecks ? accessible !== undefined : accessible != null) { + result.accessible = accessible; + } + } + + if (ariaHidden === true && Platform.OS !== 'ios') { + result.importantForAccessibility = 'no-hide-descendants'; + } else if (importantForAccessibility !== undefined) { + result.importantForAccessibility = importantForAccessibility; + } } - if (Platform.OS === 'ios' && accessibilityState !== undefined) { + if (!modern && Platform.OS === 'ios' && accessibilityState !== undefined) { result.accessibilityState = accessibilityState; } else if ( accessibilityState != null || @@ -597,4 +645,6 @@ export * from './types'; export * from './utils/constants'; export * from './components/native-text'; export * from './components/native-view'; +export { NativeViewWithContext } from './components/native-view-with-context'; export { NativeImage } from './components/native-image'; +export { NativeActivityIndicator } from './components/native-activity-indicator'; diff --git a/packages/react-native-boost/src/runtime/index.web.ts b/packages/react-native-boost/src/runtime/index.web.ts index 95a02499..5e64489f 100644 --- a/packages/react-native-boost/src/runtime/index.web.ts +++ b/packages/react-native-boost/src/runtime/index.web.ts @@ -1,9 +1,13 @@ // This is a dummy file to ensure that nothing breaks when using the runtime in a web environment. -import { TextProps, TextStyle } from 'react-native'; +import { ActivityIndicator as NativeActivityIndicator } from 'react-native'; +import type { ActivityIndicatorProps, TextProps, TextStyle } from 'react-native'; import { GenericStyleProp } from './types'; -export const processTextStyle = (style: GenericStyleProp) => ({ style }) as Partial; +export const textDefaultOverflowStyle = { overflow: 'hidden' } as const; + +export const processTextStyle = (style: GenericStyleProp, _includesDefaultStyle?: boolean) => + ({ style }) as Partial; // react-native-web's `Text` accepts `selectionColor` and resolves colors itself, so there is no native // int to pack β€” pass the value through untouched (mirroring the other web shims). Keeping the @@ -12,6 +16,28 @@ export function processSelectionColor(selectionColor?: unknown): { selectionColo return selectionColor == null ? {} : { selectionColor }; } +export function resolveActivityIndicatorDefault(value: T | undefined, fallback: T): T { + return value === undefined ? fallback : value; +} + +export const activityIndicatorStyles = { + container: { alignItems: 'center', justifyContent: 'center' }, + small: { width: 20, height: 20 }, + large: { width: 36, height: 36 }, +} as const; + +export function processActivityIndicatorStyle(style: ActivityIndicatorProps['style']): ActivityIndicatorProps['style'] { + return style ? [activityIndicatorStyles.container, style] : activityIndicatorStyles.container; +} + +export function processActivityIndicatorSize(size: ActivityIndicatorProps['size'] = 'small') { + if (size === 'small') return { style: activityIndicatorStyles.small, size }; + if (size === 'large') return { style: activityIndicatorStyles.large, size }; + return { style: { height: size, width: size }, size: undefined }; +} + +export { NativeActivityIndicator }; + // On Web there is no platform-specific `accessible` default to apply; react-native-web's `Text` // derives accessibility from the rendered DOM. Returning `undefined` makes the injected // `accessible={getDefaultTextAccessible()}` a no-op. @@ -57,5 +83,6 @@ export * from './utils/constants'; /* eslint-disable @typescript-eslint/no-require-imports,unicorn/prefer-module */ export const NativeText = require('react-native').Text; export const NativeView = require('react-native').View; +export const NativeViewWithContext = NativeView; export const NativeImage = require('react-native').Image; /* eslint-enable @typescript-eslint/no-require-imports,unicorn/prefer-module */ diff --git a/packages/react-native-boost/src/runtime/uniwind-hooks.d.ts b/packages/react-native-boost/src/runtime/uniwind-hooks.d.ts new file mode 100644 index 00000000..56528880 --- /dev/null +++ b/packages/react-native-boost/src/runtime/uniwind-hooks.d.ts @@ -0,0 +1,16 @@ +declare module 'react-native-boost/uniwind/useStyle' { + export function useStyle( + className: string | undefined, + props: object, + state?: { isPressed: boolean; isDisabled: boolean } + ): import('react-native').ViewStyle & + import('react-native').TextStyle & + import('react-native').ImageStyle & { WebkitLineClamp?: number }; +} +declare module 'react-native-boost/uniwind/useAccentColor' { + export function useAccentColor( + className: string | undefined, + props: object, + state?: { isPressed: boolean; isDisabled: boolean } + ): import('react-native').ColorValue | undefined; +} diff --git a/packages/react-native-boost/src/runtime/uniwind.ts b/packages/react-native-boost/src/runtime/uniwind.ts new file mode 100644 index 00000000..736d8b6e --- /dev/null +++ b/packages/react-native-boost/src/runtime/uniwind.ts @@ -0,0 +1,91 @@ +import { jsx } from 'react/jsx-runtime'; +import type { ActivityIndicatorProps, ImageProps, TextProps, ViewProps } from 'react-native'; +import { Platform } from 'react-native'; +import { useStyle } from 'react-native-boost/uniwind/useStyle'; +import { useAccentColor } from 'react-native-boost/uniwind/useAccentColor'; +import { + NativeView as ViewHost, + NativeViewWithContext as ContextViewHost, + NativeText as TextHost, + NativeImage as ImageHost, + NativeActivityIndicator as ActivityHost, + processViewAccessibilityProps, + processTextAccessibilityProps, + processTextStyle, + processSelectionColor, + processImageAccessibilityProps, + processImageSourceProps, + processActivityIndicatorStyle, + processActivityIndicatorSize, +} from './index'; + +export { useAnimatedValue } from 'react-native'; +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore These exports require RN 0.85; the optimizer checks the target version before using them. +export { useAnimatedValueXY, useAnimatedColor } from 'react-native'; + +type ClassProps = { className?: string }; + +export function NativeView(props: ViewProps & ClassProps) { + const style = useStyle(props.className, props); + const { id, nativeID, ...rest } = processViewAccessibilityProps(props); + return jsx(ViewHost, { ...rest, nativeID: id ?? nativeID, style: [style, props.style] }); +} + +export function NativeViewWithContext(props: ViewProps & ClassProps) { + const style = useStyle(props.className, props); + const { id, nativeID, ...rest } = processViewAccessibilityProps(props); + return jsx(ContextViewHost, { ...rest, nativeID: id ?? nativeID, style: [style, props.style] }); +} + +export function NativeText(props: TextProps & ClassProps & { selectionColorClassName?: string }) { + // Eligible Text has no press handlers, so React Native never starts pressability. + const state = { isPressed: false, isDisabled: Boolean(props.disabled) }; + const style = useStyle(props.className, props, state); + const accent = useAccentColor(props.selectionColorClassName, props, state); + const { id, nativeID, selectionColor, ...rest } = processTextAccessibilityProps(props); + const lines = style.WebkitLineClamp ?? props.numberOfLines; + return jsx(TextHost, { + ...rest, + ...processTextStyle([style, props.style]), + ...processSelectionColor(selectionColor ?? accent), + nativeID: id ?? nativeID, + allowFontScaling: props.allowFontScaling !== false, + ellipsizeMode: props.ellipsizeMode ?? 'tail', + numberOfLines: lines != null && !(lines >= 0) ? 0 : lines, + }); +} + +export function NativeImage(props: ImageProps & ClassProps & { tintColorClassName?: string }) { + const style = useStyle(props.className, props); + const accent = useAccentColor(props.tintColorClassName, props); + const resolved = { ...props, style: [style, props.style], tintColor: props.tintColor ?? accent }; + const { id, nativeID, ...rest } = processImageAccessibilityProps(resolved); + return jsx(ImageHost, { + ...rest, + ...processImageSourceProps(resolved), + nativeID: id ?? nativeID, + ...(Platform.OS === 'android' + ? { shouldNotifyLoadEvents: Boolean(props.onLoadStart || props.onLoad || props.onLoadEnd || props.onError) } + : {}), + }); +} + +export function NativeActivityIndicator(props: ActivityIndicatorProps & ClassProps & { colorClassName?: string }) { + const generatedStyle = useStyle(props.className, props); + const accent = useAccentColor(props.colorClassName, props); + const { style, color, size, animating = true, hidesWhenStopped = true, onLayout, ...rest } = props; + const resolvedColor = color ?? accent; + return jsx(ViewHost, { + style: processActivityIndicatorStyle([generatedStyle, style]), + onLayout, + children: jsx(ActivityHost, { + ...rest, + animating, + hidesWhenStopped, + color: resolvedColor === undefined ? (Platform.OS === 'ios' ? '#999999' : null) : resolvedColor, + ...processActivityIndicatorSize(size), + ...(Platform.OS === 'android' ? { styleAttr: 'Normal', indeterminate: true } : {}), + }), + }); +} diff --git a/packages/react-native-boost/src/runtime/utils/constants.ts b/packages/react-native-boost/src/runtime/utils/constants.ts index 2e2fc1bd..25f37b14 100644 --- a/packages/react-native-boost/src/runtime/utils/constants.ts +++ b/packages/react-native-boost/src/runtime/utils/constants.ts @@ -22,8 +22,7 @@ export const verticalAlignToTextAlignVerticalMap = { /** * Clamps a `numberOfLines` value exactly as `Text` does at runtime: a negative number (or `NaN`) becomes * `0`, while `null`/`undefined` pass through untouched (`!(value >= 0)` is `true` for negatives and `NaN`, - * `false` otherwise). The plugin emits this only around a non-literal `numberOfLines`; literal negatives - * are clamped at build time, so it never runs for them. + * `false` otherwise). */ -export const clampNumberOfLines = (value: number | null | undefined): number | null | undefined => +export const clampNumberOfLines = (value?: number | null): number | null | undefined => value != null && !(value >= 0) ? 0 : value; diff --git a/packages/react-native-boost/vitest.config.ts b/packages/react-native-boost/vitest.config.mts similarity index 76% rename from packages/react-native-boost/vitest.config.ts rename to packages/react-native-boost/vitest.config.mts index 67bfb6e6..8c527b64 100644 --- a/packages/react-native-boost/vitest.config.ts +++ b/packages/react-native-boost/vitest.config.mts @@ -9,20 +9,18 @@ export default defineConfig({ test: { projects: [ { - // Unit suite: aliases `react-native` to a lightweight mock for the whole project. resolve: { alias: [{ find: /^react-native$/, replacement: resolve(runtimeMockPath) }], }, test: { name: 'unit', - // babel-plugin-tester requires `it` and `describe` to be set globally globals: true, // The differential parity suite needs the REAL react-native and runs under its own config. exclude: [...configDefaults.exclude, '**/__tests__/parity/**'], }, }, - // Parity suite: dedicated config (real RN transform + redirects + react dedup). parityConfig, + fileURLToPath(new URL('src/plugin/__tests__/parity/uniwind/vitest.config.mts', import.meta.url)), ], }, }); diff --git a/packages/react-native-time-to-render/package.json b/packages/react-native-time-to-render/package.json index 9b92e6ae..0b4956aa 100644 --- a/packages/react-native-time-to-render/package.json +++ b/packages/react-native-time-to-render/package.json @@ -4,6 +4,9 @@ "description": "Benchmarks the time to render", "private": true, "main": "src/index", + "peerDependencies": { + "react-native": ">=0.83.0" + }, "codegenConfig": { "name": "RNTimeToRenderSpec", "type": "all", diff --git a/packages/react-native-time-to-render/src/NativeTimeToRender.ts b/packages/react-native-time-to-render/src/NativeTimeToRender.ts index ef233f27..8cc0f02e 100644 --- a/packages/react-native-time-to-render/src/NativeTimeToRender.ts +++ b/packages/react-native-time-to-render/src/NativeTimeToRender.ts @@ -1,3 +1,4 @@ +// React Native Codegen requires the Native prefix. import type { TurboModule } from 'react-native'; import { TurboModuleRegistry } from 'react-native'; diff --git a/packages/react-native-time-to-render/src/TimeToRenderNativeComponent.ts b/packages/react-native-time-to-render/src/TimeToRenderNativeComponent.ts index abe5f5d9..d4e69e12 100644 --- a/packages/react-native-time-to-render/src/TimeToRenderNativeComponent.ts +++ b/packages/react-native-time-to-render/src/TimeToRenderNativeComponent.ts @@ -1,3 +1,4 @@ +// React Native Codegen requires the NativeComponent suffix. import { codegenNativeComponent, type ViewProps } from 'react-native'; import { DirectEventHandler, Double } from 'react-native/Libraries/Types/CodegenTypes'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1831544b..66481ff1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,3 +1,104 @@ +--- +lockfileVersion: '9.0' + +importers: + + .: + configDependencies: {} + packageManagerDependencies: + pnpm: + specifier: 12.3.1 + version: 12.3.1 + +packages: + + '@pnpm/exe.darwin-arm64@12.3.1': + resolution: {integrity: sha512-Yer+aZnQtgE+OOLKwbRHaAEt74WBJdH8eXpLrSWf+5vj7DkkDvhSR45HVxN3a5d430fMUyF0lmQai02T650r4g==} + cpu: [arm64] + os: [darwin] + + '@pnpm/exe.darwin-x64@12.3.1': + resolution: {integrity: sha512-XPZYf+XpxufwZS8tpQQdftsv6eUtPDA0uU5NlfXA1uMVPvXJesw5PLGWmmJZHcI4rIscn2H6FngkIcvkuZaaKQ==} + cpu: [x64] + os: [darwin] + + '@pnpm/exe.linux-arm64-musl@12.3.1': + resolution: {integrity: sha512-WjKcLeuierWGSQHjb0QeDl8avQTel8rN5jDMCI55tBJTrTBXNQsdlpgzP9uCD0GSzjPH+hjWKb+GjeMNvv5Ruw==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@pnpm/exe.linux-arm64@12.3.1': + resolution: {integrity: sha512-X0HxHlEYubFRuMPBOy+ytKsv6c11v4em/ovVVCy5KCdJVBtVGF7vF5ywlGqw7EjpGdqM39pdLTWFwH9Q77lUzQ==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@pnpm/exe.linux-x64-musl@12.3.1': + resolution: {integrity: sha512-ZvzQI2+Ek0v+V6m30XV3ShIx5sm+ZIZoM669Z0F/RvMSpHgklqv3CBdEwAsQCZ7XBNZLMQIE7RPR2yIwxt3mnw==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@pnpm/exe.linux-x64@12.3.1': + resolution: {integrity: sha512-ubvbQ2OSt7fR3kSnhtknx5xnm2H7uRi0kC32ADmzKJ1vZz337kmL30rTHoUzTG7ZcIyEhODg7R+zI1cW1ZdVDw==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@pnpm/exe.win32-arm64@12.3.1': + resolution: {integrity: sha512-e+5CjFBGWP7U7RfFlH/EQnlFaP9Uyo/Y0BDCEbitsC4if28WBur3ajAkc25rRwiEwmmEpipTSzqNsKIs9Mq72Q==} + cpu: [arm64] + os: [win32] + + '@pnpm/exe.win32-x64@12.3.1': + resolution: {integrity: sha512-5pW8NQuVF3dkNdaUCm03PeoDiC5I9h4y9Fz717KLWi5jxDKmgARmD1zDdm60F5ohRZHPrRv/jAg94a/5+VXxow==} + cpu: [x64] + os: [win32] + + pnpm@12.3.1: + resolution: {integrity: sha512-PBTBVAjRSJ01D47X+TNh0mzHBwVPaEmk7qO7dAf/T+RWS0E6HEIadzoXarEWio3xx9VI8s0Nx9NE9YxOaApbGA==} + engines: {node: '>=18.*'} + hasBin: true + +snapshots: + + '@pnpm/exe.darwin-arm64@12.3.1': + optional: true + + '@pnpm/exe.darwin-x64@12.3.1': + optional: true + + '@pnpm/exe.linux-arm64-musl@12.3.1': + optional: true + + '@pnpm/exe.linux-arm64@12.3.1': + optional: true + + '@pnpm/exe.linux-x64-musl@12.3.1': + optional: true + + '@pnpm/exe.linux-x64@12.3.1': + optional: true + + '@pnpm/exe.win32-arm64@12.3.1': + optional: true + + '@pnpm/exe.win32-x64@12.3.1': + optional: true + + pnpm@12.3.1: + optionalDependencies: + '@pnpm/exe.darwin-arm64': 12.3.1 + '@pnpm/exe.darwin-x64': 12.3.1 + '@pnpm/exe.linux-arm64': 12.3.1 + '@pnpm/exe.linux-arm64-musl': 12.3.1 + '@pnpm/exe.linux-x64': 12.3.1 + '@pnpm/exe.linux-x64-musl': 12.3.1 + '@pnpm/exe.win32-arm64': 12.3.1 + '@pnpm/exe.win32-x64': 12.3.1 + +--- lockfileVersion: '9.0' settings: @@ -10,7 +111,7 @@ importers: devDependencies: '@commitlint/cli': specifier: ^21.2.2 - version: 21.2.2(@types/node@24.13.3)(conventional-commits-filter@6.0.1)(conventional-commits-parser@7.1.2)(typescript@6.0.3) + version: 21.2.2(@types/node@26.4.1)(conventional-commits-filter@6.0.1)(conventional-commits-parser@7.1.2)(typescript@7.0.2) '@commitlint/config-conventional': specifier: ^21.2.2 version: 21.2.2 @@ -21,47 +122,47 @@ importers: specifier: ^17.4.1 version: 17.4.1 oxfmt: - specifier: ^0.65.0 - version: 0.65.0 + specifier: ^0.66.0 + version: 0.66.0 oxlint: - specifier: ^1.80.0 - version: 1.80.0 + specifier: ^1.81.0 + version: 1.81.0 typescript: - specifier: ~6.0.3 - version: 6.0.3 + specifier: ~7.0.2 + version: 7.0.2 apps/docs: dependencies: fumadocs-core: - specifier: 16.15.4 - version: 16.15.4(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.38.0(react@19.2.3))(next@16.3.3(@types/node@24.13.3)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.5.4) + specifier: 16.15.5 + version: 16.15.5(@mdx-js/mdx@3.1.1(supports-color@8.1.1))(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.18)(lucide-react@1.39.0(react@19.2.8))(next@16.3.4(@babel/core@8.0.1)(@types/node@26.4.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(zod@4.5.4) fumadocs-mdx: specifier: 15.4.0 - version: 15.4.0(@types/mdast@4.0.4)(@types/mdx@2.0.14)(@types/react@19.2.17)(fumadocs-core@16.15.4(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.38.0(react@19.2.3))(next@16.3.3(@types/node@24.13.3)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.5.4))(next@16.3.3(@types/node@24.13.3)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(rolldown@1.1.5)(vite@8.1.4(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.49.0)(tsx@4.23.13)(yaml@2.9.0)) + version: 15.4.0(@types/mdast@4.0.4)(@types/mdx@2.0.14)(@types/react@19.2.18)(fumadocs-core@16.15.5(@mdx-js/mdx@3.1.1(supports-color@8.1.1))(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.18)(lucide-react@1.39.0(react@19.2.8))(next@16.3.4(@babel/core@8.0.1)(@types/node@26.4.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(zod@4.5.4))(next@16.3.4(@babel/core@8.0.1)(@types/node@26.4.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)(rolldown@1.2.7)(supports-color@8.1.1)(vite@8.2.2(@types/node@26.4.1)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.13)(yaml@2.9.0)) fumadocs-typescript: - specifier: ^5.2.7 - version: 5.3.0(6fbffc4d4f66bd8dfc78c084cbb42a44) + specifier: ^5.3.0 + version: 5.3.0(a2d293f82b7f1920866010133281da6b) fumadocs-ui: - specifier: 16.15.4 - version: 16.15.4(@types/mdx@2.0.14)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(fumadocs-core@16.15.4(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.38.0(react@19.2.3))(next@16.3.3(@types/node@24.13.3)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.5.4))(next@16.3.3(@types/node@24.13.3)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tailwindcss@4.3.3) + specifier: 16.15.5 + version: 16.15.5(@types/mdx@2.0.14)(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(fumadocs-core@16.15.5(@mdx-js/mdx@3.1.1(supports-color@8.1.1))(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.18)(lucide-react@1.39.0(react@19.2.8))(next@16.3.4(@babel/core@8.0.1)(@types/node@26.4.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(zod@4.5.4))(next@16.3.4(@babel/core@8.0.1)(@types/node@26.4.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(tailwindcss@4.3.3) lucide-react: - specifier: ^1.38.0 - version: 1.38.0(react@19.2.3) + specifier: ^1.39.0 + version: 1.39.0(react@19.2.8) mermaid: specifier: ^11.17.2 version: 11.17.2 next: - specifier: 16.3.3 - version: 16.3.3(@types/node@24.13.3)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + specifier: 16.3.4 + version: 16.3.4(@babel/core@8.0.1)(@types/node@26.4.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) react: - specifier: 19.2.3 - version: 19.2.3 + specifier: 19.2.8 + version: 19.2.8 react-dom: - specifier: 19.2.3 - version: 19.2.3(react@19.2.3) + specifier: 19.2.8 + version: 19.2.8(react@19.2.8) ts-morph: - specifier: ^27.0.2 - version: 27.0.2 + specifier: ^28.0.0 + version: 28.0.0 devDependencies: '@tailwindcss/postcss': specifier: ^4.3.3 @@ -70,44 +171,44 @@ importers: specifier: ^2.0.14 version: 2.0.14 '@types/node': - specifier: ^24 - version: 24.13.3 + specifier: ^26.4.1 + version: 26.4.1 '@types/react': - specifier: ^19.2.14 - version: 19.2.17 + specifier: ^19.2.18 + version: 19.2.18 '@types/react-dom': - specifier: ^19.2.3 - version: 19.2.3(@types/react@19.2.17) + specifier: ^19.2.0 + version: 19.2.5(@types/react@19.2.18) tailwindcss: specifier: ^4.3.3 version: 4.3.3 typescript: - specifier: ^6.0.3 - version: 6.0.3 + specifier: ^7.0.2 + version: 7.0.2 apps/example: dependencies: '@expo/metro-runtime': - specifier: ~57.0.3 - version: 57.0.3(@expo/log-box@57.0.0)(expo@57.0.4)(react-dom@19.2.3(react@19.2.3))(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3))(react@19.2.3) + specifier: ~57.0.15 + version: 57.0.15(@expo/log-box@57.0.4)(expo@57.0.19)(react-dom@19.2.3(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) '@react-native-community/slider': - specifier: 5.2.0 - version: 5.2.0 + specifier: 5.2.1 + version: 5.2.1 '@react-navigation/native': - specifier: ^7.3.3 - version: 7.3.8(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3))(react@19.2.3) + specifier: ^7.3.18 + version: 7.3.18(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) '@react-navigation/native-stack': - specifier: ^7.17.5 - version: 7.17.10(@react-navigation/native@7.3.8(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3))(react@19.2.3))(react-native-safe-area-context@5.7.0(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3))(react@19.2.3))(react-native-screens@4.25.2(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3))(react@19.2.3))(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3))(react@19.2.3) + specifier: ^7.18.10 + version: 7.18.10(@react-navigation/native@7.3.18(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3))(react-native-safe-area-context@5.7.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3))(react-native-screens@4.26.2(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) expo: - specifier: ^57.0.4 - version: 57.0.4(@babel/core@7.29.7)(@expo/dom-webview@57.0.0)(@expo/metro-runtime@57.0.3)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + specifier: ^57.0.19 + version: 57.0.19(@babel/core@7.29.7(supports-color@8.1.1))(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1)(typescript@7.0.2) expo-splash-screen: - specifier: ~57.0.2 - version: 57.0.2(expo@57.0.4)(typescript@6.0.3) + specifier: ~57.0.8 + version: 57.0.8(expo@57.0.19)(supports-color@8.1.1)(typescript@7.0.2) expo-status-bar: - specifier: ~57.0.0 - version: 57.0.0(expo@57.0.4)(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3))(react@19.2.3) + specifier: ~57.0.1 + version: 57.0.1(expo@57.0.19)(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) react: specifier: 19.2.3 version: 19.2.3 @@ -115,48 +216,51 @@ importers: specifier: 19.2.3 version: 19.2.3(react@19.2.3) react-native: - specifier: 0.86.0 - version: 0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3) + specifier: 0.86.3 + version: 0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) react-native-boost: specifier: workspace:* version: link:../../packages/react-native-boost react-native-nitro-modules: specifier: 0.37.1 - version: 0.37.1(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3))(react@19.2.3) + version: 0.37.1(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) react-native-safe-area-context: specifier: ~5.7.0 - version: 5.7.0(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3))(react@19.2.3) + version: 5.7.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) react-native-screens: - specifier: 4.25.2 - version: 4.25.2(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3))(react@19.2.3) + specifier: 4.26.2 + version: 4.26.2(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) react-native-time-to-render: specifier: workspace:* version: link:../../packages/react-native-time-to-render react-native-unistyles: specifier: 3.3.0 - version: 3.3.0(@react-native/normalize-colors@0.86.0)(react-native-nitro-modules@0.37.1(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3))(react@19.2.3))(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3))(react@19.2.3) + version: 3.3.0(@react-native/normalize-colors@0.87.1)(react-native-nitro-modules@0.37.1(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) react-native-web: specifier: ~0.21.0 version: 0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + uniwind: + specifier: 1.12.0 + version: 1.12.0(@expo/metro-config@57.0.12(expo@57.0.19)(supports-color@8.1.1)(typescript@7.0.2))(metro-cache@0.87.0(supports-color@8.1.1))(metro-transform-worker@0.87.0(supports-color@8.1.1))(metro@0.87.0(supports-color@8.1.1))(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)(tailwindcss@4.3.3) devDependencies: '@babel/core': specifier: ^7.29.7 - version: 7.29.7 + version: 7.29.7(supports-color@8.1.1) '@types/react': - specifier: ~19.2.14 - version: 19.2.17 + specifier: ~19.2.18 + version: 19.2.18 typescript: - specifier: ~6.0.3 - version: 6.0.3 + specifier: ~7.0.2 + version: 7.0.2 packages/react-native-boost: dependencies: '@babel/core': specifier: ^7.29.7 - version: 7.29.7 + version: 7.29.7(supports-color@8.1.1) '@babel/helper-module-imports': specifier: ^7.29.7 - version: 7.29.7 + version: 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': specifier: ^7.29.7 version: 7.29.7 @@ -166,81 +270,115 @@ importers: devDependencies: '@babel/plugin-syntax-jsx': specifier: ^7.29.7 - version: 7.29.7(@babel/core@7.29.7) + version: 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) '@babel/plugin-syntax-typescript': specifier: ^7.29.7 - version: 7.29.7(@babel/core@7.29.7) + version: 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) '@babel/preset-react': specifier: ^7.29.7 - version: 7.29.7(@babel/core@7.29.7) + version: 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) '@react-native/babel-preset': - specifier: 0.86.0 - version: 0.86.0(@babel/core@7.29.7) + specifier: 0.87.1 + version: 0.87.1(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) '@release-it/conventional-changelog': specifier: ^12.0.0 - version: 12.0.0(conventional-commits-filter@6.0.1)(conventional-commits-parser@7.1.2)(release-it@21.0.2(@types/node@24.13.3)) + version: 12.0.0(conventional-commits-filter@6.0.1)(conventional-commits-parser@7.1.2)(release-it@21.0.2(@types/node@26.4.1)(supports-color@8.1.1)) '@rollup/plugin-node-resolve': - specifier: ^16.0.0 + specifier: ^16.0.3 version: 16.0.3(rollup@4.63.1) + '@types/babel__core': + specifier: ^7.20.5 + version: 7.20.5 '@types/babel__helper-module-imports': - specifier: ^7.0.0 + specifier: ^7.18.3 version: 7.18.3 '@types/babel__helper-plugin-utils': - specifier: ^7.0.0 + specifier: ^7.10.3 version: 7.10.3 '@types/node': - specifier: ^24 - version: 24.13.3 + specifier: ^26.4.1 + version: 26.4.1 + '@types/react': + specifier: ^19.2.18 + version: 19.2.18 + '@types/react-dom': + specifier: ^19.2.5 + version: 19.2.5(@types/react@19.2.18) + '@types/react-test-renderer': + specifier: ^19.1.0 + version: 19.1.0 + '@typescript/typescript6': + specifier: ^6.0.2 + version: 6.0.2 + babel-plugin-react-compiler: + specifier: 1.0.0 + version: 1.0.0 babel-plugin-tester: specifier: ^12.0.0 - version: 12.0.0(@babel/core@7.29.7) + version: 12.0.0(@babel/core@7.29.7(supports-color@8.1.1)) fast-check: specifier: ^4.9.0 version: 4.9.0 + metro: + specifier: 0.87.0 + version: 0.87.0(supports-color@8.1.1) + metro-config: + specifier: 0.87.0 + version: 0.87.0(supports-color@8.1.1) react: - specifier: 19.2.3 - version: 19.2.3 + specifier: 19.2.8 + version: 19.2.8 react-dom: - specifier: 19.2.3 - version: 19.2.3(react@19.2.3) + specifier: 19.2.8 + version: 19.2.8(react@19.2.8) react-native: - specifier: 0.86.0 - version: 0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3) + specifier: 0.87.1 + version: 0.87.1(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.8)(supports-color@8.1.1) + react-test-renderer: + specifier: 19.2.8 + version: 19.2.8(react@19.2.8) release-it: specifier: ^21.0.2 - version: 21.0.2(@types/node@24.13.3) + version: 21.0.2(@types/node@26.4.1)(supports-color@8.1.1) rollup: specifier: ^4.63.1 version: 4.63.1 rollup-plugin-dts: specifier: ^6.5.1 - version: 6.5.1(rollup@4.63.1)(typescript@6.0.3) + version: 6.5.1(@typescript/typescript6@6.0.2)(rollup@4.63.1)(typescript@7.0.2) rollup-plugin-esbuild: - specifier: ^6.2.0 - version: 6.2.1(esbuild@0.28.2)(rollup@4.63.1) + specifier: ^6.2.1 + version: 6.2.1(esbuild@0.28.2)(rollup@4.63.1)(supports-color@8.1.1) typescript: - specifier: ~6.0.3 - version: 6.0.3 + specifier: ~7.0.2 + version: 7.0.2 + uniwind: + specifier: 1.12.0 + version: 1.12.0(@expo/metro-config@57.0.12(expo@57.0.19)(supports-color@8.1.1)(typescript@7.0.2))(metro-cache@0.87.0(supports-color@8.1.1))(metro-transform-worker@0.87.0(supports-color@8.1.1))(metro@0.87.0(supports-color@8.1.1))(react-native@0.87.1(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.8)(supports-color@8.1.1))(react@19.2.8)(tailwindcss@4.3.3) vitest: - specifier: ^4.1.11 - version: 4.1.11(@types/node@24.13.3)(vite@8.1.4(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.49.0)(tsx@4.23.13)(yaml@2.9.0)) + specifier: ^5.0.0 + version: 5.0.0(@types/node@26.4.1)(vite@8.2.2(@types/node@26.4.1)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.13)(yaml@2.9.0)) - packages/react-native-time-to-render: {} + packages/react-native-time-to-render: + dependencies: + react-native: + specifier: '>=0.83.0' + version: 0.87.1(@types/react@19.2.18)(react@19.2.8)(supports-color@8.1.1) tooling/benchmark: devDependencies: '@types/node': - specifier: ^24 - version: 24.13.3 + specifier: ^26.4.1 + version: 26.4.1 tsx: specifier: ^4.23.13 version: 4.23.13 typescript: - specifier: ~6.0.3 - version: 6.0.3 + specifier: ~7.0.2 + version: 7.0.2 vitest: - specifier: ^4.1.11 - version: 4.1.11(@types/node@24.13.3)(vite@8.1.4(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.49.0)(tsx@4.23.13)(yaml@2.9.0)) + specifier: ^5.0.0 + version: 5.0.0(@types/node@26.4.1)(vite@8.2.2(@types/node@26.4.1)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.13)(yaml@2.9.0)) packages: @@ -252,8 +390,8 @@ packages: resolution: {integrity: sha512-252CZ4OJRszsbP0n+vQRrhdFyDyEzt4NqYvJ/P2Iha4dw/jtmQvAF0wYqARPDZe7uuUG6Og0uQxMUfXFO2BQ7g==} engines: {node: ^20.18.0 || ^22.12.0 || >=23.3.0} - '@alloc/quick-lru@5.2.0': - resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} + '@alloc/quick-lru@5.3.0': + resolution: {integrity: sha512-U4+70Pc5ZS9osnCBCE5Jha/ciHM+Yp+CNMNC/7HvYbNRk1Ldd+f7qO65W5qfhu/TCv+/ozljlXXe9Nj8419DMA==} engines: {node: '>=10'} '@antfu/install-pkg@1.1.0': @@ -271,14 +409,26 @@ packages: resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==} engines: {node: '>=6.9.0'} + '@babel/compat-data@8.0.0': + resolution: {integrity: sha512-DOjnob/cXOUgDOozCDeq/aK2p5y8dUIVdf6tNhEV1HQRd6I8aQ4f4fbtHRVEvb6lP3BGomrKHiS8ICAASSVQSw==} + engines: {node: ^22.18.0 || >=24.11.0} + '@babel/core@7.29.7': resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==} engines: {node: '>=6.9.0'} - '@babel/generator@7.29.7': - resolution: {integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==} + '@babel/core@8.0.1': + resolution: {integrity: sha512-5FgxM4dLQpMJHSiVATk8foW263dVHQHBVpXYiimNECVWG01f4nFyEbQixeT6Mwvg7TayREJ2gpKl3o2RoMdnqw==} + engines: {node: ^22.18.0 || >=24.11.0} + + '@babel/generator@7.29.8': + resolution: {integrity: sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==} engines: {node: '>=6.9.0'} + '@babel/generator@8.0.0': + resolution: {integrity: sha512-NT9NrVwJsbSV6Y2FSstWa71EETOnzrjkL5/wX3D2mYHtKM+qvqB1DvR4D0Setb/gDBsHzRICifwEWMO8CnTF6g==} + engines: {node: ^22.18.0 || >=24.11.0} + '@babel/helper-annotate-as-pure@7.29.7': resolution: {integrity: sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw==} engines: {node: '>=6.9.0'} @@ -287,6 +437,10 @@ packages: resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==} engines: {node: '>=6.9.0'} + '@babel/helper-compilation-targets@8.0.0': + resolution: {integrity: sha512-JwculLABZvyPvyLBpwU/E/IbH2uM3mnxNtIJpxnIfb24y1PrdVxK5Dqjle4DpgqpGRnwgC7G8IkzPdSXZrO1Ew==} + engines: {node: ^22.18.0 || >=24.11.0} + '@babel/helper-create-class-features-plugin@7.29.7': resolution: {integrity: sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg==} engines: {node: '>=6.9.0'} @@ -308,6 +462,10 @@ packages: resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==} engines: {node: '>=6.9.0'} + '@babel/helper-globals@8.0.0': + resolution: {integrity: sha512-lLozHOM6sWWlxNo8CYqHy4MBZeTvHXNgVPBfPOGsjPKUzHC2Az9QwB6gxdQmpwHl6GlQtbGgS+lj5887guDiLw==} + engines: {node: ^22.18.0 || >=24.11.0} + '@babel/helper-member-expression-to-functions@7.29.7': resolution: {integrity: sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg==} engines: {node: '>=6.9.0'} @@ -350,6 +508,10 @@ packages: resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} engines: {node: '>=6.9.0'} + '@babel/helper-string-parser@8.0.0': + resolution: {integrity: sha512-6mJgmFFFIIO82vvoLt9XtRC7/TkzXfts1t/SpRX4IHSzMgqoPYCWesVu1udUPUWioAE/2fcG6WuI8zrkE1gwrg==} + engines: {node: ^22.18.0 || >=24.11.0} + '@babel/helper-validator-identifier@7.29.7': resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} engines: {node: '>=6.9.0'} @@ -362,6 +524,10 @@ packages: resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==} engines: {node: '>=6.9.0'} + '@babel/helper-validator-option@8.0.0': + resolution: {integrity: sha512-U4Dybxh4WESWHt5XhBeExi4DrY0/DNK1aHpQbsrQXCUbFHuMweT0TpLEWKvaraV2Y6fS+ZXunsZ8zIuZIgvF2Q==} + engines: {node: ^22.18.0 || >=24.11.0} + '@babel/helper-wrap-function@7.29.7': resolution: {integrity: sha512-iES0Skag9ERIF68aXadpO6dbXa03mNWK3sEqJaMnLNs/eC3l0lkImdfoy6Y09/SfkpawdAB4RjQ7PVA7TcVGdw==} engines: {node: '>=6.9.0'} @@ -370,11 +536,20 @@ packages: resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==} engines: {node: '>=6.9.0'} - '@babel/parser@7.29.7': - resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==} + '@babel/helpers@8.0.0': + resolution: {integrity: sha512-wfbi91pM3py96oIiJEz7qIpyXDytgr9zQC1HEWwlGNVRAEmItuU/0a41ZUKu1sJGyhhOIpc4t5vk4PYzt8wpsg==} + engines: {node: ^22.18.0 || >=24.11.0} + + '@babel/parser@7.29.8': + resolution: {integrity: sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==} engines: {node: '>=6.0.0'} hasBin: true + '@babel/parser@8.0.4': + resolution: {integrity: sha512-srpptsAkEbbNIC/q8nT7o+m6CQe8CJUTV/t7MYc9NnWlgYVtHOb7JH6SorxMhN0kuRJjVqXbKClG6xSbPtzz+g==} + engines: {node: ^22.18.0 || >=24.11.0} + hasBin: true + '@babel/plugin-proposal-decorators@7.29.7': resolution: {integrity: sha512-EtU0Hi3GvrTqD56xKmZvV/uCXK2ZbwVNPNLAquVItcAZpUhkXwWlo3Fmj0c2LxgSf2I8IDULeAepwNP1OefLXg==} engines: {node: '>=6.9.0'} @@ -588,8 +763,8 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/plugin-transform-regenerator@7.29.7': - resolution: {integrity: sha512-rNNFV0DBAJp988xW2DOntfDoYn1eR8GGF5AT5vYc+rjyfaQkM242c9tZUHHPe7KYaiJizXPWhQTzzdbXySyhBw==} + '@babel/plugin-transform-regenerator@7.29.8': + resolution: {integrity: sha512-0UpIXPtdDtMXfnV2OJAVMLpj3H/92vmkA6lpSRakmycJvj3VUy6Xs1dM8tXRugupykr5WB+LpiVl0J8LMVg2mg==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 @@ -632,18 +807,30 @@ packages: resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} engines: {node: '>=6.9.0'} - '@babel/traverse@7.29.7': - resolution: {integrity: sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==} + '@babel/template@8.0.0': + resolution: {integrity: sha512-eAD0QW/AlbamBbw0FeGiwasbCVPq5ncW0HNVyLP3B9czqLyh4gvw+5JTSNt6le9+ziAU7mqDZsKTHf3jTb4chQ==} + engines: {node: ^22.18.0 || >=24.11.0} + + '@babel/traverse@7.29.8': + resolution: {integrity: sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==} engines: {node: '>=6.9.0'} + '@babel/traverse@8.0.4': + resolution: {integrity: sha512-bZnmqzGG8UZneG1lLxBoWIH0G6Gr1D846Yu4/3XnY6FhCndMR49u26nTY08u/dAxWmLWF9vGQOuC+84FfIUoeg==} + engines: {node: ^22.18.0 || >=24.11.0} + '@babel/types@7.29.0': resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} engines: {node: '>=6.9.0'} - '@babel/types@7.29.7': - resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} + '@babel/types@7.29.8': + resolution: {integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==} engines: {node: '>=6.9.0'} + '@babel/types@8.0.4': + resolution: {integrity: sha512-eY+Yn3dCqTGmyiq2QRU66lA5FL8lqqqvecHt0fF3uHONIa7ToYsaCiWV8lOKqAs0Rb2SjixiKFROngnulPtt2g==} + engines: {node: ^22.18.0 || >=24.11.0} + '@braintree/sanitize-url@7.1.2': resolution: {integrity: sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==} @@ -735,18 +922,9 @@ packages: resolution: {integrity: sha512-aalGyl7dbB5PArRebDIX43ZvBlXrYm9uWzGJ26t+4SzJVPsOuvfILGGbw5X4yX7i50YEmJ8zvbiWnqH/AAnZqg==} engines: {node: '>=22'} - '@emnapi/core@1.11.1': - resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==} - - '@emnapi/runtime@1.11.1': - resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==} - '@emnapi/runtime@1.11.3': resolution: {integrity: sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==} - '@emnapi/wasi-threads@1.2.2': - resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} - '@esbuild/aix-ppc64@0.28.2': resolution: {integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==} engines: {node: '>=18'} @@ -903,8 +1081,8 @@ packages: cpu: [x64] os: [win32] - '@expo/cli@57.0.6': - resolution: {integrity: sha512-14wEb2e8lctlxGARbinUEr+9EmrTR2jIgcaPBBkZXDFMRJmdoY0ic18JuFJPi+7CuQ4XWiGK6obN4VK2PXosLA==} + '@expo/cli@57.0.21': + resolution: {integrity: sha512-CkcgAOsp1KkNRENXWfyr/esIwgFtiHosKECWZ0QQl2nFOUEQlyjWXAZ6uTCMnbHLonVnPMZpXTkHZNw3gtanFw==} hasBin: true peerDependencies: expo: '*' @@ -919,20 +1097,20 @@ packages: '@expo/code-signing-certificates@0.0.6': resolution: {integrity: sha512-iNe0puxwBNEcuua9gmTGzq+SuMDa0iATai1FlFTMHJ/vUmKvN/V//drXoLJkVb5i5H3iE/n/qIJxyoBnXouD0w==} - '@expo/config-plugins@57.0.3': - resolution: {integrity: sha512-J3P2T7FoOE9P3TuLcJXwt8jISKRxo7UntTzbJ/Qc5F7QXenNntk/4t1XndVkLucYjpnHArPd9xzDXuxxKEHVbQ==} + '@expo/config-plugins@57.0.9': + resolution: {integrity: sha512-hHgfL1avkCdEvDSw7IwlKwRYYNgcxzbNNMIk6W6lTkJpY0MajinAfeJUS0J+wPCsjUfGbVqOJM+XhPaO5ulUxg==} - '@expo/config-types@57.0.1': - resolution: {integrity: sha512-fo7d/Ym28uwGzdTV2leEvpsb9t+7i8YHjy471m31+cmDt4BRd/l7e94JHyrXAq4SWOBVus16S0JLqm89SRCCdw==} + '@expo/config-types@57.0.2': + resolution: {integrity: sha512-ewW08OonrcRIsRKIlFvvcmmafE5zemb1ocu3HkNwtVPyRtj2w42pZCAkMIROYpcVBaPnc3mDT9UZDzwXWC3i6g==} - '@expo/config@57.0.3': - resolution: {integrity: sha512-IshqjjpGtT7wj86pxgsfMD1/abNMfu1wZ07ubyYO29l+PWa0eAh2TcpyLcyBaqo01IonF6646xWwrCPcdlUl3Q==} + '@expo/config@57.0.9': + resolution: {integrity: sha512-dmzlKraIFxa7wLwV6K7WzI8jp6QZpW6Mc5mGjLimJUFjzh4uQdYaT3m3plEutM5yxBoBEwqzks7l+I/ljCbxAQ==} '@expo/devcert@1.2.1': resolution: {integrity: sha512-qC4eaxmKMTmJC2ahwyui6ud8f3W60Ss7pMkpBq40Hu3zyiAaugPXnZ24145U7K36qO9UHdZUVxsCvIpz2RYYCA==} - '@expo/devtools@57.0.0': - resolution: {integrity: sha512-i8ITQmf/wB0JNQ2gASFOKvqo1zRWCl/VfPlFRXdz6UkHen4s31NmMy0zmumS+pMpwn0+gA6oU4FF4zRDRG488Q==} + '@expo/devtools@57.0.1': + resolution: {integrity: sha512-GyUf+wFNkbttaX0jR7MZa9bm77U0IrLg6d2AjpxdyoXw/w4abHoXG0oFufwLMgP9zLTd5+Ct4X/ffNUTnlzZgg==} peerDependencies: react: '*' react-native: '*' @@ -942,59 +1120,59 @@ packages: react-native: optional: true - '@expo/dom-webview@57.0.0': - resolution: {integrity: sha512-zBZw52KnaHf9zGVp1eJk8kNfc+5ArGf+KvTL3SeMsr03K6tPJtLbgycVLjrAiLMfhzqGVjPD2G+rbNwpbLUxcg==} + '@expo/dom-webview@57.0.1': + resolution: {integrity: sha512-lAKsME4SAq+8sf56oN0DX5TBYyruupoRxbWbD2xf9RnKY8y6x8eb9LCE5pxSN0qyWdqnp+0wmyWzDkKboThKAw==} peerDependencies: expo: '*' react: '*' react-native: '*' - '@expo/env@2.4.1': - resolution: {integrity: sha512-3c9Mg9x0HmGPEsVrGAGyEDJsNUOZ55cZvZ47/HLmXh7MHV9Zv7My73wThklKrObaBBoMfE4YqpKjYKDRzojpjQ==} + '@expo/env@2.4.3': + resolution: {integrity: sha512-M1NXeZCA1mkMkYOyIe7PlyRX0/jqFtMoJgyblnlq/vpCRfmueFT7RnGSQG8uEFDF5WHOFGijAQ3fogPh3/n5Ng==} engines: {node: '>=20.12.0'} - '@expo/expo-modules-macros-plugin@0.3.0': - resolution: {integrity: sha512-2tRq8kiIZTVZcI5uggh86HefQ7s++Zk5WkFFomNp4aUqyN5ownHHvj1jPEP9jWXaXjPDmWuf5SUZTGD5G6AKkg==} + '@expo/expo-modules-macros-plugin@0.6.1': + resolution: {integrity: sha512-cpsLZE4rqkc1Y3eZTkxB98jrqY1YXgetmtxFt8q89jBRmk3quRuk1BZo+VcnCSObZardjg99r1k5xijEMONFGA==} - '@expo/fingerprint@0.20.3': - resolution: {integrity: sha512-WDV2hS87C61TsX55w4A0cPnbnN8bqdVHp2MpZsJTvB4Tv4TITgSxWYhRj/xv0pwcZvKHn05MBg6AfHPNj9o5Sw==} + '@expo/fingerprint@0.20.12': + resolution: {integrity: sha512-FIR5fkZYeFaLSowmjgyB6RPKl8AXeE8HuCBHHvyxK8UhOjpPfCAmzT4E7v2yub4qqDafKnfcOFCYxvpUEu+01w==} hasBin: true - '@expo/image-utils@0.11.1': - resolution: {integrity: sha512-0JueH4vdgAZQmhlSYFvTQzt4b4NO5cnByDuApw7bMUIjhwLRnT46Ki3ritMrzJMQaO2lLK2flInZbsZbOuy8nw==} + '@expo/image-utils@0.11.5': + resolution: {integrity: sha512-KPQBTpmpAfy/Vu9y4wPW808/qtZxjYmyJg8cm2QCPAupp+qEWA3b5zmk0ulOwQ9OgeHxuCPgUqWgkwHFo7UsrQ==} - '@expo/inline-modules@0.1.2': - resolution: {integrity: sha512-wc5wH4ND647Ne/6A/ESuelcqOQUEvK8MYjjvbp5wcl59dECk7juh0cFOJjAWkpc4pLTcvqXBSRcUDpGWm5XNWg==} + '@expo/inline-modules@0.1.7': + resolution: {integrity: sha512-Bz/khd1gIJqDkje7t5ejD5e9jFbm4xEJzWSwRKadRo6gruepbw1xJ3Eb+e58OS8fY1ZNQYjzZbspnuph67sN0g==} - '@expo/json-file@11.0.0': - resolution: {integrity: sha512-pHJCETqFL5x5BzNV6cEPwjwuECgGmnl0bNmfHIJ6LM1tlh2eVXi5HEdit3zby/JO/B8Otk5cgcqtJXgvvUat3A==} + '@expo/json-file@11.0.1': + resolution: {integrity: sha512-zxHWj4MKKMAL29ZQSY/Fssx4Thluk40JmuGNaeS078wy/NhlFhnVi+rHHunulE3xJAJ0CM73m8VK2+GkF9eRwQ==} - '@expo/local-build-cache-provider@57.0.2': - resolution: {integrity: sha512-/8/FoYrEBJZPG4wR8kO6REI4VRCPAStD+7wS/Ds6XXddxhmyMP3pvUJz/c3icyAoqpbqeZZkR6E1ptT5Gi5iXg==} + '@expo/local-build-cache-provider@57.0.8': + resolution: {integrity: sha512-SEdE0pAQrr90bRh3MNR0ZuwoIBw390cYdFgbn7Vk0Mtm9EHaBfP7kYj+2hXnXZJgOEm3/JMtfoD3rV7rWA9FGg==} - '@expo/log-box@57.0.0': - resolution: {integrity: sha512-tt9x76IEE8hp2FWTLPfEArrTyD7GCoUe3TpohESy+SPZ/OqkUnSXz40xnUuE0XbE132z8c5CRCfXQLM9DTEknQ==} + '@expo/log-box@57.0.4': + resolution: {integrity: sha512-IxwS9s1L2muj8mj8AQSuiy7u8OFJdc02NRFo2me/Tj6DiaeG5SREqmpBE4rQpR2cadqSg5jl8Qab8Cjie616dg==} peerDependencies: - '@expo/dom-webview': ^57.0.0 + '@expo/dom-webview': ^57.0.1 expo: '*' react: '*' react-native: '*' - '@expo/metro-config@57.0.3': - resolution: {integrity: sha512-k7qcjTe1xDrUW15Ue86WAsJSL0ubiSluBXVNRFoe9snAWhMzBGzdZfl9XiDf4TNUV43T0L1ch+n4GmDqamvfEg==} + '@expo/metro-config@57.0.12': + resolution: {integrity: sha512-S62Lrq35HZqBFD55423pmWb8PjaiR/W02zQC1uECBmw1vTN8WZaFz4TJ0i21EeJzwfebMb9MLxL8JZ102Z6VbA==} peerDependencies: expo: '*' peerDependenciesMeta: expo: optional: true - '@expo/metro-file-map@57.0.0': - resolution: {integrity: sha512-/sxwKQmwpYFYjFT6VYFpgldTwSv53OCx4/UCpLCv9iqQLVQ30hFEJsxGA9Jif8n9pacigK4P5/0ZJ2zMHm6arA==} + '@expo/metro-file-map@57.0.2': + resolution: {integrity: sha512-tb50nSIWwKpRufSkGuivOK0FbUxv1Uwqptb0SzFTk0bkmYmcy3gIwnCbOHTfmrQDVndRhPEiu2SYgh9Z0PCvGg==} - '@expo/metro-runtime@57.0.3': - resolution: {integrity: sha512-FMXIpvPIlAvJlaQmlmP79gNt7KS5DyruKSOkOW1DTkXnJOiXQnzvebqPLOEYH71fbS1emJ3Dq45lDz7+sryVbA==} + '@expo/metro-runtime@57.0.15': + resolution: {integrity: sha512-9RaFkqIzRud6ol9fc5ZKHgNDYeDjQVBdAQC+rjSQ6RiKV0pY+SjDqGmKY5a4M9TaV0qLMCNNrp5R88BqC2xbuQ==} peerDependencies: - '@expo/log-box': ^57.0.0 + '@expo/log-box': ^57.0.4 expo: '*' react: '*' react-dom: '*' @@ -1003,39 +1181,39 @@ packages: react-dom: optional: true - '@expo/metro@56.0.0': - resolution: {integrity: sha512-5gIgQHtEpjjvsjKfVtIv23a98LLRV0/y07PDShEwYSytAMlE3FSF8RHXqtHc1sUJL6dn7hnuIBpIbrLXXuVi0A==} + '@expo/metro@56.0.2': + resolution: {integrity: sha512-Ld5AeYMCCDa8bLeWhfuLbZFFjlV3f6ORqyPz2glGh6RltIngMuLf9BTC2yvHFjkKuGxL5SynijmA8xmNNWn5iA==} - '@expo/osascript@2.7.0': - resolution: {integrity: sha512-wKIXL8UtbuX4KwavPasIW3CUcgTbYfjzLcgUhjyKUAYDEqMaf6gmU1bqz3ffBPTokmX+G8/vFG1ZuI9etQWukA==} + '@expo/osascript@2.7.1': + resolution: {integrity: sha512-Zn03EX6In7ts2lPUW2ESUSkEhEWQN1qqsiXjadtZMJOuZRkMiAg1ZQHuvz9DjByDWNJ2pBwAGyrts9lj9k389g==} engines: {node: '>=12'} - '@expo/package-manager@1.13.0': - resolution: {integrity: sha512-s3W3eZafJDEyVL7W/jxj2Nz3eONKxSCU604S5xj8ijrVaRz83x0DnZznLf/UXQEI1w+FyibH68nHeQyk767b1A==} + '@expo/package-manager@1.13.1': + resolution: {integrity: sha512-y/K+CaYYpZpNGZhSX4HyLT/vyIunFjNfyoxNysPBCefeLKI/VCx6f9LNPzrxayr3rCYO5bl9O8H+HRQK265Nkg==} - '@expo/plist@0.8.0': - resolution: {integrity: sha512-24JlUJI4PwHN4PLydlzFEzCdiqybfaV5t04QBkOg8em3AjvHKbMgBGlKreiuOoc0rNa3DZ21ZqL+xLGMBLQNKQ==} + '@expo/plist@0.8.1': + resolution: {integrity: sha512-3gTReGIUm0oRaMClsAJYxBnVPCl6fVpsl8HS+DTVxDhW4GyVyxg9E/Znm3BvcHtUJ51RJJI14pC1wvrNilCRHw==} - '@expo/prebuild-config@57.0.5': - resolution: {integrity: sha512-KnKx6Iu4jppbNVtt9yTrN10OVoPZlOYYyLZuarY70u6fgUO9lYKDa06Hxv+PmIwcnygfj2CSBBl5ewBqS2bxtw==} + '@expo/prebuild-config@57.0.15': + resolution: {integrity: sha512-xTbWHroj0PDmlbqvmU+zF9ZZxveJkiuyiPoeRJYRGruFHebRAWnoTdw5S7d/UCzDBI8ropGGu9g2eb2nMxtvAw==} - '@expo/require-utils@57.0.1': - resolution: {integrity: sha512-uXen5/4x7j60I5slShgZr5QEtJDBK8homFiNLDnDrNrxZhrRHXASo0H6JArs3/1PDzw1wahzhGWg2WKuYyZd0A==} + '@expo/require-utils@57.0.5': + resolution: {integrity: sha512-kTAXj9lDFEIPMsbAOGCGbjBbMF0oi7CqkYM79KOX0DDD9wSwXmlKL1z2h8OwsrBf7mbOo2DjlRvZu4BEjrIxGw==} peerDependencies: - typescript: ^5.0.0 || ^5.0.0-0 || ^6.0.0 + typescript: ^5.0.0 || ^5.0.0-0 || ^6.0.0 || ^7.0.0 peerDependenciesMeta: typescript: optional: true - '@expo/router-server@57.0.2': - resolution: {integrity: sha512-hu9qhnq2PKuwMXIoRMAa2+HUeqSut9HwiQf3a62tfGTIxPN3rKUZ10TozEEB+Pd/7WIa238sXMS5v1YhTuC9gA==} + '@expo/router-server@57.0.9': + resolution: {integrity: sha512-/PxRQozFesIyCJZOAtrQE8XcmcojNiL5ctPMQnbE4ojC2EJPu0zc7c0Y4PuXsoRxrZxm8Usw76/lCVrXcfTZ2w==} peerDependencies: - '@expo/metro-runtime': ^57.0.3 + '@expo/metro-runtime': ^57.0.15 expo: '*' - expo-constants: ^57.0.3 - expo-font: ^57.0.0 + expo-constants: ^57.0.17 + expo-font: ^57.0.3 expo-router: '*' - expo-server: ^57.0.0 + expo-server: ^57.0.3 react: '*' react-dom: '*' react-server-dom-webpack: ~19.0.1 || ~19.1.2 || ~19.2.1 @@ -1049,8 +1227,8 @@ packages: react-server-dom-webpack: optional: true - '@expo/schema-utils@57.0.1': - resolution: {integrity: sha512-IVdaqtP3+iHFRE/y22mKijQtZanLcB18q1zi2kfN6RINTCryyzM7qHGsWHc5LBJbOuj1G4avCOECs97hOJm0GQ==} + '@expo/schema-utils@57.0.2': + resolution: {integrity: sha512-fMu/jyN0l1Wzv7XkeWR4IYCx1M8ryui3FdBNGrWwbRgJ7EhxXxK8E2jxP2W3pbgUwUY0V3hG8+GyfCZwny+Lxw==} '@expo/sdk-runtime-versions@1.0.0': resolution: {integrity: sha512-Doz2bfiPndXYFPMRwPyGa1k5QaKDVpY806UJj570epIiMzWaYyCtobasyfC++qfIXVb5Ocy7r3tP9d62hAQ7IQ==} @@ -1275,12 +1453,12 @@ packages: cpu: [x64] os: [win32] - '@inquirer/ansi@2.0.7': - resolution: {integrity: sha512-3eTuUO1vH2cZm2ZKHeQxnOqlTi9EfZDGgIe3BL3I4u+rJHocr9Fz86M4fjYABPvFnQG/gGK551HqDiIcETwU6Q==} + '@inquirer/ansi@2.0.8': + resolution: {integrity: sha512-WpQM+Ti6Z40EFwwt+uL2p4UabT+W179zHp6HhLVOzfbwnVn05IPO/eXIZXGNqcT1jbQ15SujNLzQ39k4QPPxBQ==} engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} - '@inquirer/checkbox@5.2.3': - resolution: {integrity: sha512-XEYX2WA8SBkLPczL6/yXPHLPCvDoptmh9v56Cy05BSV1Smk1vWy19bTC4qJBuIffw7+6l4CcaYYzGqG60RfW1g==} + '@inquirer/checkbox@5.2.4': + resolution: {integrity: sha512-oKT5RNeO9oKYizSyOxKb66IrKUsYVMQD2tnjllYW4wmSOe9WawfL3OE2p82JuvAyo+/nDIEem5P4Bm/ZIVYVew==} engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} peerDependencies: '@types/node': '>=18' @@ -1288,8 +1466,8 @@ packages: '@types/node': optional: true - '@inquirer/confirm@6.3.0': - resolution: {integrity: sha512-pZHXJImFtERmSNMBHcjwuz8Ck5vEFEYNUZnwbb8aJpjHv/TwGuFErNxF2Hp8+V+pNJs2EYPMlyWscvFEqO9jOQ==} + '@inquirer/confirm@6.3.1': + resolution: {integrity: sha512-HvnOHal39DTenOVoRpIy8Z+n4YYfNa3Qhi/P8zFqn9/d1crpmQG0DPx34rwOeOFvotgKr9Vov/14OmKR/Iwfjw==} engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} peerDependencies: '@types/node': '>=18' @@ -1297,8 +1475,8 @@ packages: '@types/node': optional: true - '@inquirer/core@12.0.1': - resolution: {integrity: sha512-JMD5Jy/ScL5TZE18m83Nw25HjqGFLoWXwnEkW7IdwwAhZpB9Bus55/WU7zn3UqR1MOCjjTQOIYpiD4vjWA3LPw==} + '@inquirer/core@12.0.2': + resolution: {integrity: sha512-9pBhkxE14uUlnHhs8lOt7qVPtS4caRY6CjADl8COejl9Mf7w8i6Uoe3DrljCqYtmYM3Iv2Q6EmPxx/oTYbvTAQ==} engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} peerDependencies: '@types/node': '>=18' @@ -1306,8 +1484,8 @@ packages: '@types/node': optional: true - '@inquirer/editor@5.3.1': - resolution: {integrity: sha512-y43COoyVUjPWIobn2Qep/uI1drPS78aaZZZ9kVi94Tyu/GuW2N8d8Q4rifJXGAXCEAXCPTTMjD8gC1HyvM5ukA==} + '@inquirer/editor@5.3.2': + resolution: {integrity: sha512-7ruKO5d/wxhGHxdCQ3eyP/aBNrPTZ+Ju7ERtnAwuIsu0RSLs6kotlTRkOxHatbD6sDaEEdwsD82pzFiT9ESIyQ==} engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} peerDependencies: '@types/node': '>=18' @@ -1315,8 +1493,8 @@ packages: '@types/node': optional: true - '@inquirer/expand@5.1.3': - resolution: {integrity: sha512-3NQJiXNJ/aj9wiAsr7pECdp5Qe9J0X9YUJCKsaFXS+ddOxfL6J4AIl3w3T4Gq3kK0WsQY5GMoDokK5X94m6lHw==} + '@inquirer/expand@5.1.4': + resolution: {integrity: sha512-12o4aYnjWouoq0fyVhYIL+hwXAaga+kntA/wrijFmVvn2heinkdvkbEXRt1Ob/9IOJ285US5tntjfI/Ukt2YkA==} engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} peerDependencies: '@types/node': '>=18' @@ -1324,8 +1502,8 @@ packages: '@types/node': optional: true - '@inquirer/external-editor@3.0.4': - resolution: {integrity: sha512-tZbbaK2ovq6vlrRBNQvjrypmrED/p5x2ncIHQ79cD55tei3dD96v5glMMA+6tiq7K104i/25DVYKWVPJuV6ptA==} + '@inquirer/external-editor@3.0.5': + resolution: {integrity: sha512-f3QQJRIX5ZEneBHNUIuPjmbdzHnmRFJA8r2dkcb8q+OM5Uv5KtnuAttQumnrjcBVBM3mcTX1CkmtAkU58VRZxg==} engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} peerDependencies: '@types/node': '>=18' @@ -1333,12 +1511,12 @@ packages: '@types/node': optional: true - '@inquirer/figures@2.0.8': - resolution: {integrity: sha512-tApbon79GM9ry56ja/Ud3SY2CL4TQsao9fIwDQbgTeNY55025GdMzQ2+UdegV/lx51VNGUB59M0v0nMpybYY4Q==} + '@inquirer/figures@2.0.9': + resolution: {integrity: sha512-EAWgUTGQ/Umgga51dE3B2PUHbufuXarDfg86uVgoSgNHNNQnyFKcOrQLWVqYMghuSyHh8+2HUH0Js9cTC1WAdg==} engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} - '@inquirer/input@5.1.4': - resolution: {integrity: sha512-3xQkQrOvgOzpSN2ciTVdRDlg1FWMCA8l+0KfB6SNlILoTCGzJTzO/gc0Rwjcb3usuGyKdaGtI6OiyMdeMeLWkg==} + '@inquirer/input@5.1.5': + resolution: {integrity: sha512-2MkYYFD1Owt1eRBwHhsLSutysRodWasA4DVpl42KJKGmzwBTXRYAIXujisrt0UW0UohUlMjNjLfMDbw4emRbfw==} engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} peerDependencies: '@types/node': '>=18' @@ -1346,8 +1524,8 @@ packages: '@types/node': optional: true - '@inquirer/number@4.2.1': - resolution: {integrity: sha512-5KaqwZNLRpUuWcoCrYghPP9TMaXL5v2Sk4xqePM7RCVegcJStoXdWibio60YIC1bec+z1fCyb67N6XPJIkZtGA==} + '@inquirer/number@4.2.2': + resolution: {integrity: sha512-tjW1gMIQsFb/9Ie11i8SpuPARBdyCVfC5XOeVugWovrdm8CpjzN+A1CBX5QwCCN2mtNmpgDDCnuLCCT94cQ5zA==} engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} peerDependencies: '@types/node': '>=18' @@ -1355,8 +1533,8 @@ packages: '@types/node': optional: true - '@inquirer/password@5.2.0': - resolution: {integrity: sha512-CvVcW09emkBESEOW+4R8CjLNkP3fB3XrjeL8CDvfpjgrJN+V9oerXmJAXXM3l+4xqYPD5Yaujzy/Ph0PLOdDuA==} + '@inquirer/password@5.2.1': + resolution: {integrity: sha512-InA6nJxMCXPavPBiTKYHIsYiWz/mkIsk1rkI6pKp5w18gVawqrZOj7eSzSmPJa3j0pVxXm6RlrFP1BE9YeOixA==} engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} peerDependencies: '@types/node': '>=18' @@ -1373,8 +1551,8 @@ packages: '@types/node': optional: true - '@inquirer/rawlist@5.3.3': - resolution: {integrity: sha512-Mu7WrtmDLaXBDEyrRLS70SZgX9ZSm4Up1w0ZxiH8C1OOp9oaVCn2k8q3QGgmlnhsKYUhuaU3zFWhAP6wxkVIMA==} + '@inquirer/rawlist@5.3.4': + resolution: {integrity: sha512-c4fDwOpQsjJDHjXdHJE1/xXH6w9/BAiyu3QKzH9Ww5Xm1q37mzyUNa20L7qdtM1ECi4e+oUEKjhK8hR37OMAMQ==} engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} peerDependencies: '@types/node': '>=18' @@ -1382,8 +1560,8 @@ packages: '@types/node': optional: true - '@inquirer/search@4.3.1': - resolution: {integrity: sha512-0VWOvsHWI0rPj6CG70MoP4oXNCB6adcyN8bVFZXnh11eLDdPIK2f2XCmva78acPPDfJisfab7qNakwBh7hdBXw==} + '@inquirer/search@4.3.2': + resolution: {integrity: sha512-YIaEhWmfkdHEWOKwhF/oJar35FFTTrAdzNbJDo9lSsRT6E/obRPx9oVm4rkTMtlcJSqGYttNtLbcHQef9Qw2yw==} engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} peerDependencies: '@types/node': '>=18' @@ -1391,8 +1569,8 @@ packages: '@types/node': optional: true - '@inquirer/select@5.2.3': - resolution: {integrity: sha512-KuRTodDa6xBXX2noIpjuitpX/QT7Sfav7dIZ/OfUY54Hxg95nrGoshSzxx6Ey7qqLbImKdiGkSDt7KjPXjQgmA==} + '@inquirer/select@5.2.4': + resolution: {integrity: sha512-L9ubNaJdoBsiom/AfPKq0QLHTkhhCFCtszzdVidGremrlJjPZfGlmvp+IULKIbnYjtVFWnvb4mhiEdNTZ12ugA==} engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} peerDependencies: '@types/node': '>=18' @@ -1400,8 +1578,8 @@ packages: '@types/node': optional: true - '@inquirer/type@4.1.0': - resolution: {integrity: sha512-FMiJpuHUG3Dk0ex+UIXkre7i+i4OcwHWk9YdcVtZHFwb/r2rnrU2ipTCNAB7A+QOP0ryzIcqOfy76fRyyvOEAw==} + '@inquirer/type@4.1.1': + resolution: {integrity: sha512-yJoHYrMnxIsJZCY+0Vb66Dy3he3kL3e2wOBKhoSwWWAzZAY82emlxwgprCtp6yRixvNRNq9ztfRWQYPNr3Go7A==} engines: {node: '>=23.5.0 || ^22.13.0 || ^20.17.0'} peerDependencies: '@types/node': '>=18' @@ -1417,8 +1595,8 @@ packages: resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - '@jest/schemas@30.4.1': - resolution: {integrity: sha512-i6b4qw5qnP8c5FEeBJg/uZQ4ddrkN6Ca8qISJh0pr7a5hfn3h3v5x60BEbOC7OYAGZNMs1LfFLwnW2CuK8F57Q==} + '@jest/schemas@30.5.0': + resolution: {integrity: sha512-/hunigyNpc4RCjC0VaW3f5RCUZVM2+WQ65qP7z083Gmvac7or2LI50XVNOtE4YPgBpV0yxYiAgorAPGniCoJmg==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} '@jest/types@29.6.3': @@ -1457,64 +1635,57 @@ packages: os: [linux] libc: [glibc] - '@napi-rs/wasm-runtime@1.2.3': - resolution: {integrity: sha512-UMduMbqO5s5zF2NkNacMT/yK5Y5QiKvWr2+50bzIIxFDwVJ2h49b+oyjaCGPhJxd2/gC2x39EHv/gHVuu36x2Q==} - engines: {node: ^20.19.0 || ^22.13.0 || >=23.5.0} - peerDependencies: - '@emnapi/core': ^1.7.1 || ^2.0.0-alpha.4 - '@emnapi/runtime': ^1.7.1 || ^2.0.0-alpha.4 - - '@next/env@16.3.3': - resolution: {integrity: sha512-U2eYQRwXj+dsqxV79zFqExDdatnNY/ZWc2nsJU1p/OgT7fd3dXwlF6OjYaFQCfMoeTA19PWq+wVmYgimVA+V+g==} + '@next/env@16.3.4': + resolution: {integrity: sha512-cjWZnUUa6jZq2kFaNe/ZyJdZonOZ/QoN0Zka2nz/FLOrfx14pQuM9c5RaSVkWMqgdt4ksgPAMWPyHSs/CyV48Q==} - '@next/swc-darwin-arm64@16.3.3': - resolution: {integrity: sha512-8Hiv32QJPwdV6KYJ8meR9SBA061tQqnIKTJDocvOXlEQqib0xMFpzArosuffFUUc0sslbh7QQ8a3Yey1QV8EIw==} + '@next/swc-darwin-arm64@16.3.4': + resolution: {integrity: sha512-iBr3I5LZNk5/bgl5//iTgD2tcym14MX0Xo7fD//u9dYAEgGzza1y9oywluPtf74YnOswVdH1908aK9xVz7zQTw==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@next/swc-darwin-x64@16.3.3': - resolution: {integrity: sha512-A1lgKgwVchRYmSe467zdwhxT9040dd8lH+o65sL5Jet8fjB4kegw/rDyPIpYVRb6jAqwXFOJpjIXJLxQKLiE3A==} + '@next/swc-darwin-x64@16.3.4': + resolution: {integrity: sha512-2dpiSyl2Jw/NrBPaU2MAKGSa+2MR82pJIn4Sm5Rjr+gxAeuh0z158Su3Z2O8zn7UNNq+ej4bToed6RcRN/Lydg==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@next/swc-linux-arm64-gnu@16.3.3': - resolution: {integrity: sha512-bf0FIssMFueU2dm7vQEWWxk0c8UjKTdW0yzuh0sQsD8pf1+KCLDdaqhYZNMYGmXwEOiHAUzgBKudovIlcvvBjg==} + '@next/swc-linux-arm64-gnu@16.3.4': + resolution: {integrity: sha512-+t+U8HZT+fApePCS5h89CSH3datz29MkzyfCn+6fpsZBG/oiEOhINcb9rtkv6sdpToLGFn2e6146NzaKCXkqrA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [glibc] - '@next/swc-linux-arm64-musl@16.3.3': - resolution: {integrity: sha512-W7viwCk9JY/cAkdz/A273rd5bb3RgT/IHwR7Upv90tunjBWNtAAhGhoecHh+teRNRSinuAFmE+l7fwZ4YKkrXg==} + '@next/swc-linux-arm64-musl@16.3.4': + resolution: {integrity: sha512-mx03GNs1ocQA5JQ4FxDMmIsNkdrZh8cuezKCrId28e5/gIPU/l7Kcy2+vmCCzdjnnmXJy+iOAu+7K0QppO6Urg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [musl] - '@next/swc-linux-x64-gnu@16.3.3': - resolution: {integrity: sha512-0W46zw1N3ODpI6n0GeivHvvob1pooozgZVqy65k0mh4/7vr+FbY9+WpHzNVXjHipJf/A3FDheBG19H1s5A25rA==} + '@next/swc-linux-x64-gnu@16.3.4': + resolution: {integrity: sha512-YIhGY6fSMfha52bnVxnzc9zaVBzJg+cqQTOD8tXIBSx4fuv0pVMxQTE0PaS59YhnMOiYiG09IMwxJAf/CFm/Dw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [glibc] - '@next/swc-linux-x64-musl@16.3.3': - resolution: {integrity: sha512-H4mBso8ZTMBPtdT0PN0pBx2ayTvQuTuvS6qT13d77yVFJXAPCxkyIhLTmdMaGTJs0krQYI/qpzdHijCeihXhbg==} + '@next/swc-linux-x64-musl@16.3.4': + resolution: {integrity: sha512-+eaaX6axpDb0yF1GCpiERe6njplvdC+nks/fKfcHu3XPGRrald8P3/X7yv7QLdjA51knnxwl9pxdIJsg+w1L+Q==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [musl] - '@next/swc-win32-arm64-msvc@16.3.3': - resolution: {integrity: sha512-cTMUJpcEGmeywofCUfhR+rSsoE33+rVPnPEYNTNdLNlsOeEg/vktOsKUSTb28vUGqD2jkm4Zaskcwn7OCI6FQg==} + '@next/swc-win32-arm64-msvc@16.3.4': + resolution: {integrity: sha512-0jcXW7Xs/uzICrmgV3MhDYDeRy++1CqnpDIerlPIqYO4bhzB4WNbX/aRnQclustsAyTkFKB0z6rbcjmNg5tR8A==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@next/swc-win32-x64-msvc@16.3.3': - resolution: {integrity: sha512-2VR4cTBzHXaBjnGsuH6GyJjENzQOmHeAh11uY1iUhjm3j5dEUrVJuUj+VL78jaGi/Dik8xS76zEj18BsFhlVZQ==} + '@next/swc-win32-x64-msvc@16.3.4': + resolution: {integrity: sha512-vvBzwu1pYQCp92maZCFCIw/XgOTMR5tur9GjakwIo2cmwRTMKajRZZDS9+e4KsUZWKu1E007WUeAFXRRjZeuzw==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -1577,249 +1748,249 @@ packages: '@octokit/types@18.0.0': resolution: {integrity: sha512-l6bAF43PNxkJp6g+W4PjoUSSkxHomXw2nOum5CTftJz1NlV3vu93NImgOYtLf6CbBUb5j+fiuzW0PPQ5JTSvZA==} - '@oxc-project/types@0.139.0': - resolution: {integrity: sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==} + '@oxc-project/types@0.148.0': + resolution: {integrity: sha512-Nm4s/jB+4FpFsPhWGEC4h7rzksesmtnMXomo6rCMcg/b8zLQuOziRgkCS1fxDCXOlJB/6Q8oABOZ/OP6RIPj9A==} - '@oxfmt/binding-android-arm-eabi@0.65.0': - resolution: {integrity: sha512-M10Gs1SSpTNI6ahGx3M/OlIdUF4hkaP6OgUb+MS79t/Pgflk3r1nW5gPFqsZGUAXg0H1AfANT9AvLdBSTIhZKg==} + '@oxfmt/binding-android-arm-eabi@0.66.0': + resolution: {integrity: sha512-2Me9eoptv6ERdEuI2P8AOlYdHHraXebJaM6SC0kc2Dfb+mLrep2db+fedBPKaYn673h/vBgvP4tkOdAbaudX6w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [android] - '@oxfmt/binding-android-arm64@0.65.0': - resolution: {integrity: sha512-6DXH5sftNlaHpWJG50hFMF+Qxtq5D2TmahvcDPxWNcGIf8qrC9Y0YgHYcYZ2hlWzaccKXh/f3GcssH8vtkl4JA==} + '@oxfmt/binding-android-arm64@0.66.0': + resolution: {integrity: sha512-u7O+bSSF0HGsDKkQQxBqvLGVepu93RA+JKu+ONqvfh4sCnCEbj31wZj4iG5gk3XfRwrmYj0/8catkO2LcblQKQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@oxfmt/binding-darwin-arm64@0.65.0': - resolution: {integrity: sha512-K9m7lr53pcOLETNsC88sWes/GWHUGjZyHx95UhYcSXy0r30haLdeXlSufSenEAtoLaW753WN8/l4M7GYcRt6cg==} + '@oxfmt/binding-darwin-arm64@0.66.0': + resolution: {integrity: sha512-/ikyMIVjX/sdo7KtjxoEsSUosfPzveVhT9RWMx9yGqFDKFJ89JAEKuEeLBmurDjrkb4w8tOnAdSO3SBaplY3bw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@oxfmt/binding-darwin-x64@0.65.0': - resolution: {integrity: sha512-sTNwIx1gre3MyiHOPLu7IGW4UyMScYL4DTmJT01p4vzB0En+OJUQz6KuH8t0PpsClRSaMuY3b0QmtoPItfO8Lg==} + '@oxfmt/binding-darwin-x64@0.66.0': + resolution: {integrity: sha512-q5xUsKeFqawa9NXa6ZGXWimFV19m8MogKPdTaSVDAAk2EQKBmBZRDeluwcl1p8ty/OFc9s9888OKEh3xfPVH0g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@oxfmt/binding-freebsd-x64@0.65.0': - resolution: {integrity: sha512-lYZMVIiIpnjGu5hJb2jxA8NYQ/e0OTGuaiAf4dqlGPNnPmUTu23FZRMltmjro/KkQm1uE4NT4n5yJ2zWmKcpfA==} + '@oxfmt/binding-freebsd-x64@0.66.0': + resolution: {integrity: sha512-CR+x4VzMY0pRXLK/xFQ/RzsSFkP5t2Z2mef0QY6OP/rTRcMUoMLCOM62/3Fp/t0K+UDoBKxvMyeb6D0zPMjleA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@oxfmt/binding-linux-arm-gnueabihf@0.65.0': - resolution: {integrity: sha512-gIdXFAt/bURnjxuoedDEWdZ0PEWEmdDcm8qdpoFYYvW3QMk/5D4vUaH4mlMeRpeTdST4izUgHVO6RawQ4QulJw==} + '@oxfmt/binding-linux-arm-gnueabihf@0.66.0': + resolution: {integrity: sha512-ZEYmO/LbH9tTQCADILHGZE4GeOXOAj2VzedHkASNwjmwlwtutJCLpCJbIs37wRGTFgWRoEcD72jpMX+IBJUGjQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxfmt/binding-linux-arm-musleabihf@0.65.0': - resolution: {integrity: sha512-jJVyADto7gA2AaX5qAjAexrxx9PJQaKWOe8PICE7yKMbjBRyOHcmj9TtVJ+MZYDUQ3hodU0AcoTj0jFQ1W4C6Q==} + '@oxfmt/binding-linux-arm-musleabihf@0.66.0': + resolution: {integrity: sha512-hNtR9/oU0CeTkq7JnRkmBQwqe17v2ZaAMLC4VcN7IIOWeRyWDk0knSPWS9iiLmtbZ2RRBBtsG01jQgkZmKCJeQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxfmt/binding-linux-arm64-gnu@0.65.0': - resolution: {integrity: sha512-p3RFkB+u7u+8up99b/NEcI1hdpLDiGgJYNwDorB60n7eH+eKposAKuMBxx+NqB3b+sJP4CZmYDh9G7X62tUsKg==} + '@oxfmt/binding-linux-arm64-gnu@0.66.0': + resolution: {integrity: sha512-uwOVQ8i6I1LT/+eDzfsgrrcZp8Fn6NPVUPn8fF5gdFGekFf0PddF+LEuwsD0/pbNUcKZhDj2rQ5UpITh9gF4iQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-arm64-musl@0.65.0': - resolution: {integrity: sha512-5Prb0uFzJHr+OUD/qS/TmU526wD+PaHDsm3KoRiUXbMIDpTSErjeQYkK3OQeshAvD/PuLa9WGEi9WPajjdOZJg==} + '@oxfmt/binding-linux-arm64-musl@0.66.0': + resolution: {integrity: sha512-tTkF2Dmx4nGAjmBlZb+UtTGqR/EK4ZrW9qBfzte07a9XWqzoGGKzpFFlyNDhQe+Uwql94+ReCTeNbhOXscw1Dg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@oxfmt/binding-linux-ppc64-gnu@0.65.0': - resolution: {integrity: sha512-S8svxTp81obnF3admN9yd+u2rOYXtyzThLGBTg1PY6TPtGcC09BaaXLQD+TBSMa7yvqhCDZ8DFri+S/yG60qCg==} + '@oxfmt/binding-linux-ppc64-gnu@0.66.0': + resolution: {integrity: sha512-F3cKHUav4yXOHn6GFnwpBhSYsJOYKKf9eqO/9jlEuqPxNw9zb98E9ZFct79gcg8pibUGkbveEu9WDlmXJpDzKw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-riscv64-gnu@0.65.0': - resolution: {integrity: sha512-WtXBr75G/h2qOHy8SiGtC1R6aS3jt4mE52v1D8AtwMXIgoOmSNP9lKvbSaTRoL0e5wsMPoi6T72QWDYPu+S+nA==} + '@oxfmt/binding-linux-riscv64-gnu@0.66.0': + resolution: {integrity: sha512-K5fDaNZfDyQMYA/3qL21bqyN0X9T15LLwwbFPt2aHc94+ZG7bh0vZEsy2y7NlRnjjHFSwN+Hzg6ldJtbOriH4Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-riscv64-musl@0.65.0': - resolution: {integrity: sha512-YwSLVvpaz4o/nv/miiPEBJz+eJ+VmbgNIrao6RccK9ce+L5EA8wP+ZD0uFeq6wKOza6zoWv/dR0sj6lip6R3EA==} + '@oxfmt/binding-linux-riscv64-musl@0.66.0': + resolution: {integrity: sha512-44Yc+I+qOmTElRcEhm5hUKIUJEQIOugymz4ua4tB0Wox7tGAfIbjzmXz/HDAtw1Ij6gmBwZlzh4hc9679RhWeA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [musl] - '@oxfmt/binding-linux-s390x-gnu@0.65.0': - resolution: {integrity: sha512-XQTPqgvyrgkKcFq+Tp2eK6JS7sqqJ+nRmy2Fav4j3I+i4dJoPJm7YwEdoeSDX9xkqj9jZ/lWfF3bXUWztIrn6A==} + '@oxfmt/binding-linux-s390x-gnu@0.66.0': + resolution: {integrity: sha512-1e29Eg9hEj2kRBB19M0seIehPbbXHCk35GvImjDvb79rjjYjXCRmtbUNHJcgoktZAMIzXrTbxDBKmTc1V4bg3A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-x64-gnu@0.65.0': - resolution: {integrity: sha512-cjZlx6S/VkeCNWCbwZriTnLnZeTcV3DEyeRGSw/2wwLP9viq+C0bJ4bC1k/ZLkFxDcB1lUgSasPkYGP1bdraOg==} + '@oxfmt/binding-linux-x64-gnu@0.66.0': + resolution: {integrity: sha512-vODY1UQo10gngn0+D4xHKU84F1Twm1LqrzV4SqPXvmQKSd87paehvZ6jqA5wKs6XQrlWul9clYMDVHcoW9CPMA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@oxfmt/binding-linux-x64-musl@0.65.0': - resolution: {integrity: sha512-2azCjxdLtK4zCcIOU1dlXlU0xxfbPi6EjwWx7Ac7teWPidIIDOcIhudup83xNCKYhtqeVd/gaVDOxbUq4syXWA==} + '@oxfmt/binding-linux-x64-musl@0.66.0': + resolution: {integrity: sha512-YDzXx2JsT4+HL4MdkVrYjO55NS5lUKNm8rLC4ZPou8+seu0v0jhecSh+ufoO6+xEa8gccEezMlI2WHJi4ApUgw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@oxfmt/binding-openharmony-arm64@0.65.0': - resolution: {integrity: sha512-KXQ7xi1e/voP0IQaw6fG6XY4Z5+Llf1XmRSZS1t7pVFCecFJ0iXaboKmVwjFtp5MLlT5iWQrJ2U1C3GJdZ2u+Q==} + '@oxfmt/binding-openharmony-arm64@0.66.0': + resolution: {integrity: sha512-mJjUYd8lj0+j4JkYyEM+5qKBf1Rnrpgjn/SVYKJhicVDqLz566ooa7Fs8zflPqt+dnZDV7X054rVIQX6ZcQNlQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@oxfmt/binding-win32-arm64-msvc@0.65.0': - resolution: {integrity: sha512-2FbbjG5jEqLSLKVJwBap84uJfpn5Y5A53KEO0aUNr+zeiRB9nyPUIFMcSbZVMFLitfBytFWRNngozXYjb6Rsbw==} + '@oxfmt/binding-win32-arm64-msvc@0.66.0': + resolution: {integrity: sha512-soV+0vESv7e5ntCHWC61x4gg8OSak6IHHnWsZmHrJFlvMj2AK+kmldErCNkVkrvc1Ts2/++rJXn+IuAb2WMXhw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@oxfmt/binding-win32-ia32-msvc@0.65.0': - resolution: {integrity: sha512-LJ+ZacAPSjegDOnSLyA1TMWAhdDrsK4el3REdr1oL2UtVBCMhO2II/Sb3cEW6mF2MfLhl8hDNCSvc7KSbgk3LQ==} + '@oxfmt/binding-win32-ia32-msvc@0.66.0': + resolution: {integrity: sha512-YCPi23uRIEYuIKTZohAkKbPFpujQ5QBuUM5iDv+UqbCmTPAkaFsxjsSuB8xlBpRT0G7eP/4HMF+cPDSqHtOD9A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ia32] os: [win32] - '@oxfmt/binding-win32-x64-msvc@0.65.0': - resolution: {integrity: sha512-higu9cWEO6XXFzATD1jf0mCK34rNfN2H9JrJie7QB1IhleVpTh0QlLH9Ip2C1H/Nd5n0v5pvRtC+5R0uE4HpVg==} + '@oxfmt/binding-win32-x64-msvc@0.66.0': + resolution: {integrity: sha512-bwTQcv/JVRPkOqQtMF0X7vpvpncDQiBcXHxZ9S2hR12Hlo8bvBdUR5x5XnxzDZ3kM0qoZw1rv7KaD66Ly+pFWA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] - '@oxlint/binding-android-arm-eabi@1.80.0': - resolution: {integrity: sha512-RM3Plj+biQpxa5d1GOOX6ciDlcUROmm4OZ/pLTpitkQt2mJv4jhtY4cbgaetOm5UKWZe05/TGQ6o1Vl8EOHkrA==} + '@oxlint/binding-android-arm-eabi@1.81.0': + resolution: {integrity: sha512-IcCRsXiedJoJopY6mpZUBEeVFsUrutmrG7dZ87zMuKJlhg70Ora9bBl1WcCxZQtyI10YpnVdEso5oCg7YcfSHw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [android] - '@oxlint/binding-android-arm64@1.80.0': - resolution: {integrity: sha512-YlO5JEf0Yr2bUUlu8O8daVcUxtcGGbcSmyV7E7nSbJbfAdxTE0PFPwgnIlw7wXJaTYjb+qs5hI5q3jxUkI7cAw==} + '@oxlint/binding-android-arm64@1.81.0': + resolution: {integrity: sha512-GRrIPyTGVhx3L3h+0T5xT2A0jFAcdPv4+IfuXpGDLIdl6XeYhgg/zw72A5ILZoUgRqZuM8F1y+V/gfDriXSxzQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@oxlint/binding-darwin-arm64@1.80.0': - resolution: {integrity: sha512-BULDOyO3AhsmdWfQeIUCykDt3dd7XZBGLhp1eIh56skRv01O+cNjNPwXMIbeW1x4+pxcln5if72wcRgViVo7PA==} + '@oxlint/binding-darwin-arm64@1.81.0': + resolution: {integrity: sha512-qNQ9tXRgLuKbqSV1S2h9h4KPHjbovO7RRR2/enUOtHzTkFZ7B9X5zqqHJua8dRyc7dBy7Aoyq5pqTSLFVcAzGQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@oxlint/binding-darwin-x64@1.80.0': - resolution: {integrity: sha512-YJ4JzLw7N5TDSQFlA0hAQGHvnDZgyypm1yunObVWcWiF9KM7eGCJKYKLgTC2Fi/57OdnBhbj4OkzPGdFQJ6HyA==} + '@oxlint/binding-darwin-x64@1.81.0': + resolution: {integrity: sha512-q0QTm32jWga2Gv4j7IaVZN0jYMi9UV73sWVgFtDA4iIfqwMCLLZ3ve+9KwfYtsaKZSgQhmPaogeZWqDZpcY1Pw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@oxlint/binding-freebsd-x64@1.80.0': - resolution: {integrity: sha512-AYUIk5QnL0s8oWAYsREZwkRYy1SupJTXALo93J1TgzHywxQtdM99FecRMQ87MXEdPQ0j1TmEpeeq3fGNkpvMqg==} + '@oxlint/binding-freebsd-x64@1.81.0': + resolution: {integrity: sha512-/+8wVWDXEC7wHVAhOc59Fw/SkMc1arLkFD8iQCaSsmzenK1X4doFqquL9H1wrtGUzaiycVqkf/sSpcILK6W1UA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@oxlint/binding-linux-arm-gnueabihf@1.80.0': - resolution: {integrity: sha512-9hBZVANupQ89W9dXyE0n8doCyaW5pDyGn3y6XlIMPZ+rIKuyqkr3SNUXmVJIhuvUq0NBU3RBiSXXE69l4XI6KA==} + '@oxlint/binding-linux-arm-gnueabihf@1.81.0': + resolution: {integrity: sha512-4xt422FEgioRq9hAL4Tq7fujGUWnc8z1BJ+Oi8RN8vB8axaP+sdK6a2xdlcQCCYnJg9QMuMFS0AucuIFx/EacA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxlint/binding-linux-arm-musleabihf@1.80.0': - resolution: {integrity: sha512-SvS2uKqzY+pbfuvAHzH4338R6Zwo805GAwrIMVvK1KxoOWCIjZUdfzTCvilD7z6JK91v011+zYMryabhDo2AsQ==} + '@oxlint/binding-linux-arm-musleabihf@1.81.0': + resolution: {integrity: sha512-u3vna8KdGplH4DRCW9K54D68fcMo7IxVrkCJWwXnIhwtBdnDnYrmzOUA/XjmBlPpcLsgw9Z5BNdY4za9+Dj+MQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@oxlint/binding-linux-arm64-gnu@1.80.0': - resolution: {integrity: sha512-tCLadyqRVL3pQTRPNg7cjXKvcvS4fbyXeQHhKk5BTJ1oftQln5/yIIWbu/Xom/DX41zv2P9QGt6+D/TtQVtY3A==} + '@oxlint/binding-linux-arm64-gnu@1.81.0': + resolution: {integrity: sha512-3j9k+gsYsE7nv71GWotXsqsa2l9/aJenD7dVHNt/CBvsb0SgRjSMnHFeP59IXUAl1wvVFhqGl2wJNMwWU3UBlA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-arm64-musl@1.80.0': - resolution: {integrity: sha512-XfpCNRlOPcLlJl4Bn/FUhjqlR6BVavEykERBf/MV7YA9VZDa5g5znVqYhyviMafcxS9Pe/i/kPvHNO0U6svEHQ==} + '@oxlint/binding-linux-arm64-musl@1.81.0': + resolution: {integrity: sha512-k5iAp3dNxW0/uDCBY+WSm8jKB2szu7SkEQZdgRRpDXvuDd69vvDcqhB3A/pWCfCwXyenjNjFn9Td1fVoyAc+Yg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@oxlint/binding-linux-ppc64-gnu@1.80.0': - resolution: {integrity: sha512-3I4yMwcFG9NeO8ioY6JBBuKsIm5GL/x7MATt1S4tVWaxPu5HcJ+XnLUbcVBTxG8q2Wu56HSj+NmXQiVYb1lp6A==} + '@oxlint/binding-linux-ppc64-gnu@1.81.0': + resolution: {integrity: sha512-TFqLja3uYmVSte6nof9GWrex9Z8WgdZrNiLC6Te5rXGDqXB2y4j/26iFhwosXiAFqDhE9JJVuuCkDKLwptTn1g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-riscv64-gnu@1.80.0': - resolution: {integrity: sha512-E1wAKymkpe1/E8helzBKdm81OBOF+ezxRyXRMEuik3ZpWDER5CPOKZwF66RsdwW98uwZv8UTFremUQtC1CzdJA==} + '@oxlint/binding-linux-riscv64-gnu@1.81.0': + resolution: {integrity: sha512-UEcySvGS0NOVo7h7n7CYyJL9+6gFAh7Zc/ToDXVScFvzHSTIxtzkMVU30rmQ6+nQ1LF+UdiRDdJajpDu+OylLg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-riscv64-musl@1.80.0': - resolution: {integrity: sha512-+gLRGD4sIo3+VA++iham5UxD9tKSoJ/VOrROCEXIcknrYtQg6iIQgvjN0cpiRF7N6UYC7pJbvHJlDnMge5LRpQ==} + '@oxlint/binding-linux-riscv64-musl@1.81.0': + resolution: {integrity: sha512-H+diDbhD00+wI1IRP8Kz88x/lat+DgtoBJzoTthS16xkTJGNaEkfb8gzmd1rzc/2uDQQMl7GNl+JFUacVeWxIA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] libc: [musl] - '@oxlint/binding-linux-s390x-gnu@1.80.0': - resolution: {integrity: sha512-aR0PrzHj9leW3NmzBAAP4EzdoBNoJcs9sjnIQPIwyRnBGYrRbXUIpEB5Q39AqK3PLY5JK5uEhDQDiUa1QSAstw==} + '@oxlint/binding-linux-s390x-gnu@1.81.0': + resolution: {integrity: sha512-8znJ/5TekjOKg1j1Acho4PJMdiAHLtlcXuWEiipOhAMV6rQcXdmDdXCbheyDczN6TjBwiNfjcP81k4AthrKRzw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@oxlint/binding-linux-x64-gnu@1.80.0': - resolution: {integrity: sha512-vSVh5cSo3Xxs6ghBCcFJlpbkbENzDog1qXtoXLa/HC3aCrR4XO76GZbXmQoCPHnu99nQpdCeC3H9tdNICfDh7A==} + '@oxlint/binding-linux-x64-gnu@1.81.0': + resolution: {integrity: sha512-Q2Wj70yFsvn5QjlmifFzbj4H+kJy53bwqc41o1fzoM7MpLV1NIbhg/LpWXRfC6KOkSAdUx1Wd8VJsdPmhp/HRA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@oxlint/binding-linux-x64-musl@1.80.0': - resolution: {integrity: sha512-FfzBXpNQ8u7/ZI/p8bl73MeZ508Ax3hxWp3SiJpEFiC+BB9XcXy5FAZHTLKDPSzrUpxQZSZJAVdDmuJp/+HDBQ==} + '@oxlint/binding-linux-x64-musl@1.81.0': + resolution: {integrity: sha512-cPInHp/ddEe5qkyK2IiyQ8Q3Mp2oLLEhhsGgTK2oZx4L6+llGam1H1yBvJZ7qHfOXj8N3hxBS8sj4tO+gtFlIg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@oxlint/binding-openharmony-arm64@1.80.0': - resolution: {integrity: sha512-zMzbkumtmprCgRwoYNzcB3iC39fXdJIMLMU33KdCjEGLlJGOEt1+LwQ4LF8ndLzAEKVz4BR0y3V6Xrkk3Nm3yA==} + '@oxlint/binding-openharmony-arm64@1.81.0': + resolution: {integrity: sha512-0CQxSX4ajqm07AHBf5U33qQzXKdd7wtq/oTL/7vpY6RNNuxrRi8W4bqUV1Jyu/vj+9KmxQyDhxfeVX1nQL6kfg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@oxlint/binding-win32-arm64-msvc@1.80.0': - resolution: {integrity: sha512-ib6iRcrXsk4t1fm3iKcwksyWh1ZkZXC/2mEzakl0ai2+6HZunf1WWMZ/xP9EJAvw9g9K4UVTC3NF/+G2qLrbTQ==} + '@oxlint/binding-win32-arm64-msvc@1.81.0': + resolution: {integrity: sha512-l0hbeISm9673hVrrQU8j/p2M7YH9Ouoj7p7E/QM55NTrKVLP+P3PF8hLu+OY+x0VtGRW+ggiQKZqmdYps9H+TA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@oxlint/binding-win32-ia32-msvc@1.80.0': - resolution: {integrity: sha512-xhRWBMpLxZvgKAH6+DJZmpP+W8Y8UdQOSU1JfxSWNXsaBaRGW77j+1hCuNHlzj7OH4SPN8fYd1q0o2qrDtoVyw==} + '@oxlint/binding-win32-ia32-msvc@1.81.0': + resolution: {integrity: sha512-ksqPP5jbFXcYreEQ7zdJh06rJQBymCTyGRCdaXjfcf2aG4f8KxUWY5wcgYHmaTK+FJ4bPG5sUAdOX+6trnH1JA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ia32] os: [win32] - '@oxlint/binding-win32-x64-msvc@1.80.0': - resolution: {integrity: sha512-yAnO7lwBYQnz2pcfBPIGQQZWIX5zd5R/1aAKIF3oE+TVj7IhoHcROjOkz3sRDngzqhfPKfFaXqug5j5rE5dn6Q==} + '@oxlint/binding-win32-x64-msvc@1.81.0': + resolution: {integrity: sha512-IZuUCwGw9emG5JtCp+fYGB+Z4OWEoeEcM8R5BA1pYw63/ieYFVdcU2ylxTpHbVHSenZnsYE+ZZ20uHAJszQ4cA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -2184,88 +2355,144 @@ packages: '@radix-ui/rect@1.1.3': resolution: {integrity: sha512-JtyZR+mqgBibTo8xea3B6ZRmzZiM/YeVBtUkas6zMuXjAlfIFIW2FgqeM9eLyvEaYX66vr6DJMK+4U6LV0KhNw==} - '@react-native-community/slider@5.2.0': - resolution: {integrity: sha512-484sH8aWEaSjxaZ7HT3YZ8CKDcNes2synko1vdEz5DFEdvKAduxKJTj22L/qBMD7rtIkfbX69DMzWDAGbOAV6w==} + '@react-native-community/slider@5.2.1': + resolution: {integrity: sha512-HBDPRFRIN47hRtd5tdDWZp3kkVN1z98jGbK9wHOfUfVO+PbBDZzD7xjiiyBqNpg6Rom2QPFOpBZzsB2bHy4yAQ==} - '@react-native/assets-registry@0.86.0': - resolution: {integrity: sha512-nIaXbm2jX1OTYp0qbviJ3O6KZivoE8z3BnhUQ2LsqfZSWRoOK/n1qsiAr6oALiNKWnXY3j2KPwtYORnZzp8xew==} - engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + '@react-native/asset-utils@0.87.1': + resolution: {integrity: sha512-FeFnbn9ENPs7IVBzZt1bBWfiRGvT4q+CsWwXGFyKVW/1ol3ylBRuTtXBGHIAmcNN4fUR60nSXsCN19pzNHkPgA==} + engines: {node: ^22.13.0 || ^24.3.0 || >= 26.0.0} - '@react-native/babel-plugin-codegen@0.86.0': - resolution: {integrity: sha512-qdsABWNW7uTll90l4Vh03gjeyu3WVDi2CyiiyvYGMRDcoYbjbQi6df3BMAm9lQI2yslZ1T14LlDDAsgTwNxplA==} + '@react-native/assets-registry@0.86.3': + resolution: {integrity: sha512-TDhgCZA4wjJg84d5A9swiOQYPIWSKEEVdg9IwMFZDupQzW/F3QoLUrfAJOcalgqTDA9/buTB8awhE3Whwg6u9Q==} engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} - '@react-native/babel-preset@0.86.0': - resolution: {integrity: sha512-bYQcWiPySNvF4dns9Ls9gMmwgq66ohvM9Fwc/Kn8r85t66UNHxch3p1QwPiSorDelFauZwJbgo9+ReibTgvpbA==} + '@react-native/babel-plugin-codegen@0.86.3': + resolution: {integrity: sha512-O6Xza4JBGPIU8J7YbKTyBoYL4thpy8jMW/oaLDWdAyOwYHKIjK47pAL5HUEbOe2bWz2PEKjbYRF2ApkJv1ottQ==} engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + + '@react-native/babel-plugin-codegen@0.87.1': + resolution: {integrity: sha512-DfnyLAHG7jH4pqeID7ib6AeD6gG8T+KvM3c/w/yEa5ONkfRcHvTC8JMIRdhUqs4ruA+8xbYcNVQzewYRCPm1Xw==} + engines: {node: ^22.13.0 || ^24.3.0 || >= 26.0.0} + + '@react-native/babel-preset@0.87.1': + resolution: {integrity: sha512-EN1oo8IqsJgq++na/buq6hYsdZJY5A8+URohSA620eGz5a+322WOAcBuvcYZTeovYwR2NegvpPk5h+QVk8iUMw==} + engines: {node: ^22.13.0 || ^24.3.0 || >= 26.0.0} peerDependencies: '@babel/core': '*' - '@react-native/codegen@0.86.0': - resolution: {integrity: sha512-uTs9DBo3+/lUqinsGZK0FKJRBVClrwMXoZToaDxE1Q2SL2e55vs2GwyZfIKzPl5uJnbu4PfFMIp0/mLXLWUMuA==} + '@react-native/codegen@0.86.3': + resolution: {integrity: sha512-Ux4jHi0fh+bdtVEcL0gaPLbY56V+SvFUDl/8sRAE1jdb4k+o7fT/4Nc29yz4X+qfjstkSqObQTMBGhdzxH9JvA==} engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} peerDependencies: '@babel/core': '*' - '@react-native/community-cli-plugin@0.86.0': - resolution: {integrity: sha512-Jv8p1ebEPfTzs8gmrjsdT2XMXFfeAg45Pman+XPLFGaSeGAZkutRFRyX9Cs9aGTSOyIA9YPJ6vDNb1ayTf1FKQ==} + '@react-native/codegen@0.87.1': + resolution: {integrity: sha512-qbaqEdlUfj2vRgvWTpMoNgHnEqAhAYJLUrpGkb0WC9n0kdtqUvgigpz4bDktZwolM9BemXwgGFgyiAnbs3t0xw==} + engines: {node: ^22.13.0 || ^24.3.0 || >= 26.0.0} + peerDependencies: + '@babel/core': '*' + + '@react-native/community-cli-plugin@0.86.3': + resolution: {integrity: sha512-qSDL9LQc5mZSZPNczT95WU9YQuPzxBklgON9vLhhqfI0yWIwKInqFx88dQ/uiEXBtf0yossthaQIqA4Ml6bF6g==} engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} peerDependencies: '@react-native-community/cli': '*' - '@react-native/metro-config': 0.86.0 + '@react-native/metro-config': 0.86.3 + peerDependenciesMeta: + '@react-native-community/cli': + optional: true + '@react-native/metro-config': + optional: true + + '@react-native/community-cli-plugin@0.87.1': + resolution: {integrity: sha512-aGBae6v+ngy8fIpU1EOaVxn/8DOgmJNphEdn5Eb0wTchUCxr8bDjpTJYNdrptyn3qI/elk8r9agQ2OBaFvrRlw==} + engines: {node: ^22.13.0 || ^24.3.0 || >= 26.0.0} + peerDependencies: + '@react-native-community/cli': '*' + '@react-native/metro-config': 0.87.1 peerDependenciesMeta: '@react-native-community/cli': optional: true '@react-native/metro-config': optional: true - '@react-native/debugger-frontend@0.86.0': - resolution: {integrity: sha512-7Mb3nDfyJeys+ELF75Ageu7VKERlnIMoO+aNPoXqTXvz+b41L6l2CqMyLpDHxkBSlenij6gEepPNgaIyWHbJZw==} + '@react-native/debugger-frontend@0.86.3': + resolution: {integrity: sha512-TQmeofQ0PcuylhhlleOeuzHYZfbrgm3gayXzowqUEzgRisTm1D40/J3ggqs7XkQi5HP5ZA3n8dHmKL9vIzPcsw==} engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} - '@react-native/debugger-shell@0.86.0': - resolution: {integrity: sha512-Y0zEkZzLz8ou6o/VLml1A31X/rMgc6DRjwxwzPMa94qRTMY070WeBCNTITQo4kKTBAUgbxh07oXPQqp0Tpja8w==} + '@react-native/debugger-frontend@0.87.1': + resolution: {integrity: sha512-RNm7soJB+8YSauLnsCCylA1eVfT6JWaDTPUEF01uu9YwDyZ7remKlZbXtmVbtscbNGcp+hbI4iZUdVOpQWsHuA==} + engines: {node: ^22.13.0 || ^24.3.0 || >= 26.0.0} + + '@react-native/debugger-shell@0.86.3': + resolution: {integrity: sha512-O4ds+J7xZfxkbih9T+cAGegBdvKSPKYJm/lDgC9CpEjFMkmzWTpVLU3Qsv9sqZuo58z+sGhIcJfPsCFFWHpqbQ==} engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} - '@react-native/dev-middleware@0.86.0': - resolution: {integrity: sha512-20pTO6yTybmvXvro520H6C7jydIQnLKOl5qFtVEcHSdFrY63r3OGei+Rx9bILgSRmH6jgnfEcijcMx7pwWuQtw==} + '@react-native/debugger-shell@0.87.1': + resolution: {integrity: sha512-eNbKjcnseJIjy5XCDwyhKOT1ngOg3Dv1fVxTDtnVFqdqjqsbfY4v9JuJG1RZJ7+mFoRRe05HE8GSQU8B7n5xwQ==} + engines: {node: ^22.13.0 || ^24.3.0 || >= 26.0.0} + + '@react-native/dev-middleware@0.86.3': + resolution: {integrity: sha512-LiEPTqTg/63bYUnrPyHLfjTDCNhA/+CUqI1+DsA9tYyewtSbULd5awsva6SgE10I+2iMhgKXS3ymkhU/kSCrGA==} engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} - '@react-native/gradle-plugin@0.86.0': - resolution: {integrity: sha512-a1RcfaEDqWExCGfCwadIxt4l8FvKYgFqeMf2uzeKyAOnb+vTGNIeCvifFL2MqvgaeYxlER437HbMIajGcuJ1pQ==} + '@react-native/dev-middleware@0.87.1': + resolution: {integrity: sha512-KgvAGUaVl6/XrWFAPK8e0ojDRbsdBjr4bnwyn6PC3CwxPQc5iv+i7hMoUwYS8ORSkHKfjt+cV86/mBQ4lG8yfA==} + engines: {node: ^22.13.0 || ^24.3.0 || >= 26.0.0} + + '@react-native/gradle-plugin@0.86.3': + resolution: {integrity: sha512-lxmx0GqLEWRIpZfpYFXlYVIs3ENQwaW6Vmp6oi29l2GoQJ1wZfFZRdMimDWlGEk8LKfHar3QH3iaPMkTcK9lEQ==} engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} - '@react-native/js-polyfills@0.86.0': - resolution: {integrity: sha512-zYy/Cjd1VTnZ2iCNaG9bDF9C3l2ntESiPRscjIlI5FKugu6aeTwsDSv1aI8Bc4Kp3vEdoVg+UQhLAhE4svREaQ==} + '@react-native/gradle-plugin@0.87.1': + resolution: {integrity: sha512-bZ5X2BNxaSlfp2KlDtUM910QqW9G1yTIeZXghuv4ag8SAQLzm5Mf9j5GjJfT6ax2Qo2aRT15Vi3Tro/yLGJstA==} + engines: {node: ^22.13.0 || ^24.3.0 || >= 26.0.0} + + '@react-native/js-polyfills@0.86.3': + resolution: {integrity: sha512-eYIJ0es967+tePBFQDnl/gidVFxLns3fnbiK6rxscQrGodvuUO6hwxpQnfNynJ8MjbbndImXihXjcnJdc7SzJg==} engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} '@react-native/normalize-colors@0.74.89': resolution: {integrity: sha512-qoMMXddVKVhZ8PA1AbUCk83trpd6N+1nF2A6k1i6LsQObyS92fELuk8kU/lQs6M7BsMHwqyLCpQJ1uFgNvIQXg==} - '@react-native/normalize-colors@0.86.0': - resolution: {integrity: sha512-kG0wfCGghUKlfxkJyyHCDVutWVYWK7/DG58ojA/4v9EfulgF+osuSQmlbNb3rcKX58qutm7JcldSeVLgGFha9g==} + '@react-native/normalize-colors@0.86.3': + resolution: {integrity: sha512-Cv3CDkprb67GrzuaS9BGbBJC/6G4lIw3nyKOHRKTqTTum4bn37y5+R0Z04L8mcbQN85eEohNrRwb7IOM4j6uvg==} - '@react-native/virtualized-lists@0.86.0': - resolution: {integrity: sha512-4/ZLXdf/OSpPDVO0AsQ1SJdRIzt5t9BNQ46QwGgxvX7/cirYR5k8KXctNGGgW8lQo2gZChEfY2zFCZg9nM/jiw==} + '@react-native/normalize-colors@0.87.1': + resolution: {integrity: sha512-8+AutemzX+a7cuKgTUyb7JUk3sFYgLyrSnlTLrL6LuW/LKDE+FYE8lJGWYhJPJcE51Ge1D8yRzxgQEdEFZtuIw==} + + '@react-native/virtualized-lists@0.86.3': + resolution: {integrity: sha512-1j44NEyNn05Ut40vHAmoSWbsIcybFkMAOBTwQt1PrESyfSS+qBoyU1LGIogNva0VIa0rQyEC5PzbA7R4/7Nhyw==} engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} peerDependencies: '@types/react': ^19.2.0 react: '*' - react-native: 0.86.0 + react-native: 0.86.3 + peerDependenciesMeta: + '@types/react': + optional: true + + '@react-native/virtualized-lists@0.87.1': + resolution: {integrity: sha512-qSZjeX3UJrDvyfjf7yc3E68rp1XnzE+5nu8ImklhkVC0+p/XiaHPb/KGkRqDdnQyWHN55BRYSsCMEwgVI6WRNQ==} + engines: {node: ^22.13.0 || ^24.3.0 || >= 26.0.0} + peerDependencies: + '@types/react': ^19.2.0 + react: '*' + react-native: 0.87.1 peerDependenciesMeta: '@types/react': optional: true - '@react-navigation/core@7.21.5': - resolution: {integrity: sha512-3hpV7uR41LBW+GHDoLhztZCb/i5ySRJISZ/rez4d7DCHSZo6ej4gNxYclaS6LRguoLiKG7SOCNa6O390AQklZQ==} + '@react-navigation/core@7.21.13': + resolution: {integrity: sha512-d1syMXra7RlJjGsjsIxEA14F6U+awFtQJeOfDc+V7x3y1mPqYQVTYnqnt3PR+fBBouUKaKsgKZsSR3MP5919Xg==} peerDependencies: react: '>= 18.2.0' - '@react-navigation/elements@2.9.30': - resolution: {integrity: sha512-2isleieiRMmP4WNMV2Q1u3qP1M47ZqsJ2hJ/Og11FeKXK8YmUTHya7PW7ecsgAh2CXKTxAcbfJDFTSvq2D++Tw==} + '@react-navigation/elements@2.9.40': + resolution: {integrity: sha512-x2fCMVKU23YeQvHSCFq2mVRYAbHqYNqb8kfWSEfIQ+XUX7dBXWd93/pr0w5jLjALDHDxQUaJKh4mIqr8q3zAwQ==} peerDependencies: '@react-native-masked-view/masked-view': '>= 0.2.0' - '@react-navigation/native': ^7.3.8 + '@react-navigation/native': ^7.3.18 react: '>= 18.2.0' react-native: '*' react-native-safe-area-context: '>= 4.0.0' @@ -2273,23 +2500,23 @@ packages: '@react-native-masked-view/masked-view': optional: true - '@react-navigation/native-stack@7.17.10': - resolution: {integrity: sha512-m1BWVEaOPX9k30DbmhsD7IlrUdl4J7Ogmo71rcNlbZQPMNB126av/nfwqB+qN7DIsiwTAnORnT+SwzD56qqD0Q==} + '@react-navigation/native-stack@7.18.10': + resolution: {integrity: sha512-dWH6bLHxdPslMmNCPi8doythC9gtavvh9LK8F0HIQLuYE5A2GOk1u6mrvnYrqyJBfsXa6gA8O0Zr6XGywynDnA==} peerDependencies: - '@react-navigation/native': ^7.3.8 + '@react-navigation/native': ^7.3.18 react: '>= 18.2.0' react-native: '*' react-native-safe-area-context: '>= 4.0.0' react-native-screens: '>= 4.0.0' - '@react-navigation/native@7.3.8': - resolution: {integrity: sha512-zHmQcxWBT8GOwsofEOmHqpdM5twkwE/esa9JFGlW4hpXeQTTe/dRcPSLjwsvePtolbDKz0YZbK+I5KrW3j63LQ==} + '@react-navigation/native@7.3.18': + resolution: {integrity: sha512-5lLYl0Kt85jcpEYxXzj2ecIKta+yo4EcAIOZoJe6jU9lbR0lu6sAQsotU3rlVAYL1VBY/xLemxs4WQ2LJs4Oew==} peerDependencies: react: '>= 18.2.0' react-native: '*' - '@react-navigation/routers@7.6.0': - resolution: {integrity: sha512-lblhDXfS75jLc7G2K7BZGM+7cjqQXk13X/MA4fq/12r62zM+fBhhreLzYflSitrDDXFRJpSvJXy0ziiGU04Xow==} + '@react-navigation/routers@7.6.4': + resolution: {integrity: sha512-GI7eJm8/KsZUQaYcXvEExikKurRZRgEsSzyZ7faENfi65yqJBCXjDMwyN1pF6pNW1MoLH1ErDwDivFxY6BzD3w==} '@release-it/conventional-changelog@12.0.0': resolution: {integrity: sha512-b9N8/tUjO9JNH1u2fVSmq8QmbqnE//Y5iCgvPcytsk0pj0GhgGCp+VmfzhEBIdd+lVGZJKs+ZSaGaOHjtmtv7g==} @@ -2297,97 +2524,98 @@ packages: peerDependencies: release-it: ^18.0.0 || ^19.0.0 || ^20.0.0 || ^21.0.0 - '@rolldown/binding-android-arm64@1.1.5': - resolution: {integrity: sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==} + '@rolldown/binding-android-arm-eabi@1.2.7': + resolution: {integrity: sha512-EypzgnYCwyVY4NDHKzGmNJT5b+XaQEBniHxsMdeIQLB/tcCzZnhqrzHpZFbX9iaxx+5RiB8caATBtfvZP7zVxQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@rolldown/binding-android-arm64@1.2.7': + resolution: {integrity: sha512-l17HE9EweWaqJZhuUuNBN/FzM62xw+DECVnJyvMsxn8vJFAGLy5QfLDoYAcronkAN8VxKZHezDpulHDPx95vFw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rolldown/binding-darwin-arm64@1.1.5': - resolution: {integrity: sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==} + '@rolldown/binding-darwin-arm64@1.2.7': + resolution: {integrity: sha512-8ED8ELFvHXc6OCETIn4gXObPiaR6bckM/ipXtbzlPVDRMBfEGjCKgO90F9YtfdpDatVx/ZQw7aZ1vUMf/+T3Mw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-x64@1.1.5': - resolution: {integrity: sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==} + '@rolldown/binding-darwin-x64@1.2.7': + resolution: {integrity: sha512-/WPripjtiAIZ2tWY7ddijORT0Ujg87wxWW/qcoFVCKAWVDPhtY0xr7Dj0M3GyNGz60jGwTElhro/mkF9dT7dDQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rolldown/binding-freebsd-x64@1.1.5': - resolution: {integrity: sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==} + '@rolldown/binding-freebsd-x64@1.2.7': + resolution: {integrity: sha512-14DI4NcqpvbICxSnGLx3PmtDaWqRP/KGSGb6C+JLLVPeZRl6dKdHba3pGsqT3vpdTqhEYIPG0MMQ8c0xYqoJxA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rolldown/binding-linux-arm-gnueabihf@1.1.5': - resolution: {integrity: sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==} + '@rolldown/binding-linux-arm-gnueabihf@1.2.7': + resolution: {integrity: sha512-bxrWIRvHWQvbJwi+VIie/kDJmQxcNE6xxWwZdqF/ExVAigtHkv54WTLQPb+QsZdnFy18fg7JPfWGL0RH6vwIlQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm64-gnu@1.1.5': - resolution: {integrity: sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==} + '@rolldown/binding-linux-arm64-gnu@1.2.7': + resolution: {integrity: sha512-toOY2BChBZyuxU7OYX6Tn389di4IzAqPTycVcci0O7FSfBqzRB3RZn+K5Is6ANf4tmgRd/K1yZTsNTXbkXsnLg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-arm64-musl@1.1.5': - resolution: {integrity: sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==} + '@rolldown/binding-linux-arm64-musl@1.2.7': + resolution: {integrity: sha512-lAIXTH/aiLRLxsTgQvfhjo4K1ydWIp00+V0voOr9beb/9ZmkUFrSIb03dXNFRgMNvkE6oGsF10ioQ6UsI+vS5Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@rolldown/binding-linux-ppc64-gnu@1.1.5': - resolution: {integrity: sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==} + '@rolldown/binding-linux-ppc64-gnu@1.2.7': + resolution: {integrity: sha512-kdnwS28Pkenp/mZMRwjXXXwxQ7pIsm+bF919LUK93BOyhcLsrVKdP2p9fxpiPNPAbNuch8ypQt0pm2P2LYCAGg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-s390x-gnu@1.1.5': - resolution: {integrity: sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==} + '@rolldown/binding-linux-s390x-gnu@1.2.7': + resolution: {integrity: sha512-516OdsyLdr5E65paF3yBF55t8mfm9+gmtCsK3xI7XKXIT7EfRlHhxL8K/NR6Hu8BWSgF5+1w74lTL0+nxcc8Qw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-gnu@1.1.5': - resolution: {integrity: sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==} + '@rolldown/binding-linux-x64-gnu@1.2.7': + resolution: {integrity: sha512-r8/z8n7GFaYRln3xmP1Cxy0HH/HLM0uBUPkEuSVEfKGDA89M0FsZRZJRSwe/tJjRx+fpH/gjorfhB8tmEbSFLA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-musl@1.1.5': - resolution: {integrity: sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==} + '@rolldown/binding-linux-x64-musl@1.2.7': + resolution: {integrity: sha512-pAsE8iiDxUg1xBqdhrTfg45AVDVpirjz00sblEYClGNNcMnDb+e8beQgqIAw6LvauX/APvgxUnwrgun/YYGBhw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@rolldown/binding-openharmony-arm64@1.1.5': - resolution: {integrity: sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==} + '@rolldown/binding-openharmony-arm64@1.2.7': + resolution: {integrity: sha512-lTcIYmmnQQA8Or/2DatS6oSqcdLHvendjS+zLu+FwgToynWMRSmQdpM65fTANJgIS4mjbMOo5KT2lnT9SAb96w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@rolldown/binding-wasm32-wasi@1.1.5': - resolution: {integrity: sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==} - engines: {node: ^20.19.0 || >=22.12.0} - cpu: [wasm32] - - '@rolldown/binding-win32-arm64-msvc@1.1.5': - resolution: {integrity: sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==} + '@rolldown/binding-win32-arm64-msvc@1.2.7': + resolution: {integrity: sha512-e3Gu3WxbNk/UqQhxqU7YIYO+9ZBvWNz3U+h/qRFosscMFzdRPbXYSaSWgSnklv2fz1TgzBTcti2z35c/7irsHw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.1.5': - resolution: {integrity: sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==} + '@rolldown/binding-win32-x64-msvc@1.2.7': + resolution: {integrity: sha512-W/jg5qoRSqjsEv0+dZi4e687mcHqmVuU0P4fK6qS/xjetW2Gmc1W8j//z5nAeNcC8Ttm0hV46IjcYeuVwYhuiw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -2551,58 +2779,30 @@ packages: cpu: [x64] os: [win32] - '@shikijs/core@4.3.1': - resolution: {integrity: sha512-ANMDxuaPsNMdDC1m4vfvhlDmJweMwkE5XitTwrq2rWHx5jM+dlm4MmHt2PP6t0uejfR77SuhrhJ0zEijIF/uhA==} - engines: {node: '>=20'} - '@shikijs/core@4.4.3': resolution: {integrity: sha512-QCR4q2ZO/ILJEuwiBMel4wdcTDb1JGwfjKTxPDF6x8ixOaluPrVqIn06C99AcRPhmYlBR56d/Fb+GN58GzExpg==} engines: {node: '>=20'} - '@shikijs/engine-javascript@4.3.1': - resolution: {integrity: sha512-JBItcnPuYq7jVJdZo/vMj94r+szT7XEjHFX+mvFDGSEIbVAXAGyHAHzhbWzpGOwYidCZrErJLLgn2PVeiokHnQ==} - engines: {node: '>=20'} - '@shikijs/engine-javascript@4.4.3': resolution: {integrity: sha512-FbOjFJp9VLdo1Wevs10BBtVxiTWwNLqZh5Gkhjgda/ioL15YOgeSl9n+6XMa3qRlPQzfhFNe641SrynFHYG0nQ==} engines: {node: '>=20'} - '@shikijs/engine-oniguruma@4.3.1': - resolution: {integrity: sha512-OXyNMzg0pews+msMj4cHeqT4xiYKKvbnn6VbdAXxfoFl3SSx4fJTc8FadECuc5/H9p3BzhNAoAUXKwAu9rWYhg==} - engines: {node: '>=20'} - '@shikijs/engine-oniguruma@4.4.3': resolution: {integrity: sha512-EcOQkxdxGQrc1Row/cC2c96/v1dbZqGnEVu1qTuT/MJmp6+cXCvQussowVmCv5Tqr3KuY3c7IbM6HTW3LJ1k9w==} engines: {node: '>=20'} - '@shikijs/langs@4.3.1': - resolution: {integrity: sha512-m0l9nsDqgBHvbZbk7A0/kXz/impK3uB/c6rAn6Gpg/uPtdZRQ+alsN/17MU5thb68XTj/4DxkZAotrM0GGSpDQ==} - engines: {node: '>=20'} - '@shikijs/langs@4.4.3': resolution: {integrity: sha512-ePic0yfAJGOF83D5wBHK/00EjK65oahBYxFk5epgq33WRv7X9UuxLEV8PtR0szC0z8dl7INIpIodB99JRFlR+A==} engines: {node: '>=20'} - '@shikijs/primitive@4.3.1': - resolution: {integrity: sha512-CXQRQOYy1leqQ8ceTeJdmXv/bsUY++6QyLpXJ94LZAAYj5X2SKRdc5ipguv4NPyGVKItB2PPwUpRNe0Sjh5S1A==} - engines: {node: '>=20'} - '@shikijs/primitive@4.4.3': resolution: {integrity: sha512-m0wBeLDQDeIxRdUmrCPdQqfuUamDwRL5isCfYbguKD6NiaKpVbsv+3J81DyIKgNW5h4WAIIr8T4EkgQrBBxvaQ==} engines: {node: '>=20'} - '@shikijs/themes@4.3.1': - resolution: {integrity: sha512-dgpoJ4WqNi2yTmizQHBJ5zcX6j2lE6icN/0yt4l1kkf16jrY/pwPLoTb1ETsWMz0OBLf9ZNvwmxft+cH+N9qSA==} - engines: {node: '>=20'} - '@shikijs/themes@4.4.3': resolution: {integrity: sha512-w8UHjeUnIR965KMWJHUPXOc2mNJUnK3vpVLYLvw5IYU2mnTTJ89E24OrJDBNiJDQ0qzb0tc4l7mrIXx5cFeIyw==} engines: {node: '>=20'} - '@shikijs/types@4.3.1': - resolution: {integrity: sha512-CHFxE0jztBIZRHH6gxXE7DXUCFXjReEGxZ/j0rfSLGKZuwp2xBYycEP14875DSa9KLL/6700oxIq6oO6ef9K2g==} - engines: {node: '>=20'} - '@shikijs/types@4.4.3': resolution: {integrity: sha512-UEJxmRR++MAGR6hugn0vgVS2W/6lWAts84FFSrnlH9sP0LNol7E5+NQ792pH8liWUhyMyjhTgSUH3k7iD7tc5g==} engines: {node: '>=20'} @@ -2626,11 +2826,11 @@ packages: resolution: {integrity: sha512-fCTuZK4QBa+39Oz9l4OGfJfz+GpwCp3AqO7Zch3to99xHPgstVsRFpeQ8LNd2o1Gv8raL2mCFwiaHh7bFSp5DQ==} engines: {node: '>=22'} - '@sinclair/typebox@0.27.10': - resolution: {integrity: sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==} + '@sinclair/typebox@0.27.12': + resolution: {integrity: sha512-hhyNJ+nbR6ZR7pToHvllEFun9TL0sbL+tk/ON75lo+Xas054uez98qRbsuNt7MBCyZKK4+8Yli/OAGZhmfBZ/g==} - '@sinclair/typebox@0.34.50': - resolution: {integrity: sha512-ydBWw0G6WFwWHzh9RK4B5c690UkreOG0llq0r+DaI7LgKgxigf8mhHzIPI3S0850g1BPkq/zpuCfrq4QFgUlTQ==} + '@sinclair/typebox@0.34.52': + resolution: {integrity: sha512-XiMQh7qqVlxZzcVD+kkGMNGMzcTrDMLWI7S4x7z1MkCkbDPrekpZXEUK0eZqZFMuHQg2a2DZOcDIh9o5v3Gonw==} '@standard-schema/spec@1.1.0': resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} @@ -2730,15 +2930,9 @@ packages: '@tailwindcss/postcss@4.3.3': resolution: {integrity: sha512-JTSZZGQi1AyKirbLN3azmjVzef92tcX7h+iSqPdaeStyFpGpDlKvvpxeOE8njhbUanbRwr3z8DyzhICWnMtQeg==} - '@ts-morph/common@0.28.1': - resolution: {integrity: sha512-W74iWf7ILp1ZKNYXY5qbddNaml7e9Sedv5lvU1V8lftlitkc9Pq1A+jlH23ltDgWYeZFFEqGCD1Ies9hqu3O+g==} - '@ts-morph/common@0.29.0': resolution: {integrity: sha512-35oUmphHbJvQ/+UTwFNme/t2p3FoKiGJ5auTjjpNTop2dyREspirjMy82PLSC1pnDJ8ah1GU98hwpVt64YXQsg==} - '@tybys/wasm-util@0.10.3': - resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} - '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -2865,6 +3059,9 @@ packages: '@types/estree@1.0.9': resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + '@types/gensync@1.0.5': + resolution: {integrity: sha512-MbsRCT7mTikHwKZ0X+LVUTLRrZZRLipTuXEO9qOYO+zmjMVk81axyClMROf6uoPD9MRVu46bx8zoR0Ad9q3NAg==} + '@types/geojson@7946.0.16': resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==} @@ -2880,6 +3077,9 @@ packages: '@types/istanbul-reports@3.0.4': resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==} + '@types/jsesc@2.5.1': + resolution: {integrity: sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw==} + '@types/mdast@4.0.4': resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} @@ -2889,20 +3089,23 @@ packages: '@types/ms@2.1.0': resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} - '@types/node@24.13.3': - resolution: {integrity: sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==} + '@types/node@26.4.1': + resolution: {integrity: sha512-k97ENvZWtvA6yqz5/FS6a7duDgOPEeOQOc2iKS/nY6mX6qJUKtLnWzQS+Xj6tXweyj6ZcTAK2Qecetnvi9nCLA==} '@types/parse-path@7.1.0': resolution: {integrity: sha512-EULJ8LApcVEPbrfND0cRQqutIOdiIgJ1Mgrhpy755r14xMohPTEpkV/k28SJvuOs9bHRFW8x+KeDAEPiGQPB9Q==} deprecated: This is a stub types definition. parse-path provides its own type definitions, so you do not need this installed. - '@types/react-dom@19.2.3': - resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + '@types/react-dom@19.2.5': + resolution: {integrity: sha512-fMPwH9v7r/pp43yUd2/Mbiex5KouJwwR3dzHkhLREUC6764VyDsqxhAxv6OFEYR1RhjOyD1naqba8ECDBe7ZQg==} peerDependencies: '@types/react': ^19.2.0 - '@types/react@19.2.17': - resolution: {integrity: sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==} + '@types/react-test-renderer@19.1.0': + resolution: {integrity: sha512-XD0WZrHqjNrxA/MaR9O22w/RNidWR9YZmBdRGI7wcnWGrv/3dA8wKCJ8m63Sn+tLJhcjmuhOi629N66W6kgWzQ==} + + '@types/react@19.2.18': + resolution: {integrity: sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==} '@types/resolve@1.20.2': resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} @@ -2922,8 +3125,129 @@ packages: '@types/yargs@17.0.35': resolution: {integrity: sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==} - '@ungap/structured-clone@1.3.3': - resolution: {integrity: sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg==} + '@typescript/typescript-aix-ppc64@7.0.2': + resolution: {integrity: sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==} + engines: {node: '>=16.20.0'} + cpu: [ppc64] + os: [aix] + + '@typescript/typescript-darwin-arm64@7.0.2': + resolution: {integrity: sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [darwin] + + '@typescript/typescript-darwin-x64@7.0.2': + resolution: {integrity: sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [darwin] + + '@typescript/typescript-freebsd-arm64@7.0.2': + resolution: {integrity: sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [freebsd] + + '@typescript/typescript-freebsd-x64@7.0.2': + resolution: {integrity: sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [freebsd] + + '@typescript/typescript-linux-arm64@7.0.2': + resolution: {integrity: sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [linux] + + '@typescript/typescript-linux-arm@7.0.2': + resolution: {integrity: sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==} + engines: {node: '>=16.20.0'} + cpu: [arm] + os: [linux] + + '@typescript/typescript-linux-loong64@7.0.2': + resolution: {integrity: sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==} + engines: {node: '>=16.20.0'} + cpu: [loong64] + os: [linux] + + '@typescript/typescript-linux-mips64el@7.0.2': + resolution: {integrity: sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==} + engines: {node: '>=16.20.0'} + cpu: [mips64el] + os: [linux] + + '@typescript/typescript-linux-ppc64@7.0.2': + resolution: {integrity: sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==} + engines: {node: '>=16.20.0'} + cpu: [ppc64] + os: [linux] + + '@typescript/typescript-linux-riscv64@7.0.2': + resolution: {integrity: sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==} + engines: {node: '>=16.20.0'} + cpu: [riscv64] + os: [linux] + + '@typescript/typescript-linux-s390x@7.0.2': + resolution: {integrity: sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==} + engines: {node: '>=16.20.0'} + cpu: [s390x] + os: [linux] + + '@typescript/typescript-linux-x64@7.0.2': + resolution: {integrity: sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [linux] + + '@typescript/typescript-netbsd-arm64@7.0.2': + resolution: {integrity: sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [netbsd] + + '@typescript/typescript-netbsd-x64@7.0.2': + resolution: {integrity: sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [netbsd] + + '@typescript/typescript-openbsd-arm64@7.0.2': + resolution: {integrity: sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [openbsd] + + '@typescript/typescript-openbsd-x64@7.0.2': + resolution: {integrity: sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [openbsd] + + '@typescript/typescript-sunos-x64@7.0.2': + resolution: {integrity: sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [sunos] + + '@typescript/typescript-win32-arm64@7.0.2': + resolution: {integrity: sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [win32] + + '@typescript/typescript-win32-x64@7.0.2': + resolution: {integrity: sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [win32] + + '@typescript/typescript6@6.0.2': + resolution: {integrity: sha512-mbCddXd+jm7hfx7w2YU64/Av4/NqqeG3GoRZgxPcgoTxYjhrcfJRw9ULch71SS4G+Q3bOXFhRvPqjguN0Hyp5w==} + hasBin: true '@ungap/structured-clone@1.4.0': resolution: {integrity: sha512-1mEZtMKPM09vDmQt5y7YvmN2+DFTP7Tg0EWXdic8/C6VRnpb33e4ghisCIE3WZjsE2N8mf+QV1Zqh7ZFYLWInQ==} @@ -2931,11 +3255,8 @@ packages: '@upsetjs/venn.js@2.0.0': resolution: {integrity: sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw==} - '@vitest/expect@4.1.11': - resolution: {integrity: sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==} - - '@vitest/mocker@4.1.11': - resolution: {integrity: sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==} + '@vitest/mocker@5.0.0': + resolution: {integrity: sha512-66PGTMIiVJP3t4a5yxU9qPtf7MdTBs8jmToMvy+HVflB3Yy13WJZTtPePdvU+wjRV02SKK5doLbSA6o9pwOmiA==} peerDependencies: msw: ^2.4.9 vite: ^6.0.0 || ^7.0.0 || ^8.0.0 @@ -2945,30 +3266,16 @@ packages: vite: optional: true - '@vitest/pretty-format@4.1.11': - resolution: {integrity: sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==} - - '@vitest/runner@4.1.11': - resolution: {integrity: sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==} - - '@vitest/snapshot@4.1.11': - resolution: {integrity: sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==} + '@vitest/spy@5.0.0': + resolution: {integrity: sha512-uy+luWBAPw9XfthoHi5AkfHUnuPYEESjl0p/r+meoBnU8bxg5GDQ3Ey8MjcJ6sqahkL4PFyrvfMJJBw7LbU06g==} - '@vitest/spy@4.1.11': - resolution: {integrity: sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==} - - '@vitest/utils@4.1.11': - resolution: {integrity: sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==} - - '@xmldom/xmldom@0.8.13': - resolution: {integrity: sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==} + '@xmldom/xmldom@0.8.15': + resolution: {integrity: sha512-/5NV/vDALVFDXgLmfsy9TRCBlKwO2LNBFzpzvb9iIj+jR+eSc6DLYYvVOdivT/jm7MtU6TebYuRmzEOI7w40UA==} engines: {node: '>=10.0.0'} - deprecated: this version has critical issues, please update to the latest version - '@xmldom/xmldom@0.9.10': - resolution: {integrity: sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw==} + '@xmldom/xmldom@0.9.12': + resolution: {integrity: sha512-5AXjrcMClTryPe9LgZrygpB1lj7s0S9E0+W+AHaVKAVyHanafK86iPSvG5xHVSp/jC+VH1UXu0TAEmY279xH7A==} engines: {node: '>=14.6'} - deprecated: this version has critical issues, please update to the latest version '@yuku-analyzer/binding-android-arm64@0.9.3': resolution: {integrity: sha512-6dOwkawiJYtUUBchfjrFo7poz460Yg+aQNgr4lHUPZ2fNW+llpMEZkbznTq25BEmmiBGPJr+L15dzTvR8zP4vQ==} @@ -3069,8 +3376,8 @@ packages: resolution: {integrity: sha512-TQf59BsZnytt8GdJKLPfUZ54g/iaUL2OWDSFCCvMOhsHduDQxO8xC4PNeyIkVcA5KwL2phPSv0douC0fgWzmnA==} engines: {node: '>= 20'} - agent-cli-detector@0.1.2: - resolution: {integrity: sha512-qdZ/9JFORtTKJNhT/IczMeEfEUbUU0K5umYeiIQHX+AjHs+Y9SXVzSgaYlpZeyNMrvuh2HpZiOTpvS57iPfBkQ==} + agent-cli-detector@0.1.7: + resolution: {integrity: sha512-d8OWDVdZMgjhLUT9ZPgSv/BdFFF9pVuscC0JdUSz3bjwE15gcp6u/o0/JooM2yyAWC49KThFhXlgTXRa9B7yng==} engines: {node: '>=18.18'} hasBin: true @@ -3118,8 +3425,8 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - argue-cli@3.1.0: - resolution: {integrity: sha512-DhBpBfXL4SS2uC0N922MMajKR3CdrTG0u2or1PNYgXMsrSzViJrbtvT0nCLlLGUI0plam/ZZCs7aAauHtW9thw==} + argue-cli@3.2.0: + resolution: {integrity: sha512-VipTB0gXgGIFO2Rg9yEVN5wLt2AurJcZqDbgmYSwPwsykhLrQhs240/bfceev4w68lI8JshoOJIk9uW7tFzROw==} engines: {node: '>=22'} aria-hidden@1.2.6: @@ -3180,12 +3487,12 @@ packages: babel-plugin-transform-flow-enums@0.0.2: resolution: {integrity: sha512-g4aaCrDDOsWjbm0PUUeVnkcVd6AKJsVc/MbnPhEotEpkeJQP6b8nzewohQi7+QS8UyPehOhGWn0nOwjvWpmMvQ==} - babel-preset-expo@57.0.2: - resolution: {integrity: sha512-PgsWlNSlNhnSkU4UCAcvl252CaB7VU1eQuSvJoRSiqfrZDGmeqwQmMUs+PcS5VUppFwmmhVlDPwOc7kR3Qr08Q==} + babel-preset-expo@57.0.10: + resolution: {integrity: sha512-08bt6bqMZFoQuWO9gkt0EXotbqKHimELFGa1LlKJvY89iKsi4S0FJSFBFJ4pn9NZvofet48HIjYDQ0SZJ7bNrg==} peerDependencies: '@babel/runtime': ^7.20.0 expo: '*' - expo-widgets: ^57.0.3 + expo-widgets: ^57.0.16 react-refresh: '>=0.14.0 <1.0.0' peerDependenciesMeta: '@babel/runtime': @@ -3240,8 +3547,8 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} - browserslist@4.28.5: - resolution: {integrity: sha512-Cu2E6QejHWzuDMTkuwgpABFgDfZrXLQq5V13YOACZx4mFAG4IwGTbTfHPMr4WtxlHoXSM8FIuRwYYCz5XiabaQ==} + browserslist@4.28.8: + resolution: {integrity: sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true @@ -3447,6 +3754,10 @@ packages: resolution: {integrity: sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==} engines: {node: '>= 0.10.0'} + content-type@2.1.0: + resolution: {integrity: sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==} + engines: {node: '>=18'} + content-type@3.0.0: resolution: {integrity: sha512-AIi5H6p0xk5uknXcN3/rmhP8jgp69OfSe/JuKiQAFprJ7UGw7mwj7m4XcmDzlrnJDG+cGpphAINGdU3g3g7kDw==} engines: {node: '>=22'} @@ -3490,11 +3801,12 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - core-js-compat@3.49.0: - resolution: {integrity: sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA==} + core-js-compat@3.50.0: + resolution: {integrity: sha512-XGpFGbMLHwSt74YLTKho7Ib242qi6O8MSX+sRokV4oz7iKXvQWGYZthjIhjRGMxjzVkAubBO512dKGYcefmX3Q==} + engines: {node: '>=6.4.0'} - core-js@3.49.0: - resolution: {integrity: sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==} + core-js@3.50.0: + resolution: {integrity: sha512-BRWgOLKkFeCgRudR6zrs8p9XJZcE14grzKMMssoYrk6krtuEZ7MTKPIY5RzOnqsEKIR9kst7wNzphttraT+Yqw==} cose-base@1.0.3: resolution: {integrity: sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==} @@ -3532,6 +3844,10 @@ packages: csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + culori@4.0.2: + resolution: {integrity: sha512-1+BhOB8ahCn4O0cep0Sh2l9KCOfOdY+BXJnKMHFFzDEouSr/el18QwXEMRlOj9UY5nCeA8UN3a/82rUWRBeyBw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + cytoscape-cose-bilkent@4.1.0: resolution: {integrity: sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==} peerDependencies: @@ -3796,8 +4112,8 @@ packages: ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} - electron-to-chromium@1.5.389: - resolution: {integrity: sha512-cEto7aeOqBfU1D+c5py5pE+ooscKE75JifxLBdFUZsqAxRS6y7kebtxAZvICszSl05gPjYHDTjY+lXpyGvpJbg==} + electron-to-chromium@1.5.420: + resolution: {integrity: sha512-2yD6XreGusOfNV+dUcvipJEXc3n/n7fgr7996aszTG+YY5E4mqM4tOq/3uhP129cazL9YHbVWSpc79ePotWtPA==} emoji-regex@10.6.0: resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} @@ -3805,6 +4121,10 @@ packages: emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + empathic@2.0.1: + resolution: {integrity: sha512-YGRs8knHhKHVShLkFET/rWAU8kmHbOV5LwN938RHI0pljAJ1Gf6SzXsSmRaEzcXTtOOmVqJ5+WtQPL5uigY50Q==} + engines: {node: '>=14'} + encodeurl@1.0.2: resolution: {integrity: sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==} engines: {node: '>= 0.8'} @@ -3813,8 +4133,8 @@ packages: resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} engines: {node: '>= 0.8'} - enhanced-resolve@5.24.3: - resolution: {integrity: sha512-PwKooW9JUzh5chmYfHM3IQl5OkK2u2Nm011MgeZrss3JmFraUx/fqrf78kk8GUMYoibx/14MdwTl/1WKkG7TpQ==} + enhanced-resolve@5.24.5: + resolution: {integrity: sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==} engines: {node: '>=10.13.0'} entities@6.0.1: @@ -3939,44 +4259,44 @@ packages: resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} engines: {node: '>=12.0.0'} - expo-asset@57.0.3: - resolution: {integrity: sha512-6E08JDuYrktY5pKNn2WiFBPBfj6bvFb++ccuBTijK8BUk/afSoy6n35h583LmC2YHgHTepWHHWNfr61nYcMd8Q==} + expo-asset@57.0.16: + resolution: {integrity: sha512-IBRfQdW3iFT+GOBERMZLZM1MUNyrjMgMskuD0elVZ1ae44858UFlTJkHO3f8vi+u5zuv7O7KofsiN8NMG/uWzw==} peerDependencies: expo: '*' react: '*' react-native: '*' - expo-constants@57.0.3: - resolution: {integrity: sha512-BghbZlzwFnA22BG0CWv6W29zx8w19FozYPfSeZ3HjMitoy4aAmF1FnFbbjYSmZz6HHXXTWOfqwobOEIaglfzxg==} + expo-constants@57.0.17: + resolution: {integrity: sha512-cPWYBKN1SEbg2lXg2f8VkePJqGZrJPLvVdQDCTfbFu9sQHO1M31Y1zILReUuGnmt/VKeUz40ze579vR00xGu/A==} peerDependencies: expo: '*' react-native: '*' - expo-file-system@57.0.0: - resolution: {integrity: sha512-G+eytNuNGMiS7dbdj1j0nAYMowXnNr1vpO+jss6nQvBlRxshcGBFMtk8xfc67ynz0MUVkzod7WwKO7Vf1iOaJw==} + expo-file-system@57.0.6: + resolution: {integrity: sha512-pm8PMYEW6BnVOCBJ7df9FcDmQtE1tqImuYphlfYe1ipQRLYtdCayRczJcbKIsnB3mqEyp4gC2gWmMbsAsAOjVg==} peerDependencies: expo: '*' react-native: '*' - expo-font@57.0.0: - resolution: {integrity: sha512-zF+J7WrNFjqyAADwdvDgkFEoIQv9DqcjJ57HVstNEH7/7Tx1ThPEhKraodEQOwSjMYWirP+4BYsVbdb+/Zr4QQ==} + expo-font@57.0.3: + resolution: {integrity: sha512-kiVUnc2A8vAvO2FfDJTsQa5BwmY+PAkof/1wRb5MOkcX1jtiaSTwz9gCUAyscMBFLundZDmZrLy1P7LZVC+NvA==} peerDependencies: expo: '*' react: '*' react-native: '*' - expo-keep-awake@57.0.0: - resolution: {integrity: sha512-WqEoyDNSmUeAI9Gu7UaWKDjOhfaV+jGcms7N0hh/EAr7sqJrI2s0HpLSC3P9cWfXFUZCL1zZjd22m/NbsJYCKg==} + expo-keep-awake@57.0.1: + resolution: {integrity: sha512-28lkFImeXTS+bhAjuCFV7w7tW5bXg27BJVrxv+nC/nyYa86qEa0oFeHwqol6ha5k4pdVDQgBF09GM4A1k76Ssg==} peerDependencies: expo: '*' react: '*' - expo-modules-autolinking@57.0.5: - resolution: {integrity: sha512-Vv2PVeEUN0/VW19ItkVA8LxAuH8SV8cjui0MKhJxCKrNyCAGGZU6sRsBJciYXR4uVxuXWKiepGW6E3k/c+Ytdg==} + expo-modules-autolinking@57.0.12: + resolution: {integrity: sha512-Q8KAlq37nLKsQ+HsS9NpQVpd5jCgqtu694TDUNHBUBpV9ViD82mRBh8Uug/h68RG9xnLS+kuL4nYaCuFRghHjg==} hasBin: true - expo-modules-core@57.0.3: - resolution: {integrity: sha512-fuD3DjPQvdaldtCJm2erV2/trPMrDJvEwPdz0RuxAQnduiNwZ3yxBoi81ZEKC5GBSJauMo53YXSwogsFagjwFQ==} + expo-modules-core@57.0.15: + resolution: {integrity: sha512-HBxPXsx3eVLLRjAY57/8ghKDWIxjaEbluKKYFIq1OHPJC85ZfHsawwc0SOU4Z/qu+mdLz7Pwt5IkkWimeNSH5A==} peerDependencies: react: '*' react-native: '*' @@ -3985,29 +4305,29 @@ packages: react-native-worklets: optional: true - expo-modules-jsi@57.0.1: - resolution: {integrity: sha512-dECN3pOFv+KrQcGrOhJKEBc1/ob7SBjTTYGRB1bc84zmQ65La0FSrAPxpvnEQf8g9giLB5y9RLqwGxHspjXigQ==} + expo-modules-jsi@57.0.7: + resolution: {integrity: sha512-/GOrTFPnCfg7+m6M1yOSEKNeNdHBCm0ju6aJ4XHlSmtE02nb7mWh1Cs6QsK2eB5Rz7hjbb356Nd7UimsFvN7Vg==} peerDependencies: react-native: '*' - expo-server@57.0.0: - resolution: {integrity: sha512-18byjwFbHVFrGzI4IH1o2MZiiYwvO/y3d+JL3sq50Lg3Id1titwIRMh1fjbHbpM+wP6x14KJY0Ers1UYsjGq8Q==} + expo-server@57.0.3: + resolution: {integrity: sha512-aK+LdKzauHSGmsOStZtyxdzv0zWssCkxTw3m4QuOhfDSJsZaMRTd9O41d8ixU/QfELTbaJ0oRNcF7JFV/7O9YQ==} engines: {node: '>=20.16.0'} - expo-splash-screen@57.0.2: - resolution: {integrity: sha512-xyfl+Twl5EiFKxYLKlofJi/r7mvdxwr/w9olUWYL4viEEpYCPCvj2YzZQtPptr/wMFe4G+X97W42sRWq6zdSfA==} + expo-splash-screen@57.0.8: + resolution: {integrity: sha512-BEsrKg4niYBZa5AFzWRyGqxA4ZemtBOPwbCy+C336iGYYPuZYLEgWJGmm2xD1gRfCdrM/TP+l2ONd86Zvk3KyA==} peerDependencies: expo: '*' - expo-status-bar@57.0.0: - resolution: {integrity: sha512-wq1fDVAjfrzCj67hOcvEkGpgRrb6xVI7oSA3bfsQ08SFk8il7vGixJq+xyEbtdcrI9CDiNKQrT3ZIjgnyGbbBA==} + expo-status-bar@57.0.1: + resolution: {integrity: sha512-Xwaq1gAoVRWx5dPG5VhT5RSbnI9OilhZnO5qoPBnUaBAa5VzRzfdS8q0/bsPt0jR2DKLtGuP0bQ6efMJ4RIMDg==} peerDependencies: expo: '*' react: '*' react-native: '*' - expo@57.0.4: - resolution: {integrity: sha512-5wW0SR6OnGUh+UPb3JWlFTAL22IgBcX36RqofjLAH2AuFqxAIpYIdwr9BYR9Wl3xyIe0t9lCoMQIZlAIZUNQvg==} + expo@57.0.19: + resolution: {integrity: sha512-oIoAIisim1DwS2Zjp0rcEY9cfcq1wU6KpkBfwwFhJWI5YHV6i/aIzTUMX+uf0Q0CD6AvMls7IZVh34o27NjaJA==} hasBin: true peerDependencies: '@expo/dom-webview': '*' @@ -4051,8 +4371,8 @@ packages: fast-string-width@3.0.2: resolution: {integrity: sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==} - fast-uri@3.1.6: - resolution: {integrity: sha512-7Ical1vFEMr0onbVzEDIreM22I4khW+fzyQPwvAFWBp1iwdshSZRsL4jjRvPG9JP1uiqMHRto+YU6R2/CzDz5Q==} + fast-uri@3.1.7: + resolution: {integrity: sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg==} fast-wrap-ansi@0.2.2: resolution: {integrity: sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==} @@ -4107,8 +4427,8 @@ packages: fontfaceobserver@2.3.0: resolution: {integrity: sha512-6FPvD/IVyT4ZlNe7Wcn5Fb/4ChigpucKYSvD6a+0iMoLn2inpo711eyIcKjmDtE5XNcgAkSH9uN/nfAeZzHEfg==} - framer-motion@13.1.1: - resolution: {integrity: sha512-B/xn2TPS4f61cEBLFjiYlQFnBZUW1YVj/LM+C+N4OP8Rs95VLEI2ot/RlfBg111la/EiyECFaJJi/A3FWA8MUA==} + framer-motion@13.2.0: + resolution: {integrity: sha512-9E33ebgMaO33w1nN/jEdW8z3/GO483fMi4rqbMG9rt83XgW9QLKRe4NcmJ8s+fQ3O34++UHrIQwlIWGIWTITjA==} peerDependencies: react: ^18.0.0 || ^19.0.0 react-dom: ^18.0.0 || ^19.0.0 @@ -4127,8 +4447,8 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] - fumadocs-core@16.15.4: - resolution: {integrity: sha512-kdOuM0tvHkLWajnDu73BmtryPuUq4xsu610ssl1YMAm9fYF7BRmvYxx2apHbyf2Lt+7w8OEiWDTmB8Ja3zqp4Q==} + fumadocs-core@16.15.5: + resolution: {integrity: sha512-U7aXspC2Jg9BS/sv5xMz8kesKNcRX9RZUp4cF74asgXVyTiu3zIjN7RXZ8HeCNHvKGXEJIZeN6omkZZAdjFsgQ==} peerDependencies: '@mdx-js/mdx': '*' '@mixedbread/sdk': 0.x.x @@ -4246,12 +4566,12 @@ packages: fumadocs-ui: optional: true - fumadocs-ui@16.15.4: - resolution: {integrity: sha512-MWtQyrTEEaxop7z8TPKTuBs3xMqxW32Sr9wBlbiPAm+MxmeOSU+bfRThrOJSAgvpXo99bbx3hxhNij0441pH8g==} + fumadocs-ui@16.15.5: + resolution: {integrity: sha512-nJFeia9HN1/cL/HULUUUaTSHtm0hojc+pLlanOpdQPkcCeFQqpk2Qf7IHj6T5TcT5g62Oo/IgE3uHTigcvWOUg==} peerDependencies: '@types/mdx': '*' '@types/react': '*' - fumadocs-core: 16.15.4 + fumadocs-core: 16.15.5 next: 16.x.x react: ^19.2.0 react-dom: ^19.2.0 @@ -4285,8 +4605,8 @@ packages: resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} engines: {node: '>=6'} - get-tsconfig@4.14.0: - resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==} + get-tsconfig@4.14.3: + resolution: {integrity: sha512-++QEw4DIY7WGoukz+/+A/8dGYPT9l9yIadnmSgZ8Rjr3YVSVDipQSO9CdnJo9ePqFqUUqh+wk9uIaoiAwsiPkA==} get-uri@8.0.1: resolution: {integrity: sha512-/5N/P4Lrh0p/mDwlDRi7Y1+P2o/OyzZI3l6Iz1Ov6XXwwm1y3RlZLuo3gVgML99djrEDtV980bBxSuOeHLk8ww==} @@ -4362,8 +4682,8 @@ packages: hastscript@9.0.1: resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==} - hermes-compiler@250829098.0.14: - resolution: {integrity: sha512-5meXwsZxgiqFaJjNzwjzI9IyUkuGGBisu+z9BvQWmGVpjH6nz11hgqkyxe4dl8UAdyIV4lTbz91+Dlnjz0VxqA==} + hermes-compiler@250829098.0.17: + resolution: {integrity: sha512-qG1PXzTEtriF6oQLZF3vyHhSMxOdW5h2TqqLri0rdpstPustd2fSvRZQMVAPdlhgFwBfYnj3OUZtiO6LjYsEFw==} hermes-estree@0.35.0: resolution: {integrity: sha512-xVx5Opwy8Oo1I5yGpVRhCvWL/iV3M+ylksSKVNlxxD90cpDpR/AR1jLYqK8HWihm065a6UI3HeyAmYzwS8NOOg==} @@ -4580,10 +4900,6 @@ packages: js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - js-yaml@4.3.0: - resolution: {integrity: sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==} - hasBin: true - js-yaml@4.3.2: resolution: {integrity: sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==} hasBin: true @@ -4650,6 +4966,12 @@ packages: cpu: [arm64] os: [android] + lightningcss-darwin-arm64@1.30.1: + resolution: {integrity: sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + lightningcss-darwin-arm64@1.32.0: resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} engines: {node: '>= 12.0.0'} @@ -4662,6 +4984,12 @@ packages: cpu: [arm64] os: [darwin] + lightningcss-darwin-x64@1.30.1: + resolution: {integrity: sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + lightningcss-darwin-x64@1.32.0: resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} engines: {node: '>= 12.0.0'} @@ -4674,6 +5002,12 @@ packages: cpu: [x64] os: [darwin] + lightningcss-freebsd-x64@1.30.1: + resolution: {integrity: sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + lightningcss-freebsd-x64@1.32.0: resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} engines: {node: '>= 12.0.0'} @@ -4686,6 +5020,12 @@ packages: cpu: [x64] os: [freebsd] + lightningcss-linux-arm-gnueabihf@1.30.1: + resolution: {integrity: sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + lightningcss-linux-arm-gnueabihf@1.32.0: resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} engines: {node: '>= 12.0.0'} @@ -4698,6 +5038,13 @@ packages: cpu: [arm] os: [linux] + lightningcss-linux-arm64-gnu@1.30.1: + resolution: {integrity: sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + lightningcss-linux-arm64-gnu@1.32.0: resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} engines: {node: '>= 12.0.0'} @@ -4712,6 +5059,13 @@ packages: os: [linux] libc: [glibc] + lightningcss-linux-arm64-musl@1.30.1: + resolution: {integrity: sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + lightningcss-linux-arm64-musl@1.32.0: resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} engines: {node: '>= 12.0.0'} @@ -4726,6 +5080,13 @@ packages: os: [linux] libc: [musl] + lightningcss-linux-x64-gnu@1.30.1: + resolution: {integrity: sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + lightningcss-linux-x64-gnu@1.32.0: resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} engines: {node: '>= 12.0.0'} @@ -4740,6 +5101,13 @@ packages: os: [linux] libc: [glibc] + lightningcss-linux-x64-musl@1.30.1: + resolution: {integrity: sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + lightningcss-linux-x64-musl@1.32.0: resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} engines: {node: '>= 12.0.0'} @@ -4754,6 +5122,12 @@ packages: os: [linux] libc: [musl] + lightningcss-win32-arm64-msvc@1.30.1: + resolution: {integrity: sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + lightningcss-win32-arm64-msvc@1.32.0: resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} engines: {node: '>= 12.0.0'} @@ -4766,6 +5140,12 @@ packages: cpu: [arm64] os: [win32] + lightningcss-win32-x64-msvc@1.30.1: + resolution: {integrity: sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + lightningcss-win32-x64-msvc@1.32.0: resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} engines: {node: '>= 12.0.0'} @@ -4778,6 +5158,10 @@ packages: cpu: [x64] os: [win32] + lightningcss@1.30.1: + resolution: {integrity: sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==} + engines: {node: '>= 12.0.0'} + lightningcss@1.32.0: resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} engines: {node: '>= 12.0.0'} @@ -4853,8 +5237,8 @@ packages: resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} engines: {node: '>=12'} - lucide-react@1.38.0: - resolution: {integrity: sha512-xZCyBd/wiVUDactoCc+42TjL0aB7EBOXsuX+tjz+W/sGzw2KhHpL1NOH3FIaVUcpimvUBpIYfz34Ofj9S5JEzQ==} + lucide-react@1.39.0: + resolution: {integrity: sha512-y8nXoEwvqqIsF927NBWXODa4bfMrcUeEb/9sgpwFqg0gUjgn3j5Hznk+v7STmPgZ2iQ11JKlbQGdFRuTOwvYkA==} peerDependencies: react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -4946,64 +5330,180 @@ packages: mermaid@11.17.2: resolution: {integrity: sha512-V6K3C8EBdEsPFZXSKMJe6ppQOENxuHARr9GvHX4hh47lAbhMRD9qf4oEK7LoaRQxULMa80/qt5gHO73aCleBBg==} - metro-babel-transformer@0.84.4: - resolution: {integrity: sha512-rvCfz8snl9h20VcvpOHxZuHP1SlAkv4HXbzw7nyyVwu6Eqo5PRerbakQ9XmUCOsRy70spJ37O+G1TK8oMzo48g==} + metro-babel-transformer@0.84.5: + resolution: {integrity: sha512-2WbHILKMiJUzfdjmGOQOqU1bWi9//gqiclc/tkk/AIsrrVw3efhZ1uhkOwMTxUEPOzqoo091H0olLmVZH5FHGQ==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + + metro-babel-transformer@0.84.6: + resolution: {integrity: sha512-B1ozl6KxFHbQjXZ2U8WiTPWXylqiD3V4EXyV5r0fqETOrqIrJlBvbVTD82UxIN+B3Vpe4l8G1tb15ocd2RyOQA==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + + metro-babel-transformer@0.87.0: + resolution: {integrity: sha512-IEn1K1FyY4J1sA5y6zqDjf2OkfmpTEqhZOeP6MJX8HepSW0cuHGw1m8bYOdv2adkG3XUE9dtM0csUs0gP/Xa5w==} + engines: {node: ^22.13.0 || ^24.3.0 || >= 26.0.0} + + metro-cache-key@0.84.5: + resolution: {integrity: sha512-3dPB2TnvGjjf0/9O7AXVQURKXuQNauTZE7WpTGTlR017Gh/B5y0m/2wcqxfveUguHSpu89KhVxCAlr2k/H7uhQ==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + + metro-cache-key@0.84.6: + resolution: {integrity: sha512-6tyXt1BZ/3U183XV48oif7Nm65TE+pTo0Vs31u4FxMgqiYQR/SiSr3RoXn3pmy4pd280ZmLmct0x3jZpu0pV4A==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + + metro-cache-key@0.87.0: + resolution: {integrity: sha512-Q+MPt6jl0zQogr4Q02WaJK6HY+GtE5A0nzj8kIV1Owgrx6OMNvm6scPTr1SM/R4LpCE8EH/Y5qfbXQ84GHTr0Q==} + engines: {node: ^22.13.0 || ^24.3.0 || >= 26.0.0} + + metro-cache@0.84.5: + resolution: {integrity: sha512-WHS0n2OxQqtwEjSeQFPePNrMvEFhmQcUQM9cRJMHByWoi/GMWFBEWOf7hVkAM/0KRutAXNbDlSu/cZB6CyxgQQ==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + + metro-cache@0.84.6: + resolution: {integrity: sha512-KBJVpb02oKNO+jlWQ1Aax7cd11YP0MClDaMgx6C4MhyZ9g95J/dC1Bo6sCzXaU9L9h/w5cQfT5zDkGW5sOj99A==} engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} - metro-cache-key@0.84.4: - resolution: {integrity: sha512-wVO79aGrkYImpnaVS4+d5RrRBRPX31QtvKB3wKGBuiNSznduZTQHzsrJZRroFJSwnygrzdsGUtDQPuqqFjFdvw==} + metro-cache@0.87.0: + resolution: {integrity: sha512-146vS1BMSKcp99jddOhFBfHwzUEWN35NrsnSJDF2sQQ0ZT5OsBcOjd574PM233TWEZISRJ5DOK+vokD+1ubx+w==} + engines: {node: ^22.13.0 || ^24.3.0 || >= 26.0.0} + + metro-config@0.84.5: + resolution: {integrity: sha512-zie+uN6oohscowi2S7ByU+wUw6CrT4ZxW9uAbONOObSxx86RGmnIAmjXHLkfmcdYoY7jzOPEbqcI6oeVmqyBQA==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + + metro-config@0.84.6: + resolution: {integrity: sha512-cD2mLEofcuV26kxSV4hfV1Pbuh6igdUhYQriO8ZVeJ7HyZfq4/wqHV0gSR7fEO8NUp9odr60/Kzm5YakmghWwQ==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + + metro-config@0.87.0: + resolution: {integrity: sha512-yZ9QAIzWH9MxwrzwRlX/CBGRWOT14l7klSDYg8hdtSdnoUs5A7MQRdHE2KB9iHVzGQW5wgWM5aXJswNeWbSQPA==} + engines: {node: ^22.13.0 || ^24.3.0 || >= 26.0.0} + + metro-core@0.84.5: + resolution: {integrity: sha512-xwm605hCi5Y6eJTTb8ZWo6pkUcoBEIyiQOfkZh5GwtDwUrP9SNhTQZhzJHrBCwwxlf3Ptl/pxWJgQ1rsNYMnrA==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + + metro-core@0.84.6: + resolution: {integrity: sha512-ZqYskM8+f3PFMosBDJd8DLqRvT1ltRGhWL/ZfTpBvf4SoadO30VVrdhAoHFVGz3G1CJpWa3fgZH7nnnX/ybllA==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + + metro-core@0.87.0: + resolution: {integrity: sha512-yW57+pCOHRC/CJZ99GA2PTd+30dORwDAjUPRCokj91IWW5In9Jwtt2FB5wACrGO8P0GHTyVHdTwDNyZsNndUbA==} + engines: {node: ^22.13.0 || ^24.3.0 || >= 26.0.0} + + metro-file-map@0.84.5: + resolution: {integrity: sha512-mlm/JL8toSbSc2akpKIGmzvrVRSCgZ5vkbycI34oMLoOnLGuLyC8WTyVJ6P0hZG/usDaGwZSl/s9BCRriqjGJA==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + + metro-file-map@0.84.6: + resolution: {integrity: sha512-ov9VywWBsHPt54Zc7/DxWvqjTwxuZvhfeyHg8ZAMGKtf2PcH1wS3EnoBH7In53Rspk/eXVWAO5+avxunPn081w==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + + metro-file-map@0.87.0: + resolution: {integrity: sha512-Dc57t8jsINwA90bbVlqaeDlxf1rVGgj5SmOEnOMbaHNUM/HCYYTJxPV8SRdOBwh7qTz/biO9vaQvBTjRBgbbsg==} + engines: {node: ^22.13.0 || ^24.3.0 || >= 26.0.0} + + metro-minify-terser@0.84.5: + resolution: {integrity: sha512-BJoFwCEDsYnagPqarayInv2+diCDNDdLlaof/p6s9w4gh+gc9HXYM+pDvsKGKKUumpZswNF3Z/ftTMqKl/5IBg==} engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} - metro-cache@0.84.4: - resolution: {integrity: sha512-gpcFQdSLUwUCk71saKoE64jLFbx2nwTfVCcPSULMNT8QYq0p1eZZE29Jvd0HtT/UlhC3ZOutLxJME5xqD2JUZg==} + metro-minify-terser@0.84.6: + resolution: {integrity: sha512-SqhB/Kxrw0XfyW0k8mEscqX/CQopm6Fp3EpdSIPzvKfGgQp9bTMahg9PAdVsXUmIWwErbmFw1VqB+VFBOKksUw==} engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} - metro-config@0.84.4: - resolution: {integrity: sha512-PMotGDjXcXLWo2TMRH+VR99phFNgYTwqh4OoieIKK3yTJa1Jmkl+fZJxDO0jfBvNF+WESHciHvpNuBtXaF3B0Q==} + metro-minify-terser@0.87.0: + resolution: {integrity: sha512-tPa0O983PDutFu3LXbArRH5NduogcKrvW6fs9VHhksTKUA1iqDyoD1ZSj/Me52xJ6T9/9pOwIVyj9ulKc/zMkg==} + engines: {node: ^22.13.0 || ^24.3.0 || >= 26.0.0} + + metro-resolver@0.84.5: + resolution: {integrity: sha512-VSSnepg1k6LyCwtb6eirWdAWlpKwBG8Rdtsr1mU38rMelFyWgh3/QuMSiZIZAIjwg/fsa8GhW5/FO54CAUPCEA==} engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} - metro-core@0.84.4: - resolution: {integrity: sha512-HONpWC5LGXZn3ffkd4Hu6AIrfE7j4Z0g0wMo/goV24WOB3lhuFZ40KgvaDiSw8iyQHloMYay5N/wPX+z8oN/PQ==} + metro-resolver@0.84.6: + resolution: {integrity: sha512-PplpGc/OCLxgKeLNsLQO/VHsYDIvkcGGr3zNT7UanHCGRqhxKv245o+naV8cRI4/pcLqyc+9j+J0AJbT3Kp5Sg==} engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} - metro-file-map@0.84.4: - resolution: {integrity: sha512-KSVDi/u60hKPx++NLu3MTIvyjzNoJnFAF8PQFxaj1jiSka/wjw+Ua6sNuJ0TDHQv+7AAoFQxeMgaRAe8Yic5wQ==} + metro-resolver@0.87.0: + resolution: {integrity: sha512-Xl3M9R3KToaHJvXlI2lSOxtYHitzxite+195DSi00HL9PcS7Xik5+3xlRjfkKb3FA86SZxYPj+WJwlcfgaZoxg==} + engines: {node: ^22.13.0 || ^24.3.0 || >= 26.0.0} + + metro-runtime@0.84.5: + resolution: {integrity: sha512-U1m2+d1Pr+JO2/iVXBB2OfXXityz7tqwIorxfrT15IEgaHvpJBq/OHiqnOWPKJbUl3JcxjcdviZZOKk85oK4Qg==} engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} - metro-minify-terser@0.84.4: - resolution: {integrity: sha512-5qpbaVOMC7CPitIpuewzVeGw7E+C3ykbv2mqTjQLl85Z3annSVGlSCTcsZjqXZzjupfK4Ztj3dDc4kc44NZwtQ==} + metro-runtime@0.84.6: + resolution: {integrity: sha512-47EYO/DOai0PA3GoAyDpyXOgHNMwbl/Z9dQlQineDLHkpD42xOwMrIx1crT0+n6F+aGoEyxZmeFwfMzaIqVqKQ==} engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} - metro-resolver@0.84.4: - resolution: {integrity: sha512-1qLgbxQ5ZGhhutuPot1Yp348ofDsATL2WkrHF65TobqTT9K3P9qJXw38bomk7ncp5B7OYMfWwtyBZo1lCV792A==} + metro-runtime@0.87.0: + resolution: {integrity: sha512-XsXZkgEwI0ZMYSBfvOMAbenzwa60XlObXJ27g6/Khgrz9ESbiBbAsd7hR62G2jRBYOhGAzG61Gk22dpRJj/mdw==} + engines: {node: ^22.13.0 || ^24.3.0 || >= 26.0.0} + + metro-source-map@0.84.5: + resolution: {integrity: sha512-2BtV5L9uPc49F13Gn5wiP6bX/EncqzqTIk2VL/0F/96Vo0YEOjluT/qktQjFODfqGFsucwnh5mPEAl/2jVEfeg==} engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} - metro-runtime@0.84.4: - resolution: {integrity: sha512-Jibypds4g7AhzdRKY+kDoj51s5EXMwgyp5ddtlreDAsWefMdOx+agWqgm0H2XSZ/ueanHHVM89fnf5OJnlxa8Q==} + metro-source-map@0.84.6: + resolution: {integrity: sha512-4p/EplRbgNxhdqA6kAj1AmIq9qqYXYSAjgxuZ73rYA+p037xDDrxK8CyDKNfbn1k6aMuVt8LV0fpt7NHUTKiZw==} engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} - metro-source-map@0.84.4: - resolution: {integrity: sha512-jbWkPxIesVuo1IWkvezmMJld6iu8nD62GsrZiV6jP37AOdbo4OBq1FJ+qkOg8sV05wAHB//jAbziuW0SlJfW4g==} + metro-source-map@0.87.0: + resolution: {integrity: sha512-31BrYqu1c2co93rF1LN9Pw+7g+BrfDyxJkNQWrYm+pfA/+eVYVumF7tFMHbXLePLmHfhvSgVvbK6su1Oyiw1ng==} + engines: {node: ^22.13.0 || ^24.3.0 || >= 26.0.0} + + metro-symbolicate@0.84.5: + resolution: {integrity: sha512-rQ40zYDAkaWBN9yvjUuAD0ZpzBMZSoKyGYXnb5JrfbKjun7fTvfoLHL3KXFYenBTYZkQtlp4cKSCv/1utxFyOw==} engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + hasBin: true - metro-symbolicate@0.84.4: - resolution: {integrity: sha512-OnfpacxUqGPZQ27t8qK9mFa7uqHIlVWeqRqkCbvMvreEBiamEeOn8krKtcwgP5M4cYDPwuSmCTopHMVthqG4zA==} + metro-symbolicate@0.84.6: + resolution: {integrity: sha512-wJFyyF5ysbVyoYVWGL6GwVMiNnGllwDU2gF096376fl0fc8IFUlADSyNlswj31K9og2sOilXe/FdyWFPczPUYg==} engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} hasBin: true - metro-transform-plugins@0.84.4: - resolution: {integrity: sha512-kehr6HbAecqD0/a3xLXobELdPaAmRAl8bel0qagPF4vhZtux93nS8S4eq2kgKt6J2GnQpVjSoW1PXdst04mwow==} + metro-symbolicate@0.87.0: + resolution: {integrity: sha512-uOpTxAXu74N+RSujUZ78L6gjI6bDdnz6XuW+AIUNuubZDEQPIpaX0StzIb0GMeQWP6zfHOHwFrWPP5Iquu1GXw==} + engines: {node: ^22.13.0 || ^24.3.0 || >= 26.0.0} + hasBin: true + + metro-transform-plugins@0.84.5: + resolution: {integrity: sha512-+InaSVGaOyt0DyRo4Y/zIdPI6CZwnbNho5LAL23tgmuGwv7fyfkF7kKfPjZcfxXBcoYdTLLFnCfCH/dHSiCqNg==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + + metro-transform-plugins@0.84.6: + resolution: {integrity: sha512-pROPMFaj25Y9+3LlLqpBRz/7B/2YPnfEQz/z3juYxkh3FGQ+3c/Qh4E+khPSTdwTw0M6LitLYHMqaHMbQmuBJw==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + + metro-transform-plugins@0.87.0: + resolution: {integrity: sha512-i8keUe9+BaSwMuQM26DGheElCpTtflAKIrSwJAm8ZsgDb50RAUQus+e6zt2suaJXJ1OxZa7vqgtqoZxdniM6Fw==} + engines: {node: ^22.13.0 || ^24.3.0 || >= 26.0.0} + + metro-transform-worker@0.84.5: + resolution: {integrity: sha512-ui1Z8x4s5RL36gMmKLaMMO7O9NNDHNdthEZSCDQHAau3JcAsTaFOK6I+2q4I/kW5u8hSEjJk9L45TXSVJw6g1A==} engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} - metro-transform-worker@0.84.4: - resolution: {integrity: sha512-W1IYMvvXTu4MxYr7d9h7CeG2vpIr3bmLLIavkPY4O1ilzDrvS8z/NEe6y+pC44Ff7raMXQgYSfdqDUwN/i39gg==} + metro-transform-worker@0.84.6: + resolution: {integrity: sha512-xLTVrOENaHR9DiuhnHS+zcle0rKsCy2CpXSIx7xD+zhJs+qVRNUyV7Z1zIt/0P/d0cSGVx846R+dIjHEudhcww==} engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} - metro@0.84.4: - resolution: {integrity: sha512-8ETTubqfD6ornDy2zYDvRcKnVDOXdFJsjetYDBsY4oAsb6NJkiwFR+FaMESyGppFmQUyBQA4H4sFGxzcQSGtFA==} + metro-transform-worker@0.87.0: + resolution: {integrity: sha512-YftLzNJxCTYxEN5k4AzR8KYwiENTEuz30L+4QeoMrtDd+U8mDThZg/ArR3JVRd8LaikwPOjVAS5SP3xPJN0AaA==} + engines: {node: ^22.13.0 || ^24.3.0 || >= 26.0.0} + + metro@0.84.5: + resolution: {integrity: sha512-r1liLkyFZMVSEMNjU1CJU5pRzs3NdkxHqXS60O25c0rCIqAR+cGk7rPydw/g0WAIKVXojIBIF45yYBPagJGcgw==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + hasBin: true + + metro@0.84.6: + resolution: {integrity: sha512-ty/tx/imE5Ph2gvPU788Ckim6RAC2tyZxLz2hWRgC9U7/fGKV7qY/slADy0qHPEw9cpUgc4BfmV3oUsYwm7oeg==} engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} hasBin: true + metro@0.87.0: + resolution: {integrity: sha512-fRqFhSzQhLNQSCvJFeuRzBRXAOOKXf1O8d2cvmMtG6yFR0jCllQ7vBsXoLP18yuqtf+N1XwWXTPF11eWy9q6dQ==} + engines: {node: ^22.13.0 || ^24.3.0 || >= 26.0.0} + hasBin: true + micromark-core-commonmark@2.0.3: resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} @@ -5159,14 +5659,14 @@ packages: engines: {node: '>=10'} hasBin: true - motion-dom@13.1.1: - resolution: {integrity: sha512-XSf8VYWSB6G/0IY3rWVbyLcxWXtAVHkN1PQE2agTaCv3u8RGvbwu56TyyR/MNzBqqNavEBTZzErcxI1TxBrjcA==} + motion-dom@13.2.0: + resolution: {integrity: sha512-N6gdSoWRDk0Rh/fVtlqUtLs+fEN3ELFZI3cn3IQE9Mnf3E+Mh8wjO6MstzCOPFh4Yf0L1as5m2eUyYWj8ylVSQ==} motion-utils@13.0.0: resolution: {integrity: sha512-7DnN7TmbLcYXcG4RVadXIihWlyuM9afoUww8Y5Agg431kGKiuL2/OMyP4mJ5wLz+pvN3t5ySClLOaVXJ+wekRQ==} - motion@13.1.1: - resolution: {integrity: sha512-WNZoK6xiF+kkTqkZ5K7FDDh6A8BG4i5Hc7KXtW8gtTxkpJFds+hIOrDaQGKjQj/AE/i4hJqAaUHEqp/Qo02y6Q==} + motion@13.2.0: + resolution: {integrity: sha512-4Hrb5vD6HhjFstLUiCmWvtpsw+WTpP4R+QXfSDYZBz7+uxE/LrRg3aV0ReJxHrTRffHhbIE6svEqnotngXvesQ==} peerDependencies: react: ^18.0.0 || ^19.0.0 react-dom: ^18.0.0 || ^19.0.0 @@ -5182,23 +5682,13 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - multitars@1.0.0: - resolution: {integrity: sha512-H/J4fMLedtudftaYMOg7ajzLYgT3/rwbWVJbqr/iUgB8DQztn38ys5HOqI1CzSxx8QhXXwOOnnBvd4v3jG5+Mg==} + multitars@1.0.2: + resolution: {integrity: sha512-6GwVw5eLi9sThdtlS4PKwC7yRLaf45pYhIEzKBHdKxi+YOXGKFX8acIniH+Uh/+k9mS2lQOupTccjoe5r0/1IQ==} mute-stream@3.0.0: resolution: {integrity: sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==} engines: {node: ^20.17.0 || >=22.9.0} - nanoid@3.3.15: - resolution: {integrity: sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true - - nanoid@3.3.16: - resolution: {integrity: sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true - nanoid@3.3.18: resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -5212,9 +5702,9 @@ packages: resolution: {integrity: sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==} engines: {node: '>= 0.6'} - negotiator@1.0.0: - resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} - engines: {node: '>= 0.6'} + negotiator@1.1.0: + resolution: {integrity: sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg==} + engines: {node: '>=18'} netmask@2.1.1: resolution: {integrity: sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA==} @@ -5230,8 +5720,8 @@ packages: react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc - next@16.3.3: - resolution: {integrity: sha512-tuRTx1nQ/yVw83cwJBo9F+njGUgMn3UHQycreWHB8XsStvvAh1AthbI8/4IpKnFaF58F+iSiHejYOlMQ/eq83g==} + next@16.3.4: + resolution: {integrity: sha512-/Ztf6CeRH+ejEXUrYtqI4gkS66eFIHuSwqi60RgcpWKodxFZx2/dqVCMKBwILfAHXQ+F1b1vAudgj3mnxqtoIA==} engines: {node: '>=20.9.0'} hasBin: true peerDependencies: @@ -5267,8 +5757,8 @@ packages: node-int64@0.4.0: resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} - node-releases@2.0.51: - resolution: {integrity: sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==} + node-releases@2.0.54: + resolution: {integrity: sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ==} engines: {node: '>=18'} npm-package-arg@11.0.3: @@ -5282,10 +5772,18 @@ packages: nullthrows@1.1.1: resolution: {integrity: sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==} - ob1@0.84.4: - resolution: {integrity: sha512-eJXMpz4aQHXF/YBB9ddqZDIS+ooO91hObo9FoW/xBkr54/zCwYYCDqT/O54vNo8kOkWs5Ou/y28NgdrV0edQNA==} + ob1@0.84.5: + resolution: {integrity: sha512-aH9RkoZc7w/90HBamFxTw8ZLFr05wXS+iOnvmrgo53Ep8Pyrm5FieQSaPIVROkfFVQISeD/zo92fes26TOwe+A==} + engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + + ob1@0.84.6: + resolution: {integrity: sha512-+s+6zjd0X68hfAcn92NsEPXnSYOF3O7exw/Fj4UfRTB+UL+Tdjdkzu5/4WquXlzqgvNm8FWuuW6/0Yd8S2rYxw==} engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} + ob1@0.87.0: + resolution: {integrity: sha512-8Q8sKCiUwsxgSmjDtVWyRgmxsgeJXXam3oQH6Id8ADfNaJMV6GZKyeAl8+pGVVdgwAZabfk5+aExle7AP/nZiA==} + engines: {node: ^22.13.0 || ^24.3.0 || >= 26.0.0} + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -5343,8 +5841,8 @@ packages: resolution: {integrity: sha512-/HfRU/lPPr4T2VigM+cvM3cU77es+XF4OEAa4aE5zpdvrxHGD2NmH0AFIWpMNAb+CsZL45rlcIO49Re0ZcRseg==} engines: {node: '>=20'} - oxfmt@0.65.0: - resolution: {integrity: sha512-SgS5VgnP42T0zl3zWD+xoH8FCqg1SAFnSRoOT/qeoa6gxcYIqrDMOmcXIg/EWSN92Du4ogB4riuKhKd6Y4CGhw==} + oxfmt@0.66.0: + resolution: {integrity: sha512-FfvqR8RFtV6JJpRrpkfqyVCQ7HDvZ/VriWFx7veftCgL1B5ZO9qNr+1rvPieycMQnNfVG0PWyJQiy7p0hq1I5w==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -5356,8 +5854,8 @@ packages: vite-plus: optional: true - oxlint@1.80.0: - resolution: {integrity: sha512-5nTiSps4qdbCWLbxzuO00alHkEO2exR9YMN/ig6QXWrLsYSG0KaObOAM+l6oU2LcKPWoSAGYbkZIGEu1ViiWKA==} + oxlint@1.81.0: + resolution: {integrity: sha512-HyrJYqeoOCL0iqaLEzGewGT48ZX99P3hxYh8udAF9RGGIghSamkXE4ClUyBpEDNqasamThgmlPbuMOe7SAZmHg==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -5469,10 +5967,6 @@ packages: resolution: {integrity: sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==} engines: {node: ^10 || ^12 || >=14} - postcss@8.5.25: - resolution: {integrity: sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==} - engines: {node: ^10 || ^12 || >=14} - postcss@8.5.26: resolution: {integrity: sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==} engines: {node: ^10 || ^12 || >=14} @@ -5485,8 +5979,8 @@ packages: resolution: {integrity: sha512-C+y9x90UElAddDZmV4qOx9W53B61PO7cIqWz2dQsWlwswuq4mr8NEwytdGKboYbQlGZ3awrkTeNvcZiZNHnQ8A==} engines: {node: '>=20'} - prettier@3.9.5: - resolution: {integrity: sha512-/FVl766LpUfB5vXgCYOYa0MeV/441Ia99AeICQIQFTY/Nw0roZwULcXpku5i1/m5kt/baz+s4Zogspd839HSMg==} + prettier@3.9.6: + resolution: {integrity: sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==} engines: {node: '>=14'} hasBin: true @@ -5494,8 +5988,8 @@ packages: resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - pretty-format@30.4.1: - resolution: {integrity: sha512-K6KiKMHTL4jjX4u3Kir2EW07nRfcqVTXIImx50wbjHQTcZPgg+gjVeNTIT3l3L1Rd4UefxfogquC9J37SoFyyw==} + pretty-format@30.5.1: + resolution: {integrity: sha512-byhRAPguVKMQIj4kjJwJ5lAskVhfuiSdiYl/aLTWpgkGEmic2jYhJh1yE9ih8Ox44Xg9ccCT11/S6QrzSJuNrg==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} proc-log@4.2.0: @@ -5567,6 +6061,11 @@ packages: peerDependencies: react: ^19.2.3 + react-dom@19.2.8: + resolution: {integrity: sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==} + peerDependencies: + react: ^19.2.8 + react-freeze@1.0.4: resolution: {integrity: sha512-r4F0Sec0BLxWicc7HEyo2x3/2icUTrRmDjaaRyzzn+7aDyFZliszMDOgLVwSnQnYENOlL1o569Ze2HZefk8clA==} engines: {node: '>=10'} @@ -5576,9 +6075,6 @@ packages: react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} - react-is@19.2.7: - resolution: {integrity: sha512-kZFnouyVv7eP/Phmrlo9FK+zcAdriZJvzxXHF1Sl1P377WSGe2G/JxVolhTrB/jeV47lKImhNUsijjHAAbcl/A==} - react-is@19.2.8: resolution: {integrity: sha512-s5un28nYxKJw5gvUHyW5PCC28CvBqLu9r3cWgzHT4Vo/5fqqkFcdRYsGcKf50WMPpjjFZS5d76fn3YCo2njKwQ==} @@ -5594,11 +6090,11 @@ packages: react: '*' react-native: '*' - react-native-screens@4.25.2: - resolution: {integrity: sha512-1Nj1fusFd+rIMKU/qC9yGKVG+3ofh11d3OdBQKL1iVvQfKvcB8vhvTGQf2TkfxW3bamxN+hCZIXmNuU0mRkyDg==} + react-native-screens@4.26.2: + resolution: {integrity: sha512-2XnWsZToKj76trGtEZzx5ELD/qOICFEprEeUntImmitQFVUkea27fiWdUSITArI356Y1qynpXZINW+Unbhky/A==} peerDependencies: react: '*' - react-native: '>=0.82.0' + react-native: '*' react-native-unistyles@3.3.0: resolution: {integrity: sha512-TpI60y3THD9+alEZThrJ+bdrZ6Ambsg8SLy34hX+Jkl3m+ubZR7fasbSC7mt9skJ33JfyJ/Hi22AgsfP9cf5jw==} @@ -5622,12 +6118,12 @@ packages: react: ^18.0.0 || ^19.0.0 react-dom: ^18.0.0 || ^19.0.0 - react-native@0.86.0: - resolution: {integrity: sha512-17ALh/dd6AO4pgOVmOO5Axll5PbErEo3XFyLokyzW6usyi+OShIEPwUW26wLPlhVifgSOIfECCH0WN+0IqtJ1w==} + react-native@0.86.3: + resolution: {integrity: sha512-JR5s3bM9ezud+Mw24GlNXNfthqPIKwrQgPPJcam+L97t2sKjjEavhCzBn+fyqZZRcM5+XlhYxpTxVkK7e1n38Q==} engines: {node: ^20.19.4 || ^22.13.0 || ^24.3.0 || >= 25.0.0} hasBin: true peerDependencies: - '@react-native/jest-preset': 0.86.0 + '@react-native/jest-preset': 0.86.3 '@types/react': ^19.1.1 react: ^19.2.3 peerDependenciesMeta: @@ -5636,6 +6132,17 @@ packages: '@types/react': optional: true + react-native@0.87.1: + resolution: {integrity: sha512-DJKG6ANoD7BtrE4z9DewiSD7/RxCX73lK5Pu49aUr85P3333Dm2roTiP0rRjQNDZdVizHSOmstWfwF/o9EjCRA==} + engines: {node: ^22.13.0 || ^24.3.0 || >= 26.0.0} + hasBin: true + peerDependencies: + '@types/react': ^19.1.1 + react: ^19.2.3 + peerDependenciesMeta: + '@types/react': + optional: true + react-refresh@0.14.2: resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==} engines: {node: '>=0.10.0'} @@ -5670,10 +6177,19 @@ packages: '@types/react': optional: true + react-test-renderer@19.2.8: + resolution: {integrity: sha512-GHKPaDRaNYU24PHTLG8Bx8VMY9t+qNfxQbt/Yjp7aMWBkKU6766SR0n6TnYu7P5I1MfEuAMUadqiyDHyI4Yy9Q==} + peerDependencies: + react: ^19.2.8 + react@19.2.3: resolution: {integrity: sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==} engines: {node: '>=0.10.0'} + react@19.2.8: + resolution: {integrity: sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==} + engines: {node: '>=0.10.0'} + readable-stream@3.6.2: resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} engines: {node: '>= 6'} @@ -5801,8 +6317,8 @@ packages: robust-predicates@3.0.3: resolution: {integrity: sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==} - rolldown@1.1.5: - resolution: {integrity: sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==} + rolldown@1.2.7: + resolution: {integrity: sha512-g0EtLvBjTUB7jhyV0S/TCup3v/XSVl45vUIGbOGU4QPiyjTenCe4mKuFvW9fEgYmS2Fo42AUssRmNuMziXdrig==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true @@ -5845,8 +6361,13 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - sax@1.6.0: - resolution: {integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==} + sandbox-cli-detector@0.2.0: + resolution: {integrity: sha512-4lyHX0ZU0AZKwjgZ1InxZAa3PNpyEb8rOQ+Zss1ReYmhNzW0Q+h1zE5nvniXN0HaAWZaZE1zgVNEirb0R7LmNg==} + engines: {node: '>=18.18'} + hasBin: true + + sax@1.6.1: + resolution: {integrity: sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==} engines: {node: '>=11.0.0'} scheduler@0.27.0: @@ -5903,14 +6424,10 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} - shell-quote@1.9.0: - resolution: {integrity: sha512-Iov+JwFv/2HcTpcwNMKd8+IWNb8tboQJNQTkAY/LLVK7gGH9jy+LGkVqPxfekHl+yMmiqXszdGWXgkfml7hjqA==} + shell-quote@1.10.0: + resolution: {integrity: sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA==} engines: {node: '>= 0.4'} - shiki@4.3.1: - resolution: {integrity: sha512-oR+qDVi2OjX1tmDpyv+3KviX01KzO6Af+0NNnKnsp9491UEGz2YpxTuJboS/6VhYpTdqzmuJBuiTlrAWWJAssw==} - engines: {node: '>=20'} - shiki@4.4.3: resolution: {integrity: sha512-Mb/GvXPHBAXdgGIcnfU5L3ldpn1XcxrGkPHwqgRx17/I2XRfqlFKk2vGkHWINn1kdXvzJZeuO3is6I9KLPFm0g==} engines: {node: '>=20'} @@ -5946,8 +6463,8 @@ packages: resolution: {integrity: sha512-WlMj/67cEJ6MDI1OcsnjuYKDNDoyPCCYZ249kuuXPiMDw9F8PXkVaQ7YWu3siTydfQ/4BEZcvGzu+aYvz7dDCQ==} engines: {node: '>= 20'} - socks@2.8.9: - resolution: {integrity: sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==} + socks@2.8.10: + resolution: {integrity: sha512-e0VyvkVTwVYViNovRkZ9aodhxVlyoMn7eJhVUPxZ+eK9P/7CBkxvvsBOHqFPEH416726W8tLXXXjKwqgTErrCQ==} engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} source-map-js@1.2.1: @@ -5986,8 +6503,10 @@ packages: resolution: {integrity: sha512-WjlahMgHmCJpqzU8bIBy4qtsZdU9lRlcZE3Lvyej6t4tuOuv1vk57OW3MBrj6hXBFx/nNoC9MPMTcr5YA7NQbg==} engines: {node: '>=6'} - standard-navigation@0.0.7: - resolution: {integrity: sha512-NCGLCNyuXrFOkGHxdNZFnpsehGtiq1oXbPhKl7ZuxFO5J//H2evqqOchmD4YwEUJnkjO4kH9Xp4hQX6hdAYCKQ==} + standard-navigation@0.0.8: + resolution: {integrity: sha512-TyVbo7INUDWtsUWDFn8RR7kwR87U0S4xHfLfbbnyeC581TmmyqQ+eM+nPw8rQTSD8QitRVcYfPaSHr/QJiUy1g==} + peerDependencies: + react: '*' statuses@1.5.0: resolution: {integrity: sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==} @@ -6112,16 +6631,17 @@ packages: resolution: {integrity: sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==} engines: {node: '>=8'} - terser@5.49.0: - resolution: {integrity: sha512-SNiDnXyHSrxVcIOtVbULzcTmniUiwcV7Nwdyj1twVubeTmbjoa8p69KKDpfkdoOavuM4/GRm1+ykI8qqnavHoA==} + terser@5.51.2: + resolution: {integrity: sha512-bWnjSNscmuI+GJze6ZupnHP8G/cTcsJF+bXCeQknk2SHQsgbNJnLrqiH9jZ2W4STPVXH2mDKKRX3iwPhc9Cn/Q==} engines: {node: '>=10'} hasBin: true throat@5.0.0: resolution: {integrity: sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==} - tinybench@2.9.0: - resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + tinybench@6.1.4: + resolution: {integrity: sha512-9APumHG7r4yOk4X4WlkmE71aZcv1gvin1czO3OQ1U9iJcFA5Ja/ygyb0vPOVHTthFozUYs8CLoLUlM8grb2lTQ==} + engines: {node: '>=20.0.0'} tinyexec@1.3.0: resolution: {integrity: sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==} @@ -6135,10 +6655,6 @@ packages: resolution: {integrity: sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==} engines: {node: ^20.0.0 || >=22.0.0} - tinyrainbow@3.1.1: - resolution: {integrity: sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==} - engines: {node: '>=14.0.0'} - tmpl@1.0.5: resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} @@ -6166,9 +6682,6 @@ packages: resolution: {integrity: sha512-JfJeIHke7y2egdGGgRAvpCwYFUsHlM2gPcrVOxFkznt/4uzQ7HFmvE63iFHVLBJNDuyDOQgijDK/tXH/f6Msjg==} engines: {node: '>=6.10'} - ts-morph@27.0.2: - resolution: {integrity: sha512-fhUhgeljcrdZ+9DZND1De1029PrE+cMkIP7ooqkLRTrRLTqcki2AstsyJm0vRNbTbVCNJ0idGlbBrfqc7/nA8w==} - ts-morph@28.0.0: resolution: {integrity: sha512-Wp3tnZ2bzwxyTZMtgWVzXDfm7lB1Drz+y9DmmYH/L702PQhPyVrp3pkou3yIz4qjS14GY9kcpmLiOOMvl8oG1g==} @@ -6204,12 +6717,17 @@ packages: engines: {node: '>=14.17'} hasBin: true + typescript@7.0.2: + resolution: {integrity: sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==} + engines: {node: '>=16.20.0'} + hasBin: true + ua-parser-js@1.0.41: resolution: {integrity: sha512-LbBDqdIC5s8iROCUjMbW1f5dJQTEFB1+KO9ogbvlb3nm9n4YHa5p4KTvFPWvh2Hs8gZMBuiB1/8+pdfe/tDPug==} hasBin: true - undici-types@7.18.2: - resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + undici-types@8.3.0: + resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==} undici@7.29.0: resolution: {integrity: sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==} @@ -6258,6 +6776,23 @@ packages: universal-user-agent@7.0.3: resolution: {integrity: sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==} + uniwind@1.12.0: + resolution: {integrity: sha512-/A2Mhq8IExb8dYrRotNWJ36FfbWbLSaDKQCy1ebrdKwh/UGzte5J83/Z/Efi6Q2fz/+yXzFrx8j69jm+YYz1lg==} + hasBin: true + peerDependencies: + '@expo/metro-config': '*' + metro: '*' + metro-cache: '*' + metro-transform-worker: '*' + react: '>=19.0.0' + react-native: '>=0.81.0' + tailwindcss: '>=4' + peerDependenciesMeta: + '@expo/metro-config': + optional: true + metro-transform-worker: + optional: true + unpipe@1.0.0: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} @@ -6266,8 +6801,8 @@ packages: resolution: {integrity: sha512-gwXJnPRewT4rT7sBi/IvxKTjsms7jX7QIDLOClApuZwR49SXbrB1z2NLUZ+vDHyqCj/n58OzRRqaW+B8OZi8vg==} engines: {node: '>=18.12.0'} - update-browserslist-db@1.2.3: - resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + update-browserslist-db@1.3.2: + resolution: {integrity: sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' @@ -6339,13 +6874,13 @@ packages: vfile@6.0.3: resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} - vite@8.1.4: - resolution: {integrity: sha512-bTT9PsdWO+MQMNG9ZXIP/qM9wGh37DFxTV/sPq9cFpHr3w4jkgef032PkAL9jAqhk3Nz8NQw3O8n6/xFkqO4QQ==} + vite@8.2.2: + resolution: {integrity: sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: '@types/node': ^20.19.0 || >=22.12.0 - '@vitejs/devtools': ^0.3.0 + '@vitejs/devtools': ^0.4.0 || ^0.5.0 esbuild: ^0.27.0 || ^0.28.0 jiti: '>=1.21.0' less: ^4.0.0 @@ -6382,23 +6917,23 @@ packages: yaml: optional: true - vitest@4.1.11: - resolution: {integrity: sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==} - engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + vitest@5.0.0: + resolution: {integrity: sha512-gpsMNoRhMjMktVxPtstOH4/PJuPyovVaMDr4oDilXaGH1EcqM2OE96SoHT2VIQ6fTGtTjqmHDrEu2X9RQiXf8Q==} + engines: {node: ^22.12.0 || ^24.0.0 || >=26.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@opentelemetry/api': ^1.9.0 - '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 - '@vitest/browser-playwright': 4.1.11 - '@vitest/browser-preview': 4.1.11 - '@vitest/browser-webdriverio': 4.1.11 - '@vitest/coverage-istanbul': 4.1.11 - '@vitest/coverage-v8': 4.1.11 - '@vitest/ui': 4.1.11 + '@types/node': ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 5.0.0 + '@vitest/browser-preview': 5.0.0 + '@vitest/browser-webdriverio': ^5.0.0-beta.5 || >=5.0.0 + '@vitest/coverage-istanbul': 5.0.0 + '@vitest/coverage-v8': 5.0.0 + '@vitest/ui': 5.0.0 happy-dom: '*' jsdom: '*' - vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + vite: ^6.4.0 || ^7.0.0 || ^8.0.0 peerDependenciesMeta: '@edge-runtime/vm': optional: true @@ -6479,8 +7014,8 @@ packages: resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} engines: {node: '>=18'} - ws@7.5.11: - resolution: {integrity: sha512-zS54Oen9bITtp7kp2XM3AydrCIq1D+HwJOuH+c+e4LfpL/lotP5osijd+UoMnxwAam1GN8R4KtLAyIrIcBNpiA==} + ws@7.5.13: + resolution: {integrity: sha512-rsKI6xDBFVf4r/x8XyChGK04QR/XHroxs/jUcoWvtEZM8TPU/X/uIY9B1CsSzYws9ZJb/6bbBu7dPhFW00CAoA==} engines: {node: '>=8.3.0'} peerDependencies: bufferutil: ^4.0.1 @@ -6491,8 +7026,8 @@ packages: utf-8-validate: optional: true - ws@8.21.0: - resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==} + ws@8.21.3: + resolution: {integrity: sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==} engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 @@ -6587,7 +7122,7 @@ snapshots: dependencies: type-fest: 4.41.0 - '@alloc/quick-lru@5.2.0': {} + '@alloc/quick-lru@5.3.0': {} '@antfu/install-pkg@1.1.0': dependencies: @@ -6608,17 +7143,20 @@ snapshots: '@babel/compat-data@7.29.7': {} - '@babel/core@7.29.7': + '@babel/compat-data@8.0.0': + optional: true + + '@babel/core@7.29.7(supports-color@8.1.1)': dependencies: '@babel/code-frame': 7.29.7 - '@babel/generator': 7.29.7 + '@babel/generator': 7.29.8 '@babel/helper-compilation-targets': 7.29.7 - '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) '@babel/helpers': 7.29.7 - '@babel/parser': 7.29.7 + '@babel/parser': 7.29.8 '@babel/template': 7.29.7 - '@babel/traverse': 7.29.7 - '@babel/types': 7.29.7 + '@babel/traverse': 7.29.8(supports-color@8.1.1) + '@babel/types': 7.29.8 '@jridgewell/remapping': 2.3.5 convert-source-map: 2.0.0 debug: 4.4.3(supports-color@8.1.1) @@ -6628,49 +7166,88 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/generator@7.29.7': + '@babel/core@8.0.1': + dependencies: + '@babel/code-frame': 8.0.0 + '@babel/generator': 8.0.0 + '@babel/helper-compilation-targets': 8.0.0 + '@babel/helpers': 8.0.0 + '@babel/parser': 8.0.4 + '@babel/template': 8.0.0 + '@babel/traverse': 8.0.4 + '@babel/types': 8.0.4 + '@types/gensync': 1.0.5 + convert-source-map: 2.0.0 + empathic: 2.0.1 + gensync: 1.0.0-beta.2 + import-meta-resolve: 4.2.0 + json5: 2.2.3 + obug: 2.1.4 + semver: 7.8.5 + optional: true + + '@babel/generator@7.29.8': + dependencies: + '@babel/parser': 7.29.8 + '@babel/types': 7.29.8 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/generator@8.0.0': dependencies: - '@babel/parser': 7.29.7 - '@babel/types': 7.29.7 + '@babel/parser': 8.0.4 + '@babel/types': 8.0.4 '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 + '@types/jsesc': 2.5.1 jsesc: 3.1.0 + optional: true '@babel/helper-annotate-as-pure@7.29.7': dependencies: - '@babel/types': 7.29.7 + '@babel/types': 7.29.8 '@babel/helper-compilation-targets@7.29.7': dependencies: '@babel/compat-data': 7.29.7 '@babel/helper-validator-option': 7.29.7 - browserslist: 4.28.5 + browserslist: 4.28.8 lru-cache: 5.1.1 semver: 6.3.1 - '@babel/helper-create-class-features-plugin@7.29.7(@babel/core@7.29.7)': + '@babel/helper-compilation-targets@8.0.0': + dependencies: + '@babel/compat-data': 8.0.0 + '@babel/helper-validator-option': 8.0.0 + browserslist: 4.28.8 + lru-cache: 11.5.2 + semver: 7.8.5 + optional: true + + '@babel/helper-create-class-features-plugin@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-annotate-as-pure': 7.29.7 - '@babel/helper-member-expression-to-functions': 7.29.7 + '@babel/helper-member-expression-to-functions': 7.29.7(supports-color@8.1.1) '@babel/helper-optimise-call-expression': 7.29.7 - '@babel/helper-replace-supers': 7.29.7(@babel/core@7.29.7) - '@babel/helper-skip-transparent-expression-wrappers': 7.29.7 - '@babel/traverse': 7.29.7 + '@babel/helper-replace-supers': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/helper-skip-transparent-expression-wrappers': 7.29.7(supports-color@8.1.1) + '@babel/traverse': 7.29.8(supports-color@8.1.1) semver: 6.3.1 transitivePeerDependencies: - supports-color - '@babel/helper-create-regexp-features-plugin@7.29.7(@babel/core@7.29.7)': + '@babel/helper-create-regexp-features-plugin@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-annotate-as-pure': 7.29.7 regexpu-core: 6.4.0 semver: 6.3.1 - '@babel/helper-define-polyfill-provider@0.6.8(@babel/core@7.29.7)': + '@babel/helper-define-polyfill-provider@0.6.8(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-compilation-targets': 7.29.7 '@babel/helper-plugin-utils': 7.29.7 debug: 4.4.3(supports-color@8.1.1) @@ -6681,62 +7258,68 @@ snapshots: '@babel/helper-globals@7.29.7': {} - '@babel/helper-member-expression-to-functions@7.29.7': + '@babel/helper-globals@8.0.0': + optional: true + + '@babel/helper-member-expression-to-functions@7.29.7(supports-color@8.1.1)': dependencies: - '@babel/traverse': 7.29.7 - '@babel/types': 7.29.7 + '@babel/traverse': 7.29.8(supports-color@8.1.1) + '@babel/types': 7.29.8 transitivePeerDependencies: - supports-color - '@babel/helper-module-imports@7.29.7': + '@babel/helper-module-imports@7.29.7(supports-color@8.1.1)': dependencies: - '@babel/traverse': 7.29.7 - '@babel/types': 7.29.7 + '@babel/traverse': 7.29.8(supports-color@8.1.1) + '@babel/types': 7.29.8 transitivePeerDependencies: - supports-color - '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7)': + '@babel/helper-module-transforms@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 - '@babel/helper-module-imports': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) + '@babel/helper-module-imports': 7.29.7(supports-color@8.1.1) '@babel/helper-validator-identifier': 7.29.7 - '@babel/traverse': 7.29.7 + '@babel/traverse': 7.29.8(supports-color@8.1.1) transitivePeerDependencies: - supports-color '@babel/helper-optimise-call-expression@7.29.7': dependencies: - '@babel/types': 7.29.7 + '@babel/types': 7.29.8 '@babel/helper-plugin-utils@7.29.7': {} - '@babel/helper-remap-async-to-generator@7.29.7(@babel/core@7.29.7)': + '@babel/helper-remap-async-to-generator@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-annotate-as-pure': 7.29.7 - '@babel/helper-wrap-function': 7.29.7 - '@babel/traverse': 7.29.7 + '@babel/helper-wrap-function': 7.29.7(supports-color@8.1.1) + '@babel/traverse': 7.29.8(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@babel/helper-replace-supers@7.29.7(@babel/core@7.29.7)': + '@babel/helper-replace-supers@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 - '@babel/helper-member-expression-to-functions': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) + '@babel/helper-member-expression-to-functions': 7.29.7(supports-color@8.1.1) '@babel/helper-optimise-call-expression': 7.29.7 - '@babel/traverse': 7.29.7 + '@babel/traverse': 7.29.8(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@babel/helper-skip-transparent-expression-wrappers@7.29.7': + '@babel/helper-skip-transparent-expression-wrappers@7.29.7(supports-color@8.1.1)': dependencies: - '@babel/traverse': 7.29.7 - '@babel/types': 7.29.7 + '@babel/traverse': 7.29.8(supports-color@8.1.1) + '@babel/types': 7.29.8 transitivePeerDependencies: - supports-color '@babel/helper-string-parser@7.29.7': {} + '@babel/helper-string-parser@8.0.0': + optional: true + '@babel/helper-validator-identifier@7.29.7': {} '@babel/helper-validator-identifier@8.0.4': @@ -6744,318 +7327,332 @@ snapshots: '@babel/helper-validator-option@7.29.7': {} - '@babel/helper-wrap-function@7.29.7': + '@babel/helper-validator-option@8.0.0': + optional: true + + '@babel/helper-wrap-function@7.29.7(supports-color@8.1.1)': dependencies: '@babel/template': 7.29.7 - '@babel/traverse': 7.29.7 - '@babel/types': 7.29.7 + '@babel/traverse': 7.29.8(supports-color@8.1.1) + '@babel/types': 7.29.8 transitivePeerDependencies: - supports-color '@babel/helpers@7.29.7': dependencies: '@babel/template': 7.29.7 - '@babel/types': 7.29.7 + '@babel/types': 7.29.8 + + '@babel/helpers@8.0.0': + dependencies: + '@babel/template': 8.0.0 + '@babel/types': 8.0.4 + optional: true + + '@babel/parser@7.29.8': + dependencies: + '@babel/types': 7.29.8 - '@babel/parser@7.29.7': + '@babel/parser@8.0.4': dependencies: - '@babel/types': 7.29.7 + '@babel/types': 8.0.4 + optional: true - '@babel/plugin-proposal-decorators@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-proposal-decorators@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 - '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7) + '@babel/core': 7.29.7(supports-color@8.1.1) + '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-decorators': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-syntax-decorators': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) transitivePeerDependencies: - supports-color - '@babel/plugin-proposal-export-default-from@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-proposal-export-default-from@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-decorators@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-syntax-decorators@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.29.7)': + '@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-export-default-from@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-syntax-export-default-from@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-flow@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-syntax-flow@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.29.7)': + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.29.7)': + '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-async-generator-functions@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-async-generator-functions@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/helper-remap-async-to-generator': 7.29.7(@babel/core@7.29.7) - '@babel/traverse': 7.29.7 + '@babel/helper-remap-async-to-generator': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/traverse': 7.29.8(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-async-to-generator@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-async-to-generator@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 - '@babel/helper-module-imports': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) + '@babel/helper-module-imports': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/helper-remap-async-to-generator': 7.29.7(@babel/core@7.29.7) + '@babel/helper-remap-async-to-generator': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-block-scoping@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-block-scoping@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-class-properties@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-class-properties@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 - '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7) + '@babel/core': 7.29.7(supports-color@8.1.1) + '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-class-static-block@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-class-static-block@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 - '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7) + '@babel/core': 7.29.7(supports-color@8.1.1) + '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-classes@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-classes@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-annotate-as-pure': 7.29.7 '@babel/helper-compilation-targets': 7.29.7 '@babel/helper-globals': 7.29.7 '@babel/helper-plugin-utils': 7.29.7 - '@babel/helper-replace-supers': 7.29.7(@babel/core@7.29.7) - '@babel/traverse': 7.29.7 + '@babel/helper-replace-supers': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/traverse': 7.29.8(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-destructuring@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-destructuring@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/traverse': 7.29.7 + '@babel/traverse': 7.29.8(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-export-namespace-from@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-export-namespace-from@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-flow-strip-types@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-flow-strip-types@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-flow': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-syntax-flow': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) - '@babel/plugin-transform-for-of@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-for-of@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/helper-skip-transparent-expression-wrappers': 7.29.7 + '@babel/helper-skip-transparent-expression-wrappers': 7.29.7(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-logical-assignment-operators@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-logical-assignment-operators@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-modules-commonjs@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-modules-commonjs@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 - '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7) + '@babel/core': 7.29.7(supports-color@8.1.1) + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-named-capturing-groups-regex@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-named-capturing-groups-regex@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 - '@babel/helper-create-regexp-features-plugin': 7.29.7(@babel/core@7.29.7) + '@babel/core': 7.29.7(supports-color@8.1.1) + '@babel/helper-create-regexp-features-plugin': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-nullish-coalescing-operator@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-nullish-coalescing-operator@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-object-rest-spread@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-object-rest-spread@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-compilation-targets': 7.29.7 '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-destructuring': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-parameters': 7.29.7(@babel/core@7.29.7) - '@babel/traverse': 7.29.7 + '@babel/plugin-transform-destructuring': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-parameters': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/traverse': 7.29.8(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-optional-catch-binding@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-optional-catch-binding@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-optional-chaining@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-optional-chaining@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/helper-skip-transparent-expression-wrappers': 7.29.7 + '@babel/helper-skip-transparent-expression-wrappers': 7.29.7(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-parameters@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-parameters@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-private-methods@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-private-methods@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 - '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7) + '@babel/core': 7.29.7(supports-color@8.1.1) + '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-private-property-in-object@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-private-property-in-object@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-annotate-as-pure': 7.29.7 - '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7) + '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-react-display-name@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-react-display-name@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-react-jsx-development@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-react-jsx-development@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 - '@babel/plugin-transform-react-jsx': 7.29.7(@babel/core@7.29.7) + '@babel/core': 7.29.7(supports-color@8.1.1) + '@babel/plugin-transform-react-jsx': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-react-jsx-self@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-react-jsx-self@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-react-jsx-source@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-react-jsx-source@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-react-jsx@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-react-jsx@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-annotate-as-pure': 7.29.7 - '@babel/helper-module-imports': 7.29.7 + '@babel/helper-module-imports': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7) - '@babel/types': 7.29.7 + '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/types': 7.29.8 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-react-pure-annotations@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-react-pure-annotations@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-annotate-as-pure': 7.29.7 '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-regenerator@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-regenerator@7.29.8(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-runtime@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-runtime@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 - '@babel/helper-module-imports': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) + '@babel/helper-module-imports': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - babel-plugin-polyfill-corejs2: 0.4.17(@babel/core@7.29.7) - babel-plugin-polyfill-corejs3: 0.13.0(@babel/core@7.29.7) - babel-plugin-polyfill-regenerator: 0.6.8(@babel/core@7.29.7) + babel-plugin-polyfill-corejs2: 0.4.17(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + babel-plugin-polyfill-corejs3: 0.13.0(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + babel-plugin-polyfill-regenerator: 0.6.8(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) semver: 6.3.1 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-typescript@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-typescript@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-annotate-as-pure': 7.29.7 - '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7) + '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 - '@babel/helper-skip-transparent-expression-wrappers': 7.29.7 - '@babel/plugin-syntax-typescript': 7.29.7(@babel/core@7.29.7) + '@babel/helper-skip-transparent-expression-wrappers': 7.29.7(supports-color@8.1.1) + '@babel/plugin-syntax-typescript': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-unicode-regex@7.29.7(@babel/core@7.29.7)': + '@babel/plugin-transform-unicode-regex@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 - '@babel/helper-create-regexp-features-plugin': 7.29.7(@babel/core@7.29.7) + '@babel/core': 7.29.7(supports-color@8.1.1) + '@babel/helper-create-regexp-features-plugin': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) '@babel/helper-plugin-utils': 7.29.7 - '@babel/preset-react@7.29.7(@babel/core@7.29.7)': + '@babel/preset-react@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 '@babel/helper-validator-option': 7.29.7 - '@babel/plugin-transform-react-display-name': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-react-jsx': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-react-jsx-development': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-react-pure-annotations': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-react-display-name': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-transform-react-jsx': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-react-jsx-development': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-react-pure-annotations': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) transitivePeerDependencies: - supports-color - '@babel/preset-typescript@7.29.7(@babel/core@7.29.7)': + '@babel/preset-typescript@7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.29.7 '@babel/helper-validator-option': 7.29.7 - '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-modules-commonjs': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-typescript': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-transform-modules-commonjs': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-typescript': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -7064,41 +7661,65 @@ snapshots: '@babel/template@7.29.7': dependencies: '@babel/code-frame': 7.29.7 - '@babel/parser': 7.29.7 - '@babel/types': 7.29.7 + '@babel/parser': 7.29.8 + '@babel/types': 7.29.8 + + '@babel/template@8.0.0': + dependencies: + '@babel/code-frame': 8.0.0 + '@babel/parser': 8.0.4 + '@babel/types': 8.0.4 + optional: true - '@babel/traverse@7.29.7': + '@babel/traverse@7.29.8(supports-color@8.1.1)': dependencies: '@babel/code-frame': 7.29.7 - '@babel/generator': 7.29.7 + '@babel/generator': 7.29.8 '@babel/helper-globals': 7.29.7 - '@babel/parser': 7.29.7 + '@babel/parser': 7.29.8 '@babel/template': 7.29.7 - '@babel/types': 7.29.7 + '@babel/types': 7.29.8 debug: 4.4.3(supports-color@8.1.1) transitivePeerDependencies: - supports-color + '@babel/traverse@8.0.4': + dependencies: + '@babel/code-frame': 8.0.0 + '@babel/generator': 8.0.0 + '@babel/helper-globals': 8.0.0 + '@babel/parser': 8.0.4 + '@babel/template': 8.0.0 + '@babel/types': 8.0.4 + obug: 2.1.4 + optional: true + '@babel/types@7.29.0': dependencies: '@babel/helper-string-parser': 7.29.7 '@babel/helper-validator-identifier': 7.29.7 - '@babel/types@7.29.7': + '@babel/types@7.29.8': dependencies: '@babel/helper-string-parser': 7.29.7 '@babel/helper-validator-identifier': 7.29.7 + '@babel/types@8.0.4': + dependencies: + '@babel/helper-string-parser': 8.0.0 + '@babel/helper-validator-identifier': 8.0.4 + optional: true + '@braintree/sanitize-url@7.1.2': {} '@chevrotain/types@11.1.2': {} - '@commitlint/cli@21.2.2(@types/node@24.13.3)(conventional-commits-filter@6.0.1)(conventional-commits-parser@7.1.2)(typescript@6.0.3)': + '@commitlint/cli@21.2.2(@types/node@26.4.1)(conventional-commits-filter@6.0.1)(conventional-commits-parser@7.1.2)(typescript@7.0.2)': dependencies: '@commitlint/config-conventional': 21.2.2 '@commitlint/format': 21.2.2 '@commitlint/lint': 21.2.2 - '@commitlint/load': 21.2.2(@types/node@24.13.3)(typescript@6.0.3) + '@commitlint/load': 21.2.2(@types/node@26.4.1)(typescript@7.0.2) '@commitlint/read': 21.2.1(conventional-commits-filter@6.0.1)(conventional-commits-parser@7.1.2) '@commitlint/types': 21.2.0 tinyexec: 1.3.0 @@ -7143,14 +7764,14 @@ snapshots: '@commitlint/rules': 21.2.2 '@commitlint/types': 21.2.0 - '@commitlint/load@21.2.2(@types/node@24.13.3)(typescript@6.0.3)': + '@commitlint/load@21.2.2(@types/node@26.4.1)(typescript@7.0.2)': dependencies: '@commitlint/config-validator': 21.2.0 '@commitlint/execute-rule': 21.0.1 '@commitlint/resolve-extends': 21.2.2 '@commitlint/types': 21.2.0 - cosmiconfig: 9.0.2(typescript@6.0.3) - cosmiconfig-typescript-loader: 6.3.0(@types/node@24.13.3)(cosmiconfig@9.0.2(typescript@6.0.3))(typescript@6.0.3) + cosmiconfig: 9.0.2(typescript@7.0.2) + cosmiconfig-typescript-loader: 6.3.0(@types/node@26.4.1)(cosmiconfig@9.0.2(typescript@7.0.2))(typescript@7.0.2) es-toolkit: 1.52.0 is-plain-obj: 4.1.0 picocolors: 1.1.1 @@ -7213,27 +7834,11 @@ snapshots: '@conventional-changelog/template@1.4.0': {} - '@emnapi/core@1.11.1': - dependencies: - '@emnapi/wasi-threads': 1.2.2 - tslib: 2.8.1 - optional: true - - '@emnapi/runtime@1.11.1': - dependencies: - tslib: 2.8.1 - optional: true - '@emnapi/runtime@1.11.3': dependencies: tslib: 2.8.1 optional: true - '@emnapi/wasi-threads@1.2.2': - dependencies: - tslib: 2.8.1 - optional: true - '@esbuild/aix-ppc64@0.28.2': optional: true @@ -7312,49 +7917,49 @@ snapshots: '@esbuild/win32-x64@0.28.2': optional: true - '@expo/cli@57.0.6(@expo/dom-webview@57.0.0)(@expo/metro-runtime@57.0.3)(expo-constants@57.0.3(expo@57.0.4)(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3)))(expo-font@57.0.0(expo@57.0.4)(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3))(react@19.2.3))(expo@57.0.4)(react-dom@19.2.3(react@19.2.3))(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3))(react@19.2.3)(typescript@6.0.3)': + '@expo/cli@57.0.21(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(expo-constants@57.0.17(expo@57.0.19)(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(supports-color@8.1.1))(expo-font@57.0.3(expo@57.0.19)(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3))(expo@57.0.19)(react-dom@19.2.3(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1)(typescript@7.0.2)': dependencies: '@expo/code-signing-certificates': 0.0.6 - '@expo/config': 57.0.3(typescript@6.0.3) - '@expo/config-plugins': 57.0.3(typescript@6.0.3) - '@expo/devcert': 1.2.1 - '@expo/env': 2.4.1 - '@expo/image-utils': 0.11.1(typescript@6.0.3) - '@expo/inline-modules': 0.1.2(typescript@6.0.3) - '@expo/json-file': 11.0.0 - '@expo/log-box': 57.0.0(@expo/dom-webview@57.0.0)(expo@57.0.4)(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3))(react@19.2.3) - '@expo/metro': 56.0.0 - '@expo/metro-config': 57.0.3(expo@57.0.4)(typescript@6.0.3) - '@expo/metro-file-map': 57.0.0 - '@expo/osascript': 2.7.0 - '@expo/package-manager': 1.13.0 - '@expo/plist': 0.8.0 - '@expo/prebuild-config': 57.0.5(typescript@6.0.3) - '@expo/require-utils': 57.0.1(typescript@6.0.3) - '@expo/router-server': 57.0.2(@expo/metro-runtime@57.0.3)(expo-constants@57.0.3(expo@57.0.4)(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3)))(expo-font@57.0.0(expo@57.0.4)(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3))(react@19.2.3))(expo-server@57.0.0)(expo@57.0.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@expo/schema-utils': 57.0.1 + '@expo/config': 57.0.9(supports-color@8.1.1)(typescript@7.0.2) + '@expo/config-plugins': 57.0.9(supports-color@8.1.1)(typescript@7.0.2) + '@expo/devcert': 1.2.1(supports-color@8.1.1) + '@expo/env': 2.4.3(supports-color@8.1.1) + '@expo/image-utils': 0.11.5(supports-color@8.1.1)(typescript@7.0.2) + '@expo/inline-modules': 0.1.7(supports-color@8.1.1)(typescript@7.0.2) + '@expo/json-file': 11.0.1 + '@expo/log-box': 57.0.4(@expo/dom-webview@57.0.1)(expo@57.0.19)(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) + '@expo/metro': 56.0.2(supports-color@8.1.1) + '@expo/metro-config': 57.0.12(expo@57.0.19)(supports-color@8.1.1)(typescript@7.0.2) + '@expo/metro-file-map': 57.0.2(supports-color@8.1.1) + '@expo/osascript': 2.7.1 + '@expo/package-manager': 1.13.1 + '@expo/plist': 0.8.1 + '@expo/prebuild-config': 57.0.15(supports-color@8.1.1)(typescript@7.0.2) + '@expo/require-utils': 57.0.5(supports-color@8.1.1)(typescript@7.0.2) + '@expo/router-server': 57.0.9(@expo/metro-runtime@57.0.15)(expo-constants@57.0.17(expo@57.0.19)(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(supports-color@8.1.1))(expo-font@57.0.3(expo@57.0.19)(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3))(expo-server@57.0.3)(expo@57.0.19)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(supports-color@8.1.1) + '@expo/schema-utils': 57.0.2 '@expo/spawn-async': 1.8.0 - '@expo/ws-tunnel': 2.0.0(ws@8.21.0) + '@expo/ws-tunnel': 2.0.0(ws@8.21.3) '@expo/xcpretty': 4.4.4 - '@react-native/dev-middleware': 0.86.0 + '@react-native/dev-middleware': 0.86.3(supports-color@8.1.1) accepts: 1.3.8 - agent-cli-detector: 0.1.2 + agent-cli-detector: 0.1.7 arg: 5.0.2 bplist-creator: 0.1.0 bplist-parser: 0.3.2 chalk: 4.1.2 ci-info: 3.9.0 - compression: 1.8.1 - connect: 3.7.0 + compression: 1.8.1(supports-color@8.1.1) + connect: 3.7.0(supports-color@8.1.1) debug: 4.4.3(supports-color@8.1.1) dnssd-advertise: 1.1.6 - expo: 57.0.4(@babel/core@7.29.7)(@expo/dom-webview@57.0.0)(@expo/metro-runtime@57.0.3)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) - expo-server: 57.0.0 + expo: 57.0.19(@babel/core@7.29.7(supports-color@8.1.1))(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1)(typescript@7.0.2) + expo-server: 57.0.3 fetch-nodeshim: 0.4.10 getenv: 2.0.0 glob: 13.0.6 lan-network: 0.2.1 - multitars: 1.0.0 + multitars: 1.0.2 node-forge: 1.4.0 npm-package-arg: 11.0.3 ora: 3.4.0 @@ -7363,18 +7968,19 @@ snapshots: progress: 2.0.3 prompts: 2.4.2 resolve-from: 5.0.0 + sandbox-cli-detector: 0.2.0 semver: 7.8.5 - send: 0.19.2 + send: 0.19.2(supports-color@8.1.1) slugify: 1.6.9 stacktrace-parser: 0.1.11 structured-headers: 0.4.1 terminal-link: 2.1.1 toqr: 0.1.1 wrap-ansi: 7.0.0 - ws: 8.21.0 + ws: 8.21.3 zod: 3.25.76 optionalDependencies: - react-native: 0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3) + react-native: 0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) transitivePeerDependencies: - '@expo/dom-webview' - '@expo/metro-runtime' @@ -7392,12 +7998,12 @@ snapshots: dependencies: node-forge: 1.4.0 - '@expo/config-plugins@57.0.3(typescript@6.0.3)': + '@expo/config-plugins@57.0.9(supports-color@8.1.1)(typescript@7.0.2)': dependencies: - '@expo/config-types': 57.0.1 - '@expo/json-file': 11.0.0 - '@expo/plist': 0.8.0 - '@expo/require-utils': 57.0.1(typescript@6.0.3) + '@expo/config-types': 57.0.2 + '@expo/json-file': 11.0.1 + '@expo/plist': 0.8.1 + '@expo/require-utils': 57.0.5(supports-color@8.1.1)(typescript@7.0.2) '@expo/sdk-runtime-versions': 1.0.0 chalk: 4.1.2 debug: 4.4.3(supports-color@8.1.1) @@ -7411,14 +8017,14 @@ snapshots: - supports-color - typescript - '@expo/config-types@57.0.1': {} + '@expo/config-types@57.0.2': {} - '@expo/config@57.0.3(typescript@6.0.3)': + '@expo/config@57.0.9(supports-color@8.1.1)(typescript@7.0.2)': dependencies: - '@expo/config-plugins': 57.0.3(typescript@6.0.3) - '@expo/config-types': 57.0.1 - '@expo/json-file': 11.0.0 - '@expo/require-utils': 57.0.1(typescript@6.0.3) + '@expo/config-plugins': 57.0.9(supports-color@8.1.1)(typescript@7.0.2) + '@expo/config-types': 57.0.2 + '@expo/json-file': 11.0.1 + '@expo/require-utils': 57.0.5(supports-color@8.1.1)(typescript@7.0.2) deepmerge: 4.3.1 getenv: 2.0.0 glob: 13.0.6 @@ -7429,27 +8035,27 @@ snapshots: - supports-color - typescript - '@expo/devcert@1.2.1': + '@expo/devcert@1.2.1(supports-color@8.1.1)': dependencies: '@expo/sudo-prompt': 9.3.2 - debug: 3.2.7 + debug: 3.2.7(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@expo/devtools@57.0.0(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3))(react@19.2.3)': + '@expo/devtools@57.0.1(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)': dependencies: chalk: 4.1.2 optionalDependencies: react: 19.2.3 - react-native: 0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3) + react-native: 0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) - '@expo/dom-webview@57.0.0(expo@57.0.4)(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3))(react@19.2.3)': + '@expo/dom-webview@57.0.1(expo@57.0.19)(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)': dependencies: - expo: 57.0.4(@babel/core@7.29.7)(@expo/dom-webview@57.0.0)(@expo/metro-runtime@57.0.3)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo: 57.0.19(@babel/core@7.29.7(supports-color@8.1.1))(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1)(typescript@7.0.2) react: 19.2.3 - react-native: 0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3) + react-native: 0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) - '@expo/env@2.4.1': + '@expo/env@2.4.3(supports-color@8.1.1)': dependencies: chalk: 4.1.2 debug: 4.4.3(supports-color@8.1.1) @@ -7457,11 +8063,11 @@ snapshots: transitivePeerDependencies: - supports-color - '@expo/expo-modules-macros-plugin@0.3.0': {} + '@expo/expo-modules-macros-plugin@0.6.1': {} - '@expo/fingerprint@0.20.3': + '@expo/fingerprint@0.20.12(supports-color@8.1.1)': dependencies: - '@expo/env': 2.4.1 + '@expo/env': 2.4.3(supports-color@8.1.1) '@expo/spawn-async': 1.8.0 arg: 5.0.2 chalk: 4.1.2 @@ -7475,9 +8081,9 @@ snapshots: transitivePeerDependencies: - supports-color - '@expo/image-utils@0.11.1(typescript@6.0.3)': + '@expo/image-utils@0.11.5(supports-color@8.1.1)(typescript@7.0.2)': dependencies: - '@expo/require-utils': 57.0.1(typescript@6.0.3) + '@expo/require-utils': 57.0.5(supports-color@8.1.1)(typescript@7.0.2) '@expo/spawn-async': 1.8.0 chalk: 4.1.2 getenv: 2.0.0 @@ -7488,69 +8094,69 @@ snapshots: - supports-color - typescript - '@expo/inline-modules@0.1.2(typescript@6.0.3)': + '@expo/inline-modules@0.1.7(supports-color@8.1.1)(typescript@7.0.2)': dependencies: - '@expo/config-plugins': 57.0.3(typescript@6.0.3) + '@expo/config-plugins': 57.0.9(supports-color@8.1.1)(typescript@7.0.2) transitivePeerDependencies: - supports-color - typescript - '@expo/json-file@11.0.0': + '@expo/json-file@11.0.1': dependencies: '@babel/code-frame': 7.29.7 json5: 2.2.3 - '@expo/local-build-cache-provider@57.0.2(typescript@6.0.3)': + '@expo/local-build-cache-provider@57.0.8(supports-color@8.1.1)(typescript@7.0.2)': dependencies: - '@expo/config': 57.0.3(typescript@6.0.3) + '@expo/config': 57.0.9(supports-color@8.1.1)(typescript@7.0.2) chalk: 4.1.2 transitivePeerDependencies: - supports-color - typescript - '@expo/log-box@57.0.0(@expo/dom-webview@57.0.0)(expo@57.0.4)(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3))(react@19.2.3)': + '@expo/log-box@57.0.4(@expo/dom-webview@57.0.1)(expo@57.0.19)(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)': dependencies: - '@expo/dom-webview': 57.0.0(expo@57.0.4)(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3))(react@19.2.3) + '@expo/dom-webview': 57.0.1(expo@57.0.19)(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) anser: 1.4.10 - expo: 57.0.4(@babel/core@7.29.7)(@expo/dom-webview@57.0.0)(@expo/metro-runtime@57.0.3)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo: 57.0.19(@babel/core@7.29.7(supports-color@8.1.1))(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1)(typescript@7.0.2) react: 19.2.3 - react-native: 0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3) + react-native: 0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) stacktrace-parser: 0.1.11 - '@expo/metro-config@57.0.3(expo@57.0.4)(typescript@6.0.3)': + '@expo/metro-config@57.0.12(expo@57.0.19)(supports-color@8.1.1)(typescript@7.0.2)': dependencies: '@babel/code-frame': 7.29.7 - '@babel/core': 7.29.7 - '@babel/generator': 7.29.7 - '@expo/config': 57.0.3(typescript@6.0.3) - '@expo/env': 2.4.1 - '@expo/json-file': 11.0.0 - '@expo/metro': 56.0.0 - '@expo/require-utils': 57.0.1(typescript@6.0.3) + '@babel/core': 7.29.7(supports-color@8.1.1) + '@babel/generator': 7.29.8 + '@expo/config': 57.0.9(supports-color@8.1.1)(typescript@7.0.2) + '@expo/env': 2.4.3(supports-color@8.1.1) + '@expo/json-file': 11.0.1 + '@expo/metro': 56.0.2(supports-color@8.1.1) + '@expo/require-utils': 57.0.5(supports-color@8.1.1)(typescript@7.0.2) '@expo/spawn-async': 1.8.0 '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/remapping': 2.3.5 '@jridgewell/sourcemap-codec': 1.6.0 - browserslist: 4.28.5 + browserslist: 4.28.8 chalk: 4.1.2 debug: 4.4.3(supports-color@8.1.1) getenv: 2.0.0 glob: 13.0.6 hermes-parser: 0.36.1 jsc-safe-url: 0.2.4 - lightningcss: 1.32.0 + lightningcss: 1.33.0 picomatch: 4.0.7 postcss: 8.5.26 resolve-from: 5.0.0 optionalDependencies: - expo: 57.0.4(@babel/core@7.29.7)(@expo/dom-webview@57.0.0)(@expo/metro-runtime@57.0.3)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo: 57.0.19(@babel/core@7.29.7(supports-color@8.1.1))(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1)(typescript@7.0.2) transitivePeerDependencies: - bufferutil - supports-color - typescript - utf-8-validate - '@expo/metro-file-map@57.0.0': + '@expo/metro-file-map@57.0.2(supports-color@8.1.1)': dependencies: debug: 4.4.3(supports-color@8.1.1) fb-watchman: 2.0.2 @@ -7561,100 +8167,100 @@ snapshots: transitivePeerDependencies: - supports-color - '@expo/metro-runtime@57.0.3(@expo/log-box@57.0.0)(expo@57.0.4)(react-dom@19.2.3(react@19.2.3))(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3))(react@19.2.3)': + '@expo/metro-runtime@57.0.15(@expo/log-box@57.0.4)(expo@57.0.19)(react-dom@19.2.3(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)': dependencies: - '@expo/log-box': 57.0.0(@expo/dom-webview@57.0.0)(expo@57.0.4)(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3))(react@19.2.3) + '@expo/log-box': 57.0.4(@expo/dom-webview@57.0.1)(expo@57.0.19)(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) anser: 1.4.10 - expo: 57.0.4(@babel/core@7.29.7)(@expo/dom-webview@57.0.0)(@expo/metro-runtime@57.0.3)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo: 57.0.19(@babel/core@7.29.7(supports-color@8.1.1))(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1)(typescript@7.0.2) pretty-format: 29.7.0 react: 19.2.3 - react-native: 0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3) + react-native: 0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) stacktrace-parser: 0.1.11 whatwg-fetch: 3.6.20 optionalDependencies: react-dom: 19.2.3(react@19.2.3) - '@expo/metro@56.0.0': - dependencies: - metro: 0.84.4 - metro-babel-transformer: 0.84.4 - metro-cache: 0.84.4 - metro-cache-key: 0.84.4 - metro-config: 0.84.4 - metro-core: 0.84.4 - metro-file-map: 0.84.4 - metro-minify-terser: 0.84.4 - metro-resolver: 0.84.4 - metro-runtime: 0.84.4 - metro-source-map: 0.84.4 - metro-symbolicate: 0.84.4 - metro-transform-plugins: 0.84.4 - metro-transform-worker: 0.84.4 + '@expo/metro@56.0.2(supports-color@8.1.1)': + dependencies: + metro: 0.84.5(supports-color@8.1.1) + metro-babel-transformer: 0.84.5(supports-color@8.1.1) + metro-cache: 0.84.5(supports-color@8.1.1) + metro-cache-key: 0.84.5 + metro-config: 0.84.5(supports-color@8.1.1) + metro-core: 0.84.5 + metro-file-map: 0.84.5(supports-color@8.1.1) + metro-minify-terser: 0.84.5 + metro-resolver: 0.84.5 + metro-runtime: 0.84.5 + metro-source-map: 0.84.5(supports-color@8.1.1) + metro-symbolicate: 0.84.5 + metro-transform-plugins: 0.84.5(supports-color@8.1.1) + metro-transform-worker: 0.84.5(supports-color@8.1.1) transitivePeerDependencies: - bufferutil - supports-color - utf-8-validate - '@expo/osascript@2.7.0': + '@expo/osascript@2.7.1': dependencies: '@expo/spawn-async': 1.8.0 - '@expo/package-manager@1.13.0': + '@expo/package-manager@1.13.1': dependencies: - '@expo/json-file': 11.0.0 + '@expo/json-file': 11.0.1 '@expo/spawn-async': 1.8.0 chalk: 4.1.2 npm-package-arg: 11.0.3 ora: 3.4.0 resolve-workspace-root: 2.0.1 - '@expo/plist@0.8.0': + '@expo/plist@0.8.1': dependencies: - '@xmldom/xmldom': 0.8.13 + '@xmldom/xmldom': 0.8.15 base64-js: 1.5.1 xmlbuilder: 15.1.1 - '@expo/prebuild-config@57.0.5(typescript@6.0.3)': + '@expo/prebuild-config@57.0.15(supports-color@8.1.1)(typescript@7.0.2)': dependencies: - '@expo/config': 57.0.3(typescript@6.0.3) - '@expo/config-plugins': 57.0.3(typescript@6.0.3) - '@expo/config-types': 57.0.1 - '@expo/image-utils': 0.11.1(typescript@6.0.3) - '@expo/json-file': 11.0.0 - '@react-native/normalize-colors': 0.86.0 + '@expo/config': 57.0.9(supports-color@8.1.1)(typescript@7.0.2) + '@expo/config-plugins': 57.0.9(supports-color@8.1.1)(typescript@7.0.2) + '@expo/config-types': 57.0.2 + '@expo/image-utils': 0.11.5(supports-color@8.1.1)(typescript@7.0.2) + '@expo/json-file': 11.0.1 + '@react-native/normalize-colors': 0.86.3 debug: 4.4.3(supports-color@8.1.1) - expo-modules-autolinking: 57.0.5(typescript@6.0.3) + expo-modules-autolinking: 57.0.12(supports-color@8.1.1)(typescript@7.0.2) resolve-from: 5.0.0 semver: 7.8.5 transitivePeerDependencies: - supports-color - typescript - '@expo/require-utils@57.0.1(typescript@6.0.3)': + '@expo/require-utils@57.0.5(supports-color@8.1.1)(typescript@7.0.2)': dependencies: '@babel/code-frame': 7.29.7 - '@babel/core': 7.29.7 - '@babel/plugin-transform-modules-commonjs': 7.29.7(@babel/core@7.29.7) + '@babel/core': 7.29.7(supports-color@8.1.1) + '@babel/plugin-transform-modules-commonjs': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) optionalDependencies: - typescript: 6.0.3 + typescript: 7.0.2 transitivePeerDependencies: - supports-color - '@expo/router-server@57.0.2(@expo/metro-runtime@57.0.3)(expo-constants@57.0.3(expo@57.0.4)(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3)))(expo-font@57.0.0(expo@57.0.4)(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3))(react@19.2.3))(expo-server@57.0.0)(expo@57.0.4)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@expo/router-server@57.0.9(@expo/metro-runtime@57.0.15)(expo-constants@57.0.17(expo@57.0.19)(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(supports-color@8.1.1))(expo-font@57.0.3(expo@57.0.19)(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3))(expo-server@57.0.3)(expo@57.0.19)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(supports-color@8.1.1)': dependencies: debug: 4.4.3(supports-color@8.1.1) - expo: 57.0.4(@babel/core@7.29.7)(@expo/dom-webview@57.0.0)(@expo/metro-runtime@57.0.3)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) - expo-constants: 57.0.3(expo@57.0.4)(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3)) - expo-font: 57.0.0(expo@57.0.4)(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3))(react@19.2.3) - expo-server: 57.0.0 + expo: 57.0.19(@babel/core@7.29.7(supports-color@8.1.1))(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1)(typescript@7.0.2) + expo-constants: 57.0.17(expo@57.0.19)(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(supports-color@8.1.1) + expo-font: 57.0.3(expo@57.0.19)(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) + expo-server: 57.0.3 react: 19.2.3 optionalDependencies: - '@expo/metro-runtime': 57.0.3(@expo/log-box@57.0.0)(expo@57.0.4)(react-dom@19.2.3(react@19.2.3))(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3))(react@19.2.3) + '@expo/metro-runtime': 57.0.15(@expo/log-box@57.0.4)(expo@57.0.19)(react-dom@19.2.3(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) react-dom: 19.2.3(react@19.2.3) transitivePeerDependencies: - supports-color - '@expo/schema-utils@57.0.1': {} + '@expo/schema-utils@57.0.2': {} '@expo/sdk-runtime-versions@1.0.0': {} @@ -7664,15 +8270,15 @@ snapshots: '@expo/sudo-prompt@9.3.2': {} - '@expo/ws-tunnel@2.0.0(ws@8.21.0)': + '@expo/ws-tunnel@2.0.0(ws@8.21.3)': dependencies: - ws: 8.21.0 + ws: 8.21.3 '@expo/xcpretty@4.4.4': dependencies: '@babel/code-frame': 7.29.7 chalk: 4.1.2 - js-yaml: 4.3.0 + js-yaml: 4.3.2 '@floating-ui/core@1.8.0': dependencies: @@ -7683,20 +8289,20 @@ snapshots: '@floating-ui/core': 1.8.0 '@floating-ui/utils': 0.2.12 - '@floating-ui/react-dom@2.1.9(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@floating-ui/react-dom@2.1.9(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: '@floating-ui/dom': 1.8.0 - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) '@floating-ui/utils@0.2.12': {} - '@fuma-translate/react@1.0.2(@types/react@19.2.17)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@fuma-translate/react@1.0.2(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) optionalDependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 '@fumadocs/tailwind@0.1.1(tailwindcss@4.3.3)': optionalDependencies: @@ -7819,141 +8425,141 @@ snapshots: '@img/sharp-win32-x64@0.35.4': optional: true - '@inquirer/ansi@2.0.7': {} + '@inquirer/ansi@2.0.8': {} - '@inquirer/checkbox@5.2.3(@types/node@24.13.3)': + '@inquirer/checkbox@5.2.4(@types/node@26.4.1)': dependencies: - '@inquirer/ansi': 2.0.7 - '@inquirer/core': 12.0.1(@types/node@24.13.3) - '@inquirer/figures': 2.0.8 - '@inquirer/type': 4.1.0(@types/node@24.13.3) + '@inquirer/ansi': 2.0.8 + '@inquirer/core': 12.0.2(@types/node@26.4.1) + '@inquirer/figures': 2.0.9 + '@inquirer/type': 4.1.1(@types/node@26.4.1) optionalDependencies: - '@types/node': 24.13.3 + '@types/node': 26.4.1 - '@inquirer/confirm@6.3.0(@types/node@24.13.3)': + '@inquirer/confirm@6.3.1(@types/node@26.4.1)': dependencies: - '@inquirer/core': 12.0.1(@types/node@24.13.3) - '@inquirer/type': 4.1.0(@types/node@24.13.3) + '@inquirer/core': 12.0.2(@types/node@26.4.1) + '@inquirer/type': 4.1.1(@types/node@26.4.1) optionalDependencies: - '@types/node': 24.13.3 + '@types/node': 26.4.1 - '@inquirer/core@12.0.1(@types/node@24.13.3)': + '@inquirer/core@12.0.2(@types/node@26.4.1)': dependencies: - '@inquirer/ansi': 2.0.7 - '@inquirer/figures': 2.0.8 - '@inquirer/type': 4.1.0(@types/node@24.13.3) + '@inquirer/ansi': 2.0.8 + '@inquirer/figures': 2.0.9 + '@inquirer/type': 4.1.1(@types/node@26.4.1) cli-width: 4.1.0 fast-wrap-ansi: 0.2.2 mute-stream: 3.0.0 signal-exit: 4.1.0 optionalDependencies: - '@types/node': 24.13.3 + '@types/node': 26.4.1 - '@inquirer/editor@5.3.1(@types/node@24.13.3)': + '@inquirer/editor@5.3.2(@types/node@26.4.1)': dependencies: - '@inquirer/core': 12.0.1(@types/node@24.13.3) - '@inquirer/external-editor': 3.0.4(@types/node@24.13.3) - '@inquirer/type': 4.1.0(@types/node@24.13.3) + '@inquirer/core': 12.0.2(@types/node@26.4.1) + '@inquirer/external-editor': 3.0.5(@types/node@26.4.1) + '@inquirer/type': 4.1.1(@types/node@26.4.1) optionalDependencies: - '@types/node': 24.13.3 + '@types/node': 26.4.1 - '@inquirer/expand@5.1.3(@types/node@24.13.3)': + '@inquirer/expand@5.1.4(@types/node@26.4.1)': dependencies: - '@inquirer/core': 12.0.1(@types/node@24.13.3) - '@inquirer/type': 4.1.0(@types/node@24.13.3) + '@inquirer/core': 12.0.2(@types/node@26.4.1) + '@inquirer/type': 4.1.1(@types/node@26.4.1) optionalDependencies: - '@types/node': 24.13.3 + '@types/node': 26.4.1 - '@inquirer/external-editor@3.0.4(@types/node@24.13.3)': + '@inquirer/external-editor@3.0.5(@types/node@26.4.1)': dependencies: chardet: 2.2.0 iconv-lite: 0.7.3 optionalDependencies: - '@types/node': 24.13.3 + '@types/node': 26.4.1 - '@inquirer/figures@2.0.8': {} + '@inquirer/figures@2.0.9': {} - '@inquirer/input@5.1.4(@types/node@24.13.3)': + '@inquirer/input@5.1.5(@types/node@26.4.1)': dependencies: - '@inquirer/core': 12.0.1(@types/node@24.13.3) - '@inquirer/type': 4.1.0(@types/node@24.13.3) + '@inquirer/core': 12.0.2(@types/node@26.4.1) + '@inquirer/type': 4.1.1(@types/node@26.4.1) optionalDependencies: - '@types/node': 24.13.3 + '@types/node': 26.4.1 - '@inquirer/number@4.2.1(@types/node@24.13.3)': + '@inquirer/number@4.2.2(@types/node@26.4.1)': dependencies: - '@inquirer/core': 12.0.1(@types/node@24.13.3) - '@inquirer/type': 4.1.0(@types/node@24.13.3) + '@inquirer/core': 12.0.2(@types/node@26.4.1) + '@inquirer/type': 4.1.1(@types/node@26.4.1) optionalDependencies: - '@types/node': 24.13.3 + '@types/node': 26.4.1 - '@inquirer/password@5.2.0(@types/node@24.13.3)': + '@inquirer/password@5.2.1(@types/node@26.4.1)': dependencies: - '@inquirer/ansi': 2.0.7 - '@inquirer/core': 12.0.1(@types/node@24.13.3) - '@inquirer/type': 4.1.0(@types/node@24.13.3) + '@inquirer/ansi': 2.0.8 + '@inquirer/core': 12.0.2(@types/node@26.4.1) + '@inquirer/type': 4.1.1(@types/node@26.4.1) optionalDependencies: - '@types/node': 24.13.3 - - '@inquirer/prompts@8.5.2(@types/node@24.13.3)': - dependencies: - '@inquirer/checkbox': 5.2.3(@types/node@24.13.3) - '@inquirer/confirm': 6.3.0(@types/node@24.13.3) - '@inquirer/editor': 5.3.1(@types/node@24.13.3) - '@inquirer/expand': 5.1.3(@types/node@24.13.3) - '@inquirer/input': 5.1.4(@types/node@24.13.3) - '@inquirer/number': 4.2.1(@types/node@24.13.3) - '@inquirer/password': 5.2.0(@types/node@24.13.3) - '@inquirer/rawlist': 5.3.3(@types/node@24.13.3) - '@inquirer/search': 4.3.1(@types/node@24.13.3) - '@inquirer/select': 5.2.3(@types/node@24.13.3) + '@types/node': 26.4.1 + + '@inquirer/prompts@8.5.2(@types/node@26.4.1)': + dependencies: + '@inquirer/checkbox': 5.2.4(@types/node@26.4.1) + '@inquirer/confirm': 6.3.1(@types/node@26.4.1) + '@inquirer/editor': 5.3.2(@types/node@26.4.1) + '@inquirer/expand': 5.1.4(@types/node@26.4.1) + '@inquirer/input': 5.1.5(@types/node@26.4.1) + '@inquirer/number': 4.2.2(@types/node@26.4.1) + '@inquirer/password': 5.2.1(@types/node@26.4.1) + '@inquirer/rawlist': 5.3.4(@types/node@26.4.1) + '@inquirer/search': 4.3.2(@types/node@26.4.1) + '@inquirer/select': 5.2.4(@types/node@26.4.1) optionalDependencies: - '@types/node': 24.13.3 + '@types/node': 26.4.1 - '@inquirer/rawlist@5.3.3(@types/node@24.13.3)': + '@inquirer/rawlist@5.3.4(@types/node@26.4.1)': dependencies: - '@inquirer/core': 12.0.1(@types/node@24.13.3) - '@inquirer/type': 4.1.0(@types/node@24.13.3) + '@inquirer/core': 12.0.2(@types/node@26.4.1) + '@inquirer/type': 4.1.1(@types/node@26.4.1) optionalDependencies: - '@types/node': 24.13.3 + '@types/node': 26.4.1 - '@inquirer/search@4.3.1(@types/node@24.13.3)': + '@inquirer/search@4.3.2(@types/node@26.4.1)': dependencies: - '@inquirer/core': 12.0.1(@types/node@24.13.3) - '@inquirer/figures': 2.0.8 - '@inquirer/type': 4.1.0(@types/node@24.13.3) + '@inquirer/core': 12.0.2(@types/node@26.4.1) + '@inquirer/figures': 2.0.9 + '@inquirer/type': 4.1.1(@types/node@26.4.1) optionalDependencies: - '@types/node': 24.13.3 + '@types/node': 26.4.1 - '@inquirer/select@5.2.3(@types/node@24.13.3)': + '@inquirer/select@5.2.4(@types/node@26.4.1)': dependencies: - '@inquirer/ansi': 2.0.7 - '@inquirer/core': 12.0.1(@types/node@24.13.3) - '@inquirer/figures': 2.0.8 - '@inquirer/type': 4.1.0(@types/node@24.13.3) + '@inquirer/ansi': 2.0.8 + '@inquirer/core': 12.0.2(@types/node@26.4.1) + '@inquirer/figures': 2.0.9 + '@inquirer/type': 4.1.1(@types/node@26.4.1) optionalDependencies: - '@types/node': 24.13.3 + '@types/node': 26.4.1 - '@inquirer/type@4.1.0(@types/node@24.13.3)': + '@inquirer/type@4.1.1(@types/node@26.4.1)': optionalDependencies: - '@types/node': 24.13.3 + '@types/node': 26.4.1 '@isaacs/ttlcache@1.4.1': {} '@jest/schemas@29.6.3': dependencies: - '@sinclair/typebox': 0.27.10 + '@sinclair/typebox': 0.27.12 - '@jest/schemas@30.4.1': + '@jest/schemas@30.5.0': dependencies: - '@sinclair/typebox': 0.34.50 + '@sinclair/typebox': 0.34.52 '@jest/types@29.6.3': dependencies: '@jest/schemas': 29.6.3 '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 24.13.3 + '@types/node': 26.4.1 '@types/yargs': 17.0.35 chalk: 4.1.2 @@ -7981,7 +8587,7 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.6.0 - '@mdx-js/mdx@3.1.1': + '@mdx-js/mdx@3.1.1(supports-color@8.1.1)': dependencies: '@types/estree': 1.0.9 '@types/estree-jsx': 1.0.5 @@ -7993,14 +8599,14 @@ snapshots: estree-util-is-identifier-name: 3.0.0 estree-util-scope: 1.0.1 estree-walker: 3.0.3 - hast-util-to-jsx-runtime: 2.3.6 + hast-util-to-jsx-runtime: 2.3.6(supports-color@8.1.1) markdown-extensions: 2.0.0 recma-build-jsx: 1.0.0 recma-jsx: 1.0.1(acorn@8.18.0) recma-stringify: 1.0.0 - rehype-recma: 1.0.0 - remark-mdx: 3.1.1 - remark-parse: 11.0.0 + rehype-recma: 1.0.0(supports-color@8.1.1) + remark-mdx: 3.1.1(supports-color@8.1.1) + remark-parse: 11.0.0(supports-color@8.1.1) remark-rehype: 11.1.2 source-map: 0.7.6 unified: 11.0.5 @@ -8018,37 +8624,30 @@ snapshots: '@napi-rs/lzma-linux-x64-gnu@1.5.1': optional: true - '@napi-rs/wasm-runtime@1.2.3(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': - dependencies: - '@emnapi/core': 1.11.1 - '@emnapi/runtime': 1.11.1 - '@tybys/wasm-util': 0.10.3 - optional: true + '@next/env@16.3.4': {} - '@next/env@16.3.3': {} - - '@next/swc-darwin-arm64@16.3.3': + '@next/swc-darwin-arm64@16.3.4': optional: true - '@next/swc-darwin-x64@16.3.3': + '@next/swc-darwin-x64@16.3.4': optional: true - '@next/swc-linux-arm64-gnu@16.3.3': + '@next/swc-linux-arm64-gnu@16.3.4': optional: true - '@next/swc-linux-arm64-musl@16.3.3': + '@next/swc-linux-arm64-musl@16.3.4': optional: true - '@next/swc-linux-x64-gnu@16.3.3': + '@next/swc-linux-x64-gnu@16.3.4': optional: true - '@next/swc-linux-x64-musl@16.3.3': + '@next/swc-linux-x64-musl@16.3.4': optional: true - '@next/swc-win32-arm64-msvc@16.3.3': + '@next/swc-win32-arm64-msvc@16.3.4': optional: true - '@next/swc-win32-x64-msvc@16.3.3': + '@next/swc-win32-x64-msvc@16.3.4': optional: true '@octokit/auth-token@6.0.0': {} @@ -8120,120 +8719,120 @@ snapshots: dependencies: '@octokit/openapi-types': 29.0.1 - '@oxc-project/types@0.139.0': {} + '@oxc-project/types@0.148.0': {} - '@oxfmt/binding-android-arm-eabi@0.65.0': + '@oxfmt/binding-android-arm-eabi@0.66.0': optional: true - '@oxfmt/binding-android-arm64@0.65.0': + '@oxfmt/binding-android-arm64@0.66.0': optional: true - '@oxfmt/binding-darwin-arm64@0.65.0': + '@oxfmt/binding-darwin-arm64@0.66.0': optional: true - '@oxfmt/binding-darwin-x64@0.65.0': + '@oxfmt/binding-darwin-x64@0.66.0': optional: true - '@oxfmt/binding-freebsd-x64@0.65.0': + '@oxfmt/binding-freebsd-x64@0.66.0': optional: true - '@oxfmt/binding-linux-arm-gnueabihf@0.65.0': + '@oxfmt/binding-linux-arm-gnueabihf@0.66.0': optional: true - '@oxfmt/binding-linux-arm-musleabihf@0.65.0': + '@oxfmt/binding-linux-arm-musleabihf@0.66.0': optional: true - '@oxfmt/binding-linux-arm64-gnu@0.65.0': + '@oxfmt/binding-linux-arm64-gnu@0.66.0': optional: true - '@oxfmt/binding-linux-arm64-musl@0.65.0': + '@oxfmt/binding-linux-arm64-musl@0.66.0': optional: true - '@oxfmt/binding-linux-ppc64-gnu@0.65.0': + '@oxfmt/binding-linux-ppc64-gnu@0.66.0': optional: true - '@oxfmt/binding-linux-riscv64-gnu@0.65.0': + '@oxfmt/binding-linux-riscv64-gnu@0.66.0': optional: true - '@oxfmt/binding-linux-riscv64-musl@0.65.0': + '@oxfmt/binding-linux-riscv64-musl@0.66.0': optional: true - '@oxfmt/binding-linux-s390x-gnu@0.65.0': + '@oxfmt/binding-linux-s390x-gnu@0.66.0': optional: true - '@oxfmt/binding-linux-x64-gnu@0.65.0': + '@oxfmt/binding-linux-x64-gnu@0.66.0': optional: true - '@oxfmt/binding-linux-x64-musl@0.65.0': + '@oxfmt/binding-linux-x64-musl@0.66.0': optional: true - '@oxfmt/binding-openharmony-arm64@0.65.0': + '@oxfmt/binding-openharmony-arm64@0.66.0': optional: true - '@oxfmt/binding-win32-arm64-msvc@0.65.0': + '@oxfmt/binding-win32-arm64-msvc@0.66.0': optional: true - '@oxfmt/binding-win32-ia32-msvc@0.65.0': + '@oxfmt/binding-win32-ia32-msvc@0.66.0': optional: true - '@oxfmt/binding-win32-x64-msvc@0.65.0': + '@oxfmt/binding-win32-x64-msvc@0.66.0': optional: true - '@oxlint/binding-android-arm-eabi@1.80.0': + '@oxlint/binding-android-arm-eabi@1.81.0': optional: true - '@oxlint/binding-android-arm64@1.80.0': + '@oxlint/binding-android-arm64@1.81.0': optional: true - '@oxlint/binding-darwin-arm64@1.80.0': + '@oxlint/binding-darwin-arm64@1.81.0': optional: true - '@oxlint/binding-darwin-x64@1.80.0': + '@oxlint/binding-darwin-x64@1.81.0': optional: true - '@oxlint/binding-freebsd-x64@1.80.0': + '@oxlint/binding-freebsd-x64@1.81.0': optional: true - '@oxlint/binding-linux-arm-gnueabihf@1.80.0': + '@oxlint/binding-linux-arm-gnueabihf@1.81.0': optional: true - '@oxlint/binding-linux-arm-musleabihf@1.80.0': + '@oxlint/binding-linux-arm-musleabihf@1.81.0': optional: true - '@oxlint/binding-linux-arm64-gnu@1.80.0': + '@oxlint/binding-linux-arm64-gnu@1.81.0': optional: true - '@oxlint/binding-linux-arm64-musl@1.80.0': + '@oxlint/binding-linux-arm64-musl@1.81.0': optional: true - '@oxlint/binding-linux-ppc64-gnu@1.80.0': + '@oxlint/binding-linux-ppc64-gnu@1.81.0': optional: true - '@oxlint/binding-linux-riscv64-gnu@1.80.0': + '@oxlint/binding-linux-riscv64-gnu@1.81.0': optional: true - '@oxlint/binding-linux-riscv64-musl@1.80.0': + '@oxlint/binding-linux-riscv64-musl@1.81.0': optional: true - '@oxlint/binding-linux-s390x-gnu@1.80.0': + '@oxlint/binding-linux-s390x-gnu@1.81.0': optional: true - '@oxlint/binding-linux-x64-gnu@1.80.0': + '@oxlint/binding-linux-x64-gnu@1.81.0': optional: true - '@oxlint/binding-linux-x64-musl@1.80.0': + '@oxlint/binding-linux-x64-musl@1.81.0': optional: true - '@oxlint/binding-openharmony-arm64@1.80.0': + '@oxlint/binding-openharmony-arm64@1.81.0': optional: true - '@oxlint/binding-win32-arm64-msvc@1.80.0': + '@oxlint/binding-win32-arm64-msvc@1.81.0': optional: true - '@oxlint/binding-win32-ia32-msvc@1.80.0': + '@oxlint/binding-win32-ia32-msvc@1.81.0': optional: true - '@oxlint/binding-win32-x64-msvc@1.80.0': + '@oxlint/binding-win32-x64-msvc@1.81.0': optional: true '@phun-ky/typeof@2.0.3': {} @@ -8242,525 +8841,609 @@ snapshots: '@radix-ui/primitive@1.1.7': {} - '@radix-ui/react-accordion@1.2.20(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@radix-ui/react-accordion@1.2.20(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: '@radix-ui/primitive': 1.1.7 - '@radix-ui/react-collapsible': 1.1.20(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-collection': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.17)(react@19.2.3) - '@radix-ui/react-context': 1.2.2(@types/react@19.2.17)(react@19.2.3) - '@radix-ui/react-direction': 1.1.4(@types/react@19.2.17)(react@19.2.3) - '@radix-ui/react-id': 1.1.4(@types/react@19.2.17)(react@19.2.3) - '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.17)(react@19.2.3) - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) + '@radix-ui/react-collapsible': 1.1.20(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-collection': 1.1.15(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-direction': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-id': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.18 + '@types/react-dom': 19.2.5(@types/react@19.2.18) - '@radix-ui/react-arrow@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@radix-ui/react-arrow@1.1.15(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: - '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.18 + '@types/react-dom': 19.2.5(@types/react@19.2.18) - '@radix-ui/react-collapsible@1.1.20(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@radix-ui/react-collapsible@1.1.20(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: '@radix-ui/primitive': 1.1.7 - '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.17)(react@19.2.3) - '@radix-ui/react-context': 1.2.2(@types/react@19.2.17)(react@19.2.3) - '@radix-ui/react-id': 1.1.4(@types/react@19.2.17)(react@19.2.3) - '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.17)(react@19.2.3) - '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.17)(react@19.2.3) - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-id': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.18 + '@types/react-dom': 19.2.5(@types/react@19.2.18) - '@radix-ui/react-collection@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@radix-ui/react-collection@1.1.15(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: - '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.17)(react@19.2.3) - '@radix-ui/react-context': 1.2.2(@types/react@19.2.17)(react@19.2.3) - '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-slot': 1.3.3(@types/react@19.2.17)(react@19.2.3) - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-slot': 1.3.3(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.18 + '@types/react-dom': 19.2.5(@types/react@19.2.18) - '@radix-ui/react-compose-refs@1.1.5(@types/react@19.2.17)(react@19.2.3)': + '@radix-ui/react-compose-refs@1.1.5(@types/react@19.2.18)(react@19.2.8)': dependencies: - react: 19.2.3 + react: 19.2.8 optionalDependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 - '@radix-ui/react-context@1.2.2(@types/react@19.2.17)(react@19.2.3)': + '@radix-ui/react-context@1.2.2(@types/react@19.2.18)(react@19.2.8)': dependencies: - react: 19.2.3 + react: 19.2.8 optionalDependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 - '@radix-ui/react-dialog@1.1.23(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@radix-ui/react-dialog@1.1.23(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: '@radix-ui/primitive': 1.1.7 - '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.17)(react@19.2.3) - '@radix-ui/react-context': 1.2.2(@types/react@19.2.17)(react@19.2.3) - '@radix-ui/react-dismissable-layer': 1.1.19(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-focus-guards': 1.1.6(@types/react@19.2.17)(react@19.2.3) - '@radix-ui/react-focus-scope': 1.1.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-id': 1.1.4(@types/react@19.2.17)(react@19.2.3) - '@radix-ui/react-portal': 1.1.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-slot': 1.3.3(@types/react@19.2.17)(react@19.2.3) - '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.17)(react@19.2.3) - '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.17)(react@19.2.3) + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-dismissable-layer': 1.1.19(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-focus-guards': 1.1.6(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-focus-scope': 1.1.16(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-id': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-portal': 1.1.17(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-slot': 1.3.3(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.18)(react@19.2.8) aria-hidden: 1.2.6 - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) - react-remove-scroll: 2.7.2(@types/react@19.2.17)(react@19.2.3) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + react-remove-scroll: 2.7.2(@types/react@19.2.18)(react@19.2.8) optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.18 + '@types/react-dom': 19.2.5(@types/react@19.2.18) - '@radix-ui/react-direction@1.1.4(@types/react@19.2.17)(react@19.2.3)': + '@radix-ui/react-direction@1.1.4(@types/react@19.2.18)(react@19.2.8)': dependencies: - react: 19.2.3 + react: 19.2.8 optionalDependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 - '@radix-ui/react-dismissable-layer@1.1.19(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@radix-ui/react-dismissable-layer@1.1.19(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: '@radix-ui/primitive': 1.1.7 - '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.17)(react@19.2.3) - '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.17)(react@19.2.3) - '@radix-ui/react-use-effect-event': 0.0.5(@types/react@19.2.17)(react@19.2.3) - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-effect-event': 0.0.5(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.18 + '@types/react-dom': 19.2.5(@types/react@19.2.18) - '@radix-ui/react-focus-guards@1.1.6(@types/react@19.2.17)(react@19.2.3)': + '@radix-ui/react-focus-guards@1.1.6(@types/react@19.2.18)(react@19.2.8)': dependencies: - react: 19.2.3 + react: 19.2.8 optionalDependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 - '@radix-ui/react-focus-scope@1.1.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@radix-ui/react-focus-scope@1.1.16(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: - '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.17)(react@19.2.3) - '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.17)(react@19.2.3) - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.18 + '@types/react-dom': 19.2.5(@types/react@19.2.18) - '@radix-ui/react-id@1.1.4(@types/react@19.2.17)(react@19.2.3)': + '@radix-ui/react-id@1.1.4(@types/react@19.2.18)(react@19.2.8)': dependencies: - '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.17)(react@19.2.3) - react: 19.2.3 + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 optionalDependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 - '@radix-ui/react-navigation-menu@1.2.22(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@radix-ui/react-navigation-menu@1.2.22(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: '@radix-ui/primitive': 1.1.7 - '@radix-ui/react-collection': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.17)(react@19.2.3) - '@radix-ui/react-context': 1.2.2(@types/react@19.2.17)(react@19.2.3) - '@radix-ui/react-direction': 1.1.4(@types/react@19.2.17)(react@19.2.3) - '@radix-ui/react-dismissable-layer': 1.1.19(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-id': 1.1.4(@types/react@19.2.17)(react@19.2.3) - '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.17)(react@19.2.3) - '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.17)(react@19.2.3) - '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.17)(react@19.2.3) - '@radix-ui/react-use-previous': 1.1.4(@types/react@19.2.17)(react@19.2.3) - '@radix-ui/react-visually-hidden': 1.2.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) + '@radix-ui/react-collection': 1.1.15(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-direction': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-dismissable-layer': 1.1.19(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-id': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-previous': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-visually-hidden': 1.2.11(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.18 + '@types/react-dom': 19.2.5(@types/react@19.2.18) - '@radix-ui/react-popover@1.1.23(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@radix-ui/react-popover@1.1.23(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: '@radix-ui/primitive': 1.1.7 - '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.17)(react@19.2.3) - '@radix-ui/react-context': 1.2.2(@types/react@19.2.17)(react@19.2.3) - '@radix-ui/react-dismissable-layer': 1.1.19(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-focus-guards': 1.1.6(@types/react@19.2.17)(react@19.2.3) - '@radix-ui/react-focus-scope': 1.1.16(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-id': 1.1.4(@types/react@19.2.17)(react@19.2.3) - '@radix-ui/react-popper': 1.3.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-portal': 1.1.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-slot': 1.3.3(@types/react@19.2.17)(react@19.2.3) - '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.17)(react@19.2.3) + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-dismissable-layer': 1.1.19(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-focus-guards': 1.1.6(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-focus-scope': 1.1.16(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-id': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-popper': 1.3.7(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-portal': 1.1.17(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-slot': 1.3.3(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.18)(react@19.2.8) aria-hidden: 1.2.6 - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) - react-remove-scroll: 2.7.2(@types/react@19.2.17)(react@19.2.3) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + react-remove-scroll: 2.7.2(@types/react@19.2.18)(react@19.2.8) optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) - - '@radix-ui/react-popper@1.3.7(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': - dependencies: - '@floating-ui/react-dom': 2.1.9(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-arrow': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.17)(react@19.2.3) - '@radix-ui/react-context': 1.2.2(@types/react@19.2.17)(react@19.2.3) - '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.17)(react@19.2.3) - '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.17)(react@19.2.3) - '@radix-ui/react-use-rect': 1.1.4(@types/react@19.2.17)(react@19.2.3) - '@radix-ui/react-use-size': 1.1.4(@types/react@19.2.17)(react@19.2.3) + '@types/react': 19.2.18 + '@types/react-dom': 19.2.5(@types/react@19.2.18) + + '@radix-ui/react-popper@1.3.7(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': + dependencies: + '@floating-ui/react-dom': 2.1.9(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-arrow': 1.1.15(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-rect': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-size': 1.1.4(@types/react@19.2.18)(react@19.2.8) '@radix-ui/rect': 1.1.3 - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.18 + '@types/react-dom': 19.2.5(@types/react@19.2.18) - '@radix-ui/react-portal@1.1.17(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@radix-ui/react-portal@1.1.17(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: - '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.17)(react@19.2.3) - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.18 + '@types/react-dom': 19.2.5(@types/react@19.2.18) - '@radix-ui/react-presence@1.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@radix-ui/react-presence@1.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: - '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.17)(react@19.2.3) - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.18 + '@types/react-dom': 19.2.5(@types/react@19.2.18) - '@radix-ui/react-primitive@2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@radix-ui/react-primitive@2.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: - '@radix-ui/react-slot': 1.3.3(@types/react@19.2.17)(react@19.2.3) - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) + '@radix-ui/react-slot': 1.3.3(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.18 + '@types/react-dom': 19.2.5(@types/react@19.2.18) - '@radix-ui/react-roving-focus@1.1.19(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@radix-ui/react-roving-focus@1.1.19(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: '@radix-ui/primitive': 1.1.7 - '@radix-ui/react-collection': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.17)(react@19.2.3) - '@radix-ui/react-context': 1.2.2(@types/react@19.2.17)(react@19.2.3) - '@radix-ui/react-direction': 1.1.4(@types/react@19.2.17)(react@19.2.3) - '@radix-ui/react-id': 1.1.4(@types/react@19.2.17)(react@19.2.3) - '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.17)(react@19.2.3) - '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.17)(react@19.2.3) - '@radix-ui/react-use-is-hydrated': 0.1.3(@types/react@19.2.17)(react@19.2.3) - '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.17)(react@19.2.3) - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) + '@radix-ui/react-collection': 1.1.15(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-direction': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-id': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-is-hydrated': 0.1.3(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.18 + '@types/react-dom': 19.2.5(@types/react@19.2.18) - '@radix-ui/react-scroll-area@1.2.18(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@radix-ui/react-scroll-area@1.2.18(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: '@radix-ui/number': 1.1.3 '@radix-ui/primitive': 1.1.7 - '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.17)(react@19.2.3) - '@radix-ui/react-context': 1.2.2(@types/react@19.2.17)(react@19.2.3) - '@radix-ui/react-direction': 1.1.4(@types/react@19.2.17)(react@19.2.3) - '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.17)(react@19.2.3) - '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.17)(react@19.2.3) - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-direction': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-use-callback-ref': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.18 + '@types/react-dom': 19.2.5(@types/react@19.2.18) - '@radix-ui/react-slot@1.3.3(@types/react@19.2.17)(react@19.2.3)': + '@radix-ui/react-slot@1.3.3(@types/react@19.2.18)(react@19.2.8)': dependencies: - '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.17)(react@19.2.3) - react: 19.2.3 + '@radix-ui/react-compose-refs': 1.1.5(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 optionalDependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 - '@radix-ui/react-tabs@1.1.21(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@radix-ui/react-tabs@1.1.21(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: '@radix-ui/primitive': 1.1.7 - '@radix-ui/react-context': 1.2.2(@types/react@19.2.17)(react@19.2.3) - '@radix-ui/react-direction': 1.1.4(@types/react@19.2.17)(react@19.2.3) - '@radix-ui/react-id': 1.1.4(@types/react@19.2.17)(react@19.2.3) - '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-roving-focus': 1.1.19(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.17)(react@19.2.3) - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) + '@radix-ui/react-context': 1.2.2(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-direction': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-id': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-roving-focus': 1.1.19(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-use-controllable-state': 1.2.6(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.18 + '@types/react-dom': 19.2.5(@types/react@19.2.18) - '@radix-ui/react-use-callback-ref@1.1.4(@types/react@19.2.17)(react@19.2.3)': + '@radix-ui/react-use-callback-ref@1.1.4(@types/react@19.2.18)(react@19.2.8)': dependencies: - react: 19.2.3 + react: 19.2.8 optionalDependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 - '@radix-ui/react-use-controllable-state@1.2.6(@types/react@19.2.17)(react@19.2.3)': + '@radix-ui/react-use-controllable-state@1.2.6(@types/react@19.2.18)(react@19.2.8)': dependencies: '@radix-ui/primitive': 1.1.7 - '@radix-ui/react-use-effect-event': 0.0.5(@types/react@19.2.17)(react@19.2.3) - '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.17)(react@19.2.3) - react: 19.2.3 + '@radix-ui/react-use-effect-event': 0.0.5(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 optionalDependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 - '@radix-ui/react-use-effect-event@0.0.5(@types/react@19.2.17)(react@19.2.3)': + '@radix-ui/react-use-effect-event@0.0.5(@types/react@19.2.18)(react@19.2.8)': dependencies: - '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.17)(react@19.2.3) - react: 19.2.3 + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 optionalDependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 - '@radix-ui/react-use-is-hydrated@0.1.3(@types/react@19.2.17)(react@19.2.3)': + '@radix-ui/react-use-is-hydrated@0.1.3(@types/react@19.2.18)(react@19.2.8)': dependencies: - react: 19.2.3 + react: 19.2.8 optionalDependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 - '@radix-ui/react-use-layout-effect@1.1.4(@types/react@19.2.17)(react@19.2.3)': + '@radix-ui/react-use-layout-effect@1.1.4(@types/react@19.2.18)(react@19.2.8)': dependencies: - react: 19.2.3 + react: 19.2.8 optionalDependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 - '@radix-ui/react-use-previous@1.1.4(@types/react@19.2.17)(react@19.2.3)': + '@radix-ui/react-use-previous@1.1.4(@types/react@19.2.18)(react@19.2.8)': dependencies: - react: 19.2.3 + react: 19.2.8 optionalDependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 - '@radix-ui/react-use-rect@1.1.4(@types/react@19.2.17)(react@19.2.3)': + '@radix-ui/react-use-rect@1.1.4(@types/react@19.2.18)(react@19.2.8)': dependencies: '@radix-ui/rect': 1.1.3 - react: 19.2.3 + react: 19.2.8 optionalDependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 - '@radix-ui/react-use-size@1.1.4(@types/react@19.2.17)(react@19.2.3)': + '@radix-ui/react-use-size@1.1.4(@types/react@19.2.18)(react@19.2.8)': dependencies: - '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.17)(react@19.2.3) - react: 19.2.3 + '@radix-ui/react-use-layout-effect': 1.1.4(@types/react@19.2.18)(react@19.2.8) + react: 19.2.8 optionalDependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 - '@radix-ui/react-visually-hidden@1.2.11(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@radix-ui/react-visually-hidden@1.2.11(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8)': dependencies: - '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) + '@radix-ui/react-primitive': 2.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) optionalDependencies: - '@types/react': 19.2.17 - '@types/react-dom': 19.2.3(@types/react@19.2.17) + '@types/react': 19.2.18 + '@types/react-dom': 19.2.5(@types/react@19.2.18) '@radix-ui/rect@1.1.3': {} - '@react-native-community/slider@5.2.0': {} + '@react-native-community/slider@5.2.1': {} + + '@react-native/asset-utils@0.87.1': {} - '@react-native/assets-registry@0.86.0': {} + '@react-native/assets-registry@0.86.3': {} - '@react-native/babel-plugin-codegen@0.86.0(@babel/core@7.29.7)': + '@react-native/babel-plugin-codegen@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/traverse': 7.29.7 - '@react-native/codegen': 0.86.0(@babel/core@7.29.7) + '@babel/traverse': 7.29.8(supports-color@8.1.1) + '@react-native/codegen': 0.86.3(@babel/core@7.29.7(supports-color@8.1.1)) transitivePeerDependencies: - '@babel/core' - supports-color - '@react-native/babel-preset@0.86.0(@babel/core@7.29.7)': - dependencies: - '@babel/core': 7.29.7 - '@babel/plugin-proposal-export-default-from': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.29.7) - '@babel/plugin-syntax-export-default-from': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.29.7) - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.29.7) - '@babel/plugin-transform-async-generator-functions': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-async-to-generator': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-block-scoping': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-class-properties': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-classes': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-destructuring': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-flow-strip-types': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-for-of': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-modules-commonjs': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-named-capturing-groups-regex': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-nullish-coalescing-operator': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-optional-catch-binding': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-optional-chaining': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-private-methods': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-private-property-in-object': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-react-display-name': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-react-jsx': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-react-jsx-self': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-react-jsx-source': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-regenerator': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-runtime': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-typescript': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-unicode-regex': 7.29.7(@babel/core@7.29.7) - '@react-native/babel-plugin-codegen': 0.86.0(@babel/core@7.29.7) - babel-plugin-syntax-hermes-parser: 0.36.0 - babel-plugin-transform-flow-enums: 0.0.2(@babel/core@7.29.7) + '@react-native/babel-plugin-codegen@0.87.1(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': + dependencies: + '@babel/traverse': 7.29.8(supports-color@8.1.1) + '@react-native/codegen': 0.87.1(@babel/core@7.29.7(supports-color@8.1.1)) + transitivePeerDependencies: + - '@babel/core' + - supports-color + + '@react-native/babel-preset@0.87.1(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1)': + dependencies: + '@babel/core': 7.29.7(supports-color@8.1.1) + '@babel/plugin-proposal-export-default-from': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-syntax-export-default-from': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-transform-async-generator-functions': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-async-to-generator': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-block-scoping': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-transform-class-properties': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-classes': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-destructuring': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-flow-strip-types': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-transform-for-of': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-modules-commonjs': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-named-capturing-groups-regex': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-transform-nullish-coalescing-operator': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-transform-optional-catch-binding': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-transform-optional-chaining': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-private-methods': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-private-property-in-object': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-react-display-name': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-transform-react-jsx': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-react-jsx-self': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-transform-react-jsx-source': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-transform-regenerator': 7.29.8(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-transform-runtime': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-typescript': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-unicode-regex': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@react-native/babel-plugin-codegen': 0.87.1(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + babel-plugin-syntax-hermes-parser: 0.36.1 + babel-plugin-transform-flow-enums: 0.0.2(@babel/core@7.29.7(supports-color@8.1.1)) react-refresh: 0.14.2 transitivePeerDependencies: - supports-color - '@react-native/codegen@0.86.0(@babel/core@7.29.7)': + '@react-native/codegen@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.7 - '@babel/parser': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) + '@babel/parser': 7.29.8 hermes-parser: 0.36.0 invariant: 2.2.4 nullthrows: 1.1.1 tinyglobby: 0.2.17 yargs: 17.7.3 - '@react-native/community-cli-plugin@0.86.0': + '@react-native/codegen@0.87.1(@babel/core@7.29.7(supports-color@8.1.1))': + dependencies: + '@babel/core': 7.29.7(supports-color@8.1.1) + '@babel/parser': 7.29.8 + hermes-parser: 0.36.1 + invariant: 2.2.4 + nullthrows: 1.1.1 + tinyglobby: 0.2.17 + yargs: 17.7.3 + + '@react-native/community-cli-plugin@0.86.3(supports-color@8.1.1)': dependencies: - '@react-native/dev-middleware': 0.86.0 + '@react-native/dev-middleware': 0.86.3(supports-color@8.1.1) debug: 4.4.3(supports-color@8.1.1) invariant: 2.2.4 - metro: 0.84.4 - metro-config: 0.84.4 - metro-core: 0.84.4 + metro: 0.84.6(supports-color@8.1.1) + metro-config: 0.84.6(supports-color@8.1.1) + metro-core: 0.84.6 semver: 7.8.5 transitivePeerDependencies: - bufferutil - supports-color - utf-8-validate - '@react-native/debugger-frontend@0.86.0': {} - - '@react-native/debugger-shell@0.86.0': + '@react-native/community-cli-plugin@0.87.1(supports-color@8.1.1)': dependencies: - cross-spawn: 7.0.6 + '@react-native/asset-utils': 0.87.1 + '@react-native/dev-middleware': 0.87.1(supports-color@8.1.1) + debug: 4.4.3(supports-color@8.1.1) + invariant: 2.2.4 + metro: 0.87.0(supports-color@8.1.1) + semver: 7.8.5 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@react-native/debugger-frontend@0.86.3': {} + + '@react-native/debugger-frontend@0.87.1': {} + + '@react-native/debugger-shell@0.86.3(supports-color@8.1.1)': + dependencies: + cross-spawn: 7.0.6 + debug: 4.4.3(supports-color@8.1.1) + fb-dotslash: 0.5.8 + transitivePeerDependencies: + - supports-color + + '@react-native/debugger-shell@0.87.1(supports-color@8.1.1)': + dependencies: + cross-spawn: 7.0.6 debug: 4.4.3(supports-color@8.1.1) fb-dotslash: 0.5.8 transitivePeerDependencies: - supports-color - '@react-native/dev-middleware@0.86.0': + '@react-native/dev-middleware@0.86.3(supports-color@8.1.1)': + dependencies: + '@isaacs/ttlcache': 1.4.1 + '@react-native/debugger-frontend': 0.86.3 + '@react-native/debugger-shell': 0.86.3(supports-color@8.1.1) + chrome-launcher: 0.15.2(supports-color@8.1.1) + chromium-edge-launcher: 0.3.0(supports-color@8.1.1) + connect: 3.7.0(supports-color@8.1.1) + debug: 4.4.3(supports-color@8.1.1) + invariant: 2.2.4 + nullthrows: 1.1.1 + open: 7.4.2 + serve-static: 1.16.3(supports-color@8.1.1) + ws: 7.5.13 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@react-native/dev-middleware@0.87.1(supports-color@8.1.1)': dependencies: '@isaacs/ttlcache': 1.4.1 - '@react-native/debugger-frontend': 0.86.0 - '@react-native/debugger-shell': 0.86.0 - chrome-launcher: 0.15.2 - chromium-edge-launcher: 0.3.0 - connect: 3.7.0 + '@react-native/debugger-frontend': 0.87.1 + '@react-native/debugger-shell': 0.87.1(supports-color@8.1.1) + chrome-launcher: 0.15.2(supports-color@8.1.1) + chromium-edge-launcher: 0.3.0(supports-color@8.1.1) + connect: 3.7.0(supports-color@8.1.1) debug: 4.4.3(supports-color@8.1.1) invariant: 2.2.4 nullthrows: 1.1.1 open: 7.4.2 - serve-static: 1.16.3 - ws: 7.5.11 + serve-static: 1.16.3(supports-color@8.1.1) + ws: 7.5.13 transitivePeerDependencies: - bufferutil - supports-color - utf-8-validate - '@react-native/gradle-plugin@0.86.0': {} + '@react-native/gradle-plugin@0.86.3': {} - '@react-native/js-polyfills@0.86.0': {} + '@react-native/gradle-plugin@0.87.1': {} + + '@react-native/js-polyfills@0.86.3': {} '@react-native/normalize-colors@0.74.89': {} - '@react-native/normalize-colors@0.86.0': {} + '@react-native/normalize-colors@0.86.3': {} + + '@react-native/normalize-colors@0.87.1': {} - '@react-native/virtualized-lists@0.86.0(@types/react@19.2.17)(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3))(react@19.2.3)': + '@react-native/virtualized-lists@0.86.3(@types/react@19.2.18)(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)': dependencies: invariant: 2.2.4 nullthrows: 1.1.1 react: 19.2.3 - react-native: 0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3) + react-native: 0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) optionalDependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 - '@react-navigation/core@7.21.5(react@19.2.3)': + '@react-native/virtualized-lists@0.87.1(@types/react@19.2.18)(react-native@0.87.1(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.8)(supports-color@8.1.1))(react@19.2.8)': dependencies: - '@react-navigation/routers': 7.6.0 + invariant: 2.2.4 + nullthrows: 1.1.1 + react: 19.2.8 + react-native: 0.87.1(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.8)(supports-color@8.1.1) + optionalDependencies: + '@types/react': 19.2.18 + + '@react-native/virtualized-lists@0.87.1(@types/react@19.2.18)(react-native@0.87.1(@types/react@19.2.18)(react@19.2.8)(supports-color@8.1.1))(react@19.2.8)': + dependencies: + invariant: 2.2.4 + nullthrows: 1.1.1 + react: 19.2.8 + react-native: 0.87.1(@types/react@19.2.18)(react@19.2.8)(supports-color@8.1.1) + optionalDependencies: + '@types/react': 19.2.18 + + '@react-navigation/core@7.21.13(react@19.2.3)': + dependencies: + '@react-navigation/routers': 7.6.4 escape-string-regexp: 4.0.0 fast-deep-equal: 3.1.3 nanoid: 3.3.18 query-string: 7.1.3 react: 19.2.3 - react-is: 19.2.7 + react-is: 19.2.8 use-latest-callback: 0.2.6(react@19.2.3) use-sync-external-store: 1.6.0(react@19.2.3) - '@react-navigation/elements@2.9.30(@react-navigation/native@7.3.8(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3))(react@19.2.3))(react-native-safe-area-context@5.7.0(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3))(react@19.2.3))(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3))(react@19.2.3)': + '@react-navigation/elements@2.9.40(@react-navigation/native@7.3.18(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3))(react-native-safe-area-context@5.7.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)': dependencies: - '@react-navigation/native': 7.3.8(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3))(react@19.2.3) + '@react-navigation/native': 7.3.18(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) color: 4.2.3 react: 19.2.3 - react-native: 0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3) - react-native-safe-area-context: 5.7.0(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3))(react@19.2.3) + react-native: 0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) + react-native-safe-area-context: 5.7.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) use-latest-callback: 0.2.6(react@19.2.3) use-sync-external-store: 1.6.0(react@19.2.3) - '@react-navigation/native-stack@7.17.10(@react-navigation/native@7.3.8(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3))(react@19.2.3))(react-native-safe-area-context@5.7.0(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3))(react@19.2.3))(react-native-screens@4.25.2(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3))(react@19.2.3))(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3))(react@19.2.3)': + '@react-navigation/native-stack@7.18.10(@react-navigation/native@7.3.18(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3))(react-native-safe-area-context@5.7.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3))(react-native-screens@4.26.2(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)': dependencies: - '@react-navigation/elements': 2.9.30(@react-navigation/native@7.3.8(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3))(react@19.2.3))(react-native-safe-area-context@5.7.0(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3))(react@19.2.3))(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3))(react@19.2.3) - '@react-navigation/native': 7.3.8(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3))(react@19.2.3) + '@react-navigation/elements': 2.9.40(@react-navigation/native@7.3.18(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3))(react-native-safe-area-context@5.7.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) + '@react-navigation/native': 7.3.18(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) color: 4.2.3 react: 19.2.3 - react-native: 0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3) - react-native-safe-area-context: 5.7.0(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3))(react@19.2.3) - react-native-screens: 4.25.2(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3))(react@19.2.3) + react-native: 0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) + react-native-safe-area-context: 5.7.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) + react-native-screens: 4.26.2(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) sf-symbols-typescript: 2.2.0 warn-once: 0.1.1 transitivePeerDependencies: - '@react-native-masked-view/masked-view' - '@react-navigation/native@7.3.8(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3))(react@19.2.3)': + '@react-navigation/native@7.3.18(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)': dependencies: - '@react-navigation/core': 7.21.5(react@19.2.3) + '@react-navigation/core': 7.21.13(react@19.2.3) escape-string-regexp: 4.0.0 fast-deep-equal: 3.1.3 - nanoid: 3.3.15 + nanoid: 3.3.18 react: 19.2.3 - react-native: 0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3) - standard-navigation: 0.0.7 + react-native: 0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) + standard-navigation: 0.0.8(react@19.2.3) use-latest-callback: 0.2.6(react@19.2.3) - '@react-navigation/routers@7.6.0': + '@react-navigation/routers@7.6.4': dependencies: nanoid: 3.3.18 - '@release-it/conventional-changelog@12.0.0(conventional-commits-filter@6.0.1)(conventional-commits-parser@7.1.2)(release-it@21.0.2(@types/node@24.13.3))': + '@release-it/conventional-changelog@12.0.0(conventional-commits-filter@6.0.1)(conventional-commits-parser@7.1.2)(release-it@21.0.2(@types/node@26.4.1)(supports-color@8.1.1))': dependencies: '@conventional-changelog/git-client': 3.1.2(conventional-commits-filter@6.0.1)(conventional-commits-parser@7.1.2) concat-stream: 2.0.0 @@ -8768,59 +9451,55 @@ snapshots: conventional-changelog-angular: 9.4.0 conventional-changelog-conventionalcommits: 10.4.0 conventional-recommended-bump: 12.1.0 - release-it: 21.0.2(@types/node@24.13.3) + release-it: 21.0.2(@types/node@26.4.1)(supports-color@8.1.1) semver: 7.8.5 transitivePeerDependencies: - conventional-commits-filter - conventional-commits-parser - '@rolldown/binding-android-arm64@1.1.5': + '@rolldown/binding-android-arm-eabi@1.2.7': optional: true - '@rolldown/binding-darwin-arm64@1.1.5': + '@rolldown/binding-android-arm64@1.2.7': optional: true - '@rolldown/binding-darwin-x64@1.1.5': + '@rolldown/binding-darwin-arm64@1.2.7': optional: true - '@rolldown/binding-freebsd-x64@1.1.5': + '@rolldown/binding-darwin-x64@1.2.7': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.1.5': + '@rolldown/binding-freebsd-x64@1.2.7': optional: true - '@rolldown/binding-linux-arm64-gnu@1.1.5': + '@rolldown/binding-linux-arm-gnueabihf@1.2.7': optional: true - '@rolldown/binding-linux-arm64-musl@1.1.5': + '@rolldown/binding-linux-arm64-gnu@1.2.7': optional: true - '@rolldown/binding-linux-ppc64-gnu@1.1.5': + '@rolldown/binding-linux-arm64-musl@1.2.7': optional: true - '@rolldown/binding-linux-s390x-gnu@1.1.5': + '@rolldown/binding-linux-ppc64-gnu@1.2.7': optional: true - '@rolldown/binding-linux-x64-gnu@1.1.5': + '@rolldown/binding-linux-s390x-gnu@1.2.7': optional: true - '@rolldown/binding-linux-x64-musl@1.1.5': + '@rolldown/binding-linux-x64-gnu@1.2.7': optional: true - '@rolldown/binding-openharmony-arm64@1.1.5': + '@rolldown/binding-linux-x64-musl@1.2.7': optional: true - '@rolldown/binding-wasm32-wasi@1.1.5': - dependencies: - '@emnapi/core': 1.11.1 - '@emnapi/runtime': 1.11.1 - '@napi-rs/wasm-runtime': 1.2.3(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) + '@rolldown/binding-openharmony-arm64@1.2.7': optional: true - '@rolldown/binding-win32-arm64-msvc@1.1.5': + '@rolldown/binding-win32-arm64-msvc@1.2.7': optional: true - '@rolldown/binding-win32-x64-msvc@1.1.5': + '@rolldown/binding-win32-x64-msvc@1.2.7': optional: true '@rolldown/pluginutils@1.0.1': {} @@ -8918,14 +9597,6 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.63.1': optional: true - '@shikijs/core@4.3.1': - dependencies: - '@shikijs/primitive': 4.3.1 - '@shikijs/types': 4.3.1 - '@shikijs/vscode-textmate': 10.0.2 - '@types/hast': 3.0.5 - hast-util-to-html: 9.0.5 - '@shikijs/core@4.4.3': dependencies: '@shikijs/primitive': 4.4.3 @@ -8934,61 +9605,31 @@ snapshots: '@types/hast': 3.0.5 hast-util-to-html: 9.0.5 - '@shikijs/engine-javascript@4.3.1': - dependencies: - '@shikijs/types': 4.3.1 - '@shikijs/vscode-textmate': 10.0.2 - oniguruma-to-es: 4.3.6 - '@shikijs/engine-javascript@4.4.3': dependencies: '@shikijs/types': 4.4.3 '@shikijs/vscode-textmate': 10.0.2 oniguruma-to-es: 4.3.6 - '@shikijs/engine-oniguruma@4.3.1': - dependencies: - '@shikijs/types': 4.3.1 - '@shikijs/vscode-textmate': 10.0.2 - '@shikijs/engine-oniguruma@4.4.3': dependencies: '@shikijs/types': 4.4.3 '@shikijs/vscode-textmate': 10.0.2 - '@shikijs/langs@4.3.1': - dependencies: - '@shikijs/types': 4.3.1 - '@shikijs/langs@4.4.3': dependencies: '@shikijs/types': 4.4.3 - '@shikijs/primitive@4.3.1': - dependencies: - '@shikijs/types': 4.3.1 - '@shikijs/vscode-textmate': 10.0.2 - '@types/hast': 3.0.5 - '@shikijs/primitive@4.4.3': dependencies: '@shikijs/types': 4.4.3 '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.5 - '@shikijs/themes@4.3.1': - dependencies: - '@shikijs/types': 4.3.1 - '@shikijs/themes@4.4.3': dependencies: '@shikijs/types': 4.4.3 - '@shikijs/types@4.3.1': - dependencies: - '@shikijs/vscode-textmate': 10.0.2 - '@types/hast': 3.0.5 - '@shikijs/types@4.4.3': dependencies: '@shikijs/vscode-textmate': 10.0.2 @@ -9009,9 +9650,9 @@ snapshots: '@simple-libs/stream-utils@2.0.0': {} - '@sinclair/typebox@0.27.10': {} + '@sinclair/typebox@0.27.12': {} - '@sinclair/typebox@0.34.50': {} + '@sinclair/typebox@0.34.52': {} '@standard-schema/spec@1.1.0': {} @@ -9022,7 +9663,7 @@ snapshots: '@tailwindcss/node@4.3.3': dependencies: '@jridgewell/remapping': 2.3.5 - enhanced-resolve: 5.24.3 + enhanced-resolve: 5.24.5 jiti: 2.7.0 lightningcss: 1.32.0 magic-string: 0.30.21 @@ -9082,40 +9723,29 @@ snapshots: '@tailwindcss/postcss@4.3.3': dependencies: - '@alloc/quick-lru': 5.2.0 + '@alloc/quick-lru': 5.3.0 '@tailwindcss/node': 4.3.3 '@tailwindcss/oxide': 4.3.3 - postcss: 8.5.25 + postcss: 8.5.26 tailwindcss: 4.3.3 - '@ts-morph/common@0.28.1': - dependencies: - minimatch: 10.2.6 - path-browserify: 1.0.1 - tinyglobby: 0.2.17 - '@ts-morph/common@0.29.0': dependencies: minimatch: 10.2.6 path-browserify: 1.0.1 tinyglobby: 0.2.17 - '@tybys/wasm-util@0.10.3': - dependencies: - tslib: 2.8.1 - optional: true - '@types/babel__core@7.20.5': dependencies: - '@babel/parser': 7.29.7 - '@babel/types': 7.29.7 + '@babel/parser': 7.29.8 + '@babel/types': 7.29.8 '@types/babel__generator': 7.27.0 '@types/babel__template': 7.4.4 '@types/babel__traverse': 7.28.0 '@types/babel__generator@7.27.0': dependencies: - '@babel/types': 7.29.7 + '@babel/types': 7.29.8 '@types/babel__helper-module-imports@7.18.3': dependencies: @@ -9128,12 +9758,12 @@ snapshots: '@types/babel__template@7.4.4': dependencies: - '@babel/parser': 7.29.7 - '@babel/types': 7.29.7 + '@babel/parser': 7.29.8 + '@babel/types': 7.29.8 '@types/babel__traverse@7.28.0': dependencies: - '@babel/types': 7.29.7 + '@babel/types': 7.29.8 '@types/chai@5.2.3': dependencies: @@ -9269,6 +9899,9 @@ snapshots: '@types/estree@1.0.9': {} + '@types/gensync@1.0.5': + optional: true + '@types/geojson@7946.0.16': {} '@types/hast@3.0.5': @@ -9285,6 +9918,9 @@ snapshots: dependencies: '@types/istanbul-lib-report': 3.0.3 + '@types/jsesc@2.5.1': + optional: true + '@types/mdast@4.0.4': dependencies: '@types/unist': 3.0.3 @@ -9293,19 +9929,23 @@ snapshots: '@types/ms@2.1.0': {} - '@types/node@24.13.3': + '@types/node@26.4.1': dependencies: - undici-types: 7.18.2 + undici-types: 8.3.0 '@types/parse-path@7.1.0': dependencies: parse-path: 7.1.0 - '@types/react-dom@19.2.3(@types/react@19.2.17)': + '@types/react-dom@19.2.5(@types/react@19.2.18)': + dependencies: + '@types/react': 19.2.18 + + '@types/react-test-renderer@19.1.0': dependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 - '@types/react@19.2.17': + '@types/react@19.2.18': dependencies: csstype: 3.2.3 @@ -9324,7 +9964,69 @@ snapshots: dependencies: '@types/yargs-parser': 21.0.3 - '@ungap/structured-clone@1.3.3': {} + '@typescript/typescript-aix-ppc64@7.0.2': + optional: true + + '@typescript/typescript-darwin-arm64@7.0.2': + optional: true + + '@typescript/typescript-darwin-x64@7.0.2': + optional: true + + '@typescript/typescript-freebsd-arm64@7.0.2': + optional: true + + '@typescript/typescript-freebsd-x64@7.0.2': + optional: true + + '@typescript/typescript-linux-arm64@7.0.2': + optional: true + + '@typescript/typescript-linux-arm@7.0.2': + optional: true + + '@typescript/typescript-linux-loong64@7.0.2': + optional: true + + '@typescript/typescript-linux-mips64el@7.0.2': + optional: true + + '@typescript/typescript-linux-ppc64@7.0.2': + optional: true + + '@typescript/typescript-linux-riscv64@7.0.2': + optional: true + + '@typescript/typescript-linux-s390x@7.0.2': + optional: true + + '@typescript/typescript-linux-x64@7.0.2': + optional: true + + '@typescript/typescript-netbsd-arm64@7.0.2': + optional: true + + '@typescript/typescript-netbsd-x64@7.0.2': + optional: true + + '@typescript/typescript-openbsd-arm64@7.0.2': + optional: true + + '@typescript/typescript-openbsd-x64@7.0.2': + optional: true + + '@typescript/typescript-sunos-x64@7.0.2': + optional: true + + '@typescript/typescript-win32-arm64@7.0.2': + optional: true + + '@typescript/typescript-win32-x64@7.0.2': + optional: true + + '@typescript/typescript6@6.0.2': + dependencies: + '@typescript/old': typescript@6.0.3 '@ungap/structured-clone@1.4.0': {} @@ -9333,50 +10035,20 @@ snapshots: d3-selection: 3.0.0 d3-transition: 3.0.1(d3-selection@3.0.0) - '@vitest/expect@4.1.11': - dependencies: - '@standard-schema/spec': 1.1.0 - '@types/chai': 5.2.3 - '@vitest/spy': 4.1.11 - '@vitest/utils': 4.1.11 - chai: 6.2.2 - tinyrainbow: 3.1.1 - - '@vitest/mocker@4.1.11(vite@8.1.4(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.49.0)(tsx@4.23.13)(yaml@2.9.0))': + '@vitest/mocker@5.0.0(vite@8.2.2(@types/node@26.4.1)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.13)(yaml@2.9.0))': dependencies: - '@vitest/spy': 4.1.11 + '@jridgewell/trace-mapping': 0.3.31 + '@vitest/spy': 5.0.0 estree-walker: 3.0.3 - magic-string: 0.30.21 + magic-string: 1.2.3 optionalDependencies: - vite: 8.1.4(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.49.0)(tsx@4.23.13)(yaml@2.9.0) - - '@vitest/pretty-format@4.1.11': - dependencies: - tinyrainbow: 3.1.1 - - '@vitest/runner@4.1.11': - dependencies: - '@vitest/utils': 4.1.11 - pathe: 2.0.3 - - '@vitest/snapshot@4.1.11': - dependencies: - '@vitest/pretty-format': 4.1.11 - '@vitest/utils': 4.1.11 - magic-string: 0.30.21 - pathe: 2.0.3 + vite: 8.2.2(@types/node@26.4.1)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.13)(yaml@2.9.0) - '@vitest/spy@4.1.11': {} - - '@vitest/utils@4.1.11': - dependencies: - '@vitest/pretty-format': 4.1.11 - convert-source-map: 2.0.0 - tinyrainbow: 3.1.1 + '@vitest/spy@5.0.0': {} - '@xmldom/xmldom@0.8.13': {} + '@xmldom/xmldom@0.8.15': {} - '@xmldom/xmldom@0.9.10': {} + '@xmldom/xmldom@0.9.12': {} '@yuku-analyzer/binding-android-arm64@0.9.3': optional: true @@ -9428,7 +10100,7 @@ snapshots: accepts@2.0.0: dependencies: mime-types: 3.0.2 - negotiator: 1.0.0 + negotiator: 1.1.0 acorn-jsx@5.3.2(acorn@8.18.0): dependencies: @@ -9440,12 +10112,12 @@ snapshots: agent-base@9.0.0: {} - agent-cli-detector@0.1.2: {} + agent-cli-detector@0.1.7: {} ajv@8.20.0: dependencies: fast-deep-equal: 3.1.3 - fast-uri: 3.1.6 + fast-uri: 3.1.7 json-schema-traverse: 1.0.0 require-from-string: 2.0.2 @@ -9477,7 +10149,7 @@ snapshots: argparse@2.0.1: {} - argue-cli@3.1.0: {} + argue-cli@3.2.0: {} aria-hidden@1.2.6: dependencies: @@ -9497,33 +10169,33 @@ snapshots: dependencies: retry: 0.13.1 - babel-plugin-polyfill-corejs2@0.4.17(@babel/core@7.29.7): + babel-plugin-polyfill-corejs2@0.4.17(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1): dependencies: '@babel/compat-data': 7.29.7 - '@babel/core': 7.29.7 - '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.7) + '@babel/core': 7.29.7(supports-color@8.1.1) + '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) semver: 6.3.1 transitivePeerDependencies: - supports-color - babel-plugin-polyfill-corejs3@0.13.0(@babel/core@7.29.7): + babel-plugin-polyfill-corejs3@0.13.0(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1): dependencies: - '@babel/core': 7.29.7 - '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.7) - core-js-compat: 3.49.0 + '@babel/core': 7.29.7(supports-color@8.1.1) + '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + core-js-compat: 3.50.0 transitivePeerDependencies: - supports-color - babel-plugin-polyfill-regenerator@0.6.8(@babel/core@7.29.7): + babel-plugin-polyfill-regenerator@0.6.8(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1): dependencies: - '@babel/core': 7.29.7 - '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.7) + '@babel/core': 7.29.7(supports-color@8.1.1) + '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) transitivePeerDependencies: - supports-color babel-plugin-react-compiler@1.0.0: dependencies: - '@babel/types': 7.29.7 + '@babel/types': 7.29.8 babel-plugin-react-native-web@0.21.2: {} @@ -9535,72 +10207,72 @@ snapshots: dependencies: hermes-parser: 0.36.1 - babel-plugin-tester@12.0.0(@babel/core@7.29.7): + babel-plugin-tester@12.0.0(@babel/core@7.29.7(supports-color@8.1.1)): dependencies: '@-xun/fs': 2.0.0 - '@babel/core': 7.29.7 - core-js: 3.49.0 + '@babel/core': 7.29.7(supports-color@8.1.1) + core-js: 3.50.0 lodash.mergewith: 4.6.2 - prettier: 3.9.5 - pretty-format: 30.4.1 + prettier: 3.9.6 + pretty-format: 30.5.1 rejoinder: 2.1.0 strip-indent~3: strip-indent@3.0.0 type-fest: 4.41.0 - babel-plugin-transform-flow-enums@0.0.2(@babel/core@7.29.7): + babel-plugin-transform-flow-enums@0.0.2(@babel/core@7.29.7(supports-color@8.1.1)): dependencies: - '@babel/plugin-syntax-flow': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-syntax-flow': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) transitivePeerDependencies: - '@babel/core' - babel-preset-expo@57.0.2(@babel/core@7.29.7)(@babel/runtime@7.29.7)(expo@57.0.4)(react-refresh@0.14.2): - dependencies: - '@babel/generator': 7.29.7 - '@babel/helper-module-imports': 7.29.7 - '@babel/plugin-proposal-decorators': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-proposal-export-default-from': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.29.7) - '@babel/plugin-syntax-export-default-from': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.29.7) - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.29.7) - '@babel/plugin-transform-async-generator-functions': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-async-to-generator': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-block-scoping': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-class-properties': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-class-static-block': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-classes': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-destructuring': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-export-namespace-from': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-flow-strip-types': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-for-of': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-logical-assignment-operators': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-modules-commonjs': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-named-capturing-groups-regex': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-nullish-coalescing-operator': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-object-rest-spread': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-optional-catch-binding': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-optional-chaining': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-parameters': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-private-methods': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-private-property-in-object': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-react-display-name': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-react-jsx': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-react-jsx-development': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-react-pure-annotations': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-runtime': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-typescript': 7.29.7(@babel/core@7.29.7) - '@babel/plugin-transform-unicode-regex': 7.29.7(@babel/core@7.29.7) - '@babel/preset-typescript': 7.29.7(@babel/core@7.29.7) - '@react-native/babel-plugin-codegen': 0.86.0(@babel/core@7.29.7) + babel-preset-expo@57.0.10(@babel/core@7.29.7(supports-color@8.1.1))(@babel/runtime@7.29.7)(expo@57.0.19)(react-refresh@0.14.2)(supports-color@8.1.1): + dependencies: + '@babel/generator': 7.29.8 + '@babel/helper-module-imports': 7.29.7(supports-color@8.1.1) + '@babel/plugin-proposal-decorators': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-proposal-export-default-from': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-syntax-export-default-from': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-transform-async-generator-functions': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-async-to-generator': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-block-scoping': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-transform-class-properties': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-class-static-block': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-classes': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-destructuring': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-export-namespace-from': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-transform-flow-strip-types': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-transform-for-of': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-logical-assignment-operators': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-transform-modules-commonjs': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-named-capturing-groups-regex': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-transform-nullish-coalescing-operator': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-transform-object-rest-spread': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-optional-catch-binding': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-transform-optional-chaining': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-parameters': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-transform-private-methods': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-private-property-in-object': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-react-display-name': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-transform-react-jsx': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-react-jsx-development': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-react-pure-annotations': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/plugin-transform-runtime': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-typescript': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-unicode-regex': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1)) + '@babel/preset-typescript': 7.29.7(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) + '@react-native/babel-plugin-codegen': 0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(supports-color@8.1.1) babel-plugin-react-compiler: 1.0.0 babel-plugin-react-native-web: 0.21.2 babel-plugin-syntax-hermes-parser: 0.36.1 - babel-plugin-transform-flow-enums: 0.0.2(@babel/core@7.29.7) + babel-plugin-transform-flow-enums: 0.0.2(@babel/core@7.29.7(supports-color@8.1.1)) debug: 4.4.3(supports-color@8.1.1) react-refresh: 0.14.2 optionalDependencies: '@babel/runtime': 7.29.7 - expo: 57.0.4(@babel/core@7.29.7)(@expo/dom-webview@57.0.0)(@expo/metro-runtime@57.0.3)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo: 57.0.19(@babel/core@7.29.7(supports-color@8.1.1))(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1)(typescript@7.0.2) transitivePeerDependencies: - '@babel/core' - supports-color @@ -9639,13 +10311,13 @@ snapshots: dependencies: fill-range: 7.1.1 - browserslist@4.28.5: + browserslist@4.28.8: dependencies: baseline-browser-mapping: 2.11.20 caniuse-lite: 1.0.30001810 - electron-to-chromium: 1.5.389 - node-releases: 2.0.51 - update-browserslist-db: 1.2.3(browserslist@4.28.5) + electron-to-chromium: 1.5.420 + node-releases: 2.0.54 + update-browserslist-db: 1.3.2(browserslist@4.28.8) bser@2.1.1: dependencies: @@ -9711,21 +10383,21 @@ snapshots: dependencies: readdirp: 5.1.1 - chrome-launcher@0.15.2: + chrome-launcher@0.15.2(supports-color@8.1.1): dependencies: - '@types/node': 24.13.3 + '@types/node': 26.4.1 escape-string-regexp: 4.0.0 is-wsl: 2.2.0 - lighthouse-logger: 1.4.2 + lighthouse-logger: 1.4.2(supports-color@8.1.1) transitivePeerDependencies: - supports-color - chromium-edge-launcher@0.3.0: + chromium-edge-launcher@0.3.0(supports-color@8.1.1): dependencies: - '@types/node': 24.13.3 + '@types/node': 26.4.1 escape-string-regexp: 4.0.0 is-wsl: 2.2.0 - lighthouse-logger: 1.4.2 + lighthouse-logger: 1.4.2(supports-color@8.1.1) mkdirp: 1.0.4 transitivePeerDependencies: - supports-color @@ -9814,11 +10486,11 @@ snapshots: dependencies: mime-db: 1.54.0 - compression@1.8.1: + compression@1.8.1(supports-color@8.1.1): dependencies: bytes: 3.1.2 compressible: 2.0.18 - debug: 2.6.9 + debug: 2.6.9(supports-color@8.1.1) negotiator: 0.6.4 on-headers: 1.1.0 safe-buffer: 5.2.1 @@ -9837,15 +10509,17 @@ snapshots: confbox@0.2.4: {} - connect@3.7.0: + connect@3.7.0(supports-color@8.1.1): dependencies: - debug: 2.6.9 - finalhandler: 1.1.2 + debug: 2.6.9(supports-color@8.1.1) + finalhandler: 1.1.2(supports-color@8.1.1) parseurl: 1.3.3 utils-merge: 1.0.1 transitivePeerDependencies: - supports-color + content-type@2.1.0: {} + content-type@3.0.0: {} conventional-changelog-angular@9.4.0: @@ -9862,7 +10536,7 @@ snapshots: dependencies: '@conventional-changelog/template': 1.4.0 '@simple-libs/stream-utils': 2.0.0 - argue-cli: 3.1.0 + argue-cli: 3.2.0 conventional-commits-filter: 6.0.1 semver: 7.8.5 @@ -9871,7 +10545,7 @@ snapshots: '@conventional-changelog/git-client': 3.1.2(conventional-commits-filter@6.0.1)(conventional-commits-parser@7.1.2) '@simple-libs/hosted-git-info': 2.0.0 '@simple-libs/normalize-package-data': 1.0.0 - argue-cli: 3.1.0 + argue-cli: 3.2.0 conventional-changelog-preset-loader: 6.0.1 conventional-changelog-writer: 9.2.1 conventional-commits-filter: 6.0.1 @@ -9883,23 +10557,23 @@ snapshots: conventional-commits-parser@7.1.2: dependencies: '@simple-libs/stream-utils': 2.0.0 - argue-cli: 3.1.0 + argue-cli: 3.2.0 conventional-recommended-bump@12.1.0: dependencies: '@conventional-changelog/git-client': 3.1.2(conventional-commits-filter@6.0.1)(conventional-commits-parser@7.1.2) - argue-cli: 3.1.0 + argue-cli: 3.2.0 conventional-changelog-preset-loader: 6.0.1 conventional-commits-filter: 6.0.1 conventional-commits-parser: 7.1.2 convert-source-map@2.0.0: {} - core-js-compat@3.49.0: + core-js-compat@3.50.0: dependencies: - browserslist: 4.28.5 + browserslist: 4.28.8 - core-js@3.49.0: {} + core-js@3.50.0: {} cose-base@1.0.3: dependencies: @@ -9909,21 +10583,21 @@ snapshots: dependencies: layout-base: 2.0.1 - cosmiconfig-typescript-loader@6.3.0(@types/node@24.13.3)(cosmiconfig@9.0.2(typescript@6.0.3))(typescript@6.0.3): + cosmiconfig-typescript-loader@6.3.0(@types/node@26.4.1)(cosmiconfig@9.0.2(typescript@7.0.2))(typescript@7.0.2): dependencies: - '@types/node': 24.13.3 - cosmiconfig: 9.0.2(typescript@6.0.3) + '@types/node': 26.4.1 + cosmiconfig: 9.0.2(typescript@7.0.2) jiti: 2.6.1 - typescript: 6.0.3 + typescript: 7.0.2 - cosmiconfig@9.0.2(typescript@6.0.3): + cosmiconfig@9.0.2(typescript@7.0.2): dependencies: env-paths: 2.2.1 import-fresh: 3.3.1 js-yaml: 4.3.2 parse-json: 5.2.0 optionalDependencies: - typescript: 6.0.3 + typescript: 7.0.2 cross-fetch@3.2.0: dependencies: @@ -9943,6 +10617,8 @@ snapshots: csstype@3.2.3: {} + culori@4.0.2: {} + cytoscape-cose-bilkent@4.1.0(cytoscape@3.34.2): dependencies: cose-base: 1.0.3 @@ -10131,13 +10807,17 @@ snapshots: dayjs@1.11.23: {} - debug@2.6.9: + debug@2.6.9(supports-color@8.1.1): dependencies: ms: 2.0.0 + optionalDependencies: + supports-color: 8.1.1 - debug@3.2.7: + debug@3.2.7(supports-color@8.1.1): dependencies: ms: 2.1.3 + optionalDependencies: + supports-color: 8.1.1 debug@4.4.3(supports-color@8.1.1): dependencies: @@ -10205,17 +10885,20 @@ snapshots: ee-first@1.1.1: {} - electron-to-chromium@1.5.389: {} + electron-to-chromium@1.5.420: {} emoji-regex@10.6.0: {} emoji-regex@8.0.0: {} + empathic@2.0.1: + optional: true + encodeurl@1.0.2: {} encodeurl@2.0.0: {} - enhanced-resolve@5.24.3: + enhanced-resolve@5.24.5: dependencies: graceful-fs: 4.2.11 tapable: 2.3.3 @@ -10356,45 +11039,45 @@ snapshots: expect-type@1.4.0: {} - expo-asset@57.0.3(expo@57.0.4)(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3))(react@19.2.3)(typescript@6.0.3): + expo-asset@57.0.16(expo@57.0.19)(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1)(typescript@7.0.2): dependencies: - '@expo/image-utils': 0.11.1(typescript@6.0.3) - expo: 57.0.4(@babel/core@7.29.7)(@expo/dom-webview@57.0.0)(@expo/metro-runtime@57.0.3)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) - expo-constants: 57.0.3(expo@57.0.4)(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3)) + '@expo/image-utils': 0.11.5(supports-color@8.1.1)(typescript@7.0.2) + expo: 57.0.19(@babel/core@7.29.7(supports-color@8.1.1))(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1)(typescript@7.0.2) + expo-constants: 57.0.17(expo@57.0.19)(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(supports-color@8.1.1) react: 19.2.3 - react-native: 0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3) + react-native: 0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) transitivePeerDependencies: - supports-color - typescript - expo-constants@57.0.3(expo@57.0.4)(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3)): + expo-constants@57.0.17(expo@57.0.19)(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(supports-color@8.1.1): dependencies: - '@expo/env': 2.4.1 - expo: 57.0.4(@babel/core@7.29.7)(@expo/dom-webview@57.0.0)(@expo/metro-runtime@57.0.3)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) - react-native: 0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3) + '@expo/env': 2.4.3(supports-color@8.1.1) + expo: 57.0.19(@babel/core@7.29.7(supports-color@8.1.1))(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1)(typescript@7.0.2) + react-native: 0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) transitivePeerDependencies: - supports-color - expo-file-system@57.0.0(expo@57.0.4)(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3)): + expo-file-system@57.0.6(expo@57.0.19)(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1)): dependencies: - expo: 57.0.4(@babel/core@7.29.7)(@expo/dom-webview@57.0.0)(@expo/metro-runtime@57.0.3)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) - react-native: 0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3) + expo: 57.0.19(@babel/core@7.29.7(supports-color@8.1.1))(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1)(typescript@7.0.2) + react-native: 0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) - expo-font@57.0.0(expo@57.0.4)(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3))(react@19.2.3): + expo-font@57.0.3(expo@57.0.19)(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3): dependencies: - expo: 57.0.4(@babel/core@7.29.7)(@expo/dom-webview@57.0.0)(@expo/metro-runtime@57.0.3)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo: 57.0.19(@babel/core@7.29.7(supports-color@8.1.1))(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1)(typescript@7.0.2) fontfaceobserver: 2.3.0 react: 19.2.3 - react-native: 0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3) + react-native: 0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) - expo-keep-awake@57.0.0(expo@57.0.4)(react@19.2.3): + expo-keep-awake@57.0.1(expo@57.0.19)(react@19.2.3): dependencies: - expo: 57.0.4(@babel/core@7.29.7)(@expo/dom-webview@57.0.0)(@expo/metro-runtime@57.0.3)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo: 57.0.19(@babel/core@7.29.7(supports-color@8.1.1))(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1)(typescript@7.0.2) react: 19.2.3 - expo-modules-autolinking@57.0.5(typescript@6.0.3): + expo-modules-autolinking@57.0.12(supports-color@8.1.1)(typescript@7.0.2): dependencies: - '@expo/require-utils': 57.0.1(typescript@6.0.3) + '@expo/require-utils': 57.0.5(supports-color@8.1.1)(typescript@7.0.2) '@expo/spawn-async': 1.8.0 chalk: 4.1.2 commander: 7.2.0 @@ -10402,65 +11085,65 @@ snapshots: - supports-color - typescript - expo-modules-core@57.0.3(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3))(react@19.2.3): + expo-modules-core@57.0.15(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3): dependencies: - '@expo/expo-modules-macros-plugin': 0.3.0 - expo-modules-jsi: 57.0.1(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3)) + '@expo/expo-modules-macros-plugin': 0.6.1 + expo-modules-jsi: 57.0.7(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1)) invariant: 2.2.4 react: 19.2.3 - react-native: 0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3) + react-native: 0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) - expo-modules-jsi@57.0.1(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3)): + expo-modules-jsi@57.0.7(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1)): dependencies: - react-native: 0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3) + react-native: 0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) - expo-server@57.0.0: {} + expo-server@57.0.3: {} - expo-splash-screen@57.0.2(expo@57.0.4)(typescript@6.0.3): + expo-splash-screen@57.0.8(expo@57.0.19)(supports-color@8.1.1)(typescript@7.0.2): dependencies: - '@expo/config-plugins': 57.0.3(typescript@6.0.3) - '@expo/image-utils': 0.11.1(typescript@6.0.3) - expo: 57.0.4(@babel/core@7.29.7)(@expo/dom-webview@57.0.0)(@expo/metro-runtime@57.0.3)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + '@expo/config-plugins': 57.0.9(supports-color@8.1.1)(typescript@7.0.2) + '@expo/image-utils': 0.11.5(supports-color@8.1.1)(typescript@7.0.2) + expo: 57.0.19(@babel/core@7.29.7(supports-color@8.1.1))(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1)(typescript@7.0.2) xml2js: 0.6.0 transitivePeerDependencies: - supports-color - typescript - expo-status-bar@57.0.0(expo@57.0.4)(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3))(react@19.2.3): + expo-status-bar@57.0.1(expo@57.0.19)(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3): dependencies: - expo: 57.0.4(@babel/core@7.29.7)(@expo/dom-webview@57.0.0)(@expo/metro-runtime@57.0.3)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) + expo: 57.0.19(@babel/core@7.29.7(supports-color@8.1.1))(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1)(typescript@7.0.2) react: 19.2.3 - react-native: 0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3) + react-native: 0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) - expo@57.0.4(@babel/core@7.29.7)(@expo/dom-webview@57.0.0)(@expo/metro-runtime@57.0.3)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3))(react@19.2.3)(typescript@6.0.3): + expo@57.0.19(@babel/core@7.29.7(supports-color@8.1.1))(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(react-dom@19.2.3(react@19.2.3))(react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1)(typescript@7.0.2): dependencies: '@babel/runtime': 7.29.7 - '@expo/cli': 57.0.6(@expo/dom-webview@57.0.0)(@expo/metro-runtime@57.0.3)(expo-constants@57.0.3(expo@57.0.4)(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3)))(expo-font@57.0.0(expo@57.0.4)(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3))(react@19.2.3))(expo@57.0.4)(react-dom@19.2.3(react@19.2.3))(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) - '@expo/config': 57.0.3(typescript@6.0.3) - '@expo/config-plugins': 57.0.3(typescript@6.0.3) - '@expo/devtools': 57.0.0(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3))(react@19.2.3) - '@expo/fingerprint': 0.20.3 - '@expo/local-build-cache-provider': 57.0.2(typescript@6.0.3) - '@expo/log-box': 57.0.0(@expo/dom-webview@57.0.0)(expo@57.0.4)(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3))(react@19.2.3) - '@expo/metro': 56.0.0 - '@expo/metro-config': 57.0.3(expo@57.0.4)(typescript@6.0.3) - '@ungap/structured-clone': 1.3.3 - babel-preset-expo: 57.0.2(@babel/core@7.29.7)(@babel/runtime@7.29.7)(expo@57.0.4)(react-refresh@0.14.2) - expo-asset: 57.0.3(expo@57.0.4)(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3))(react@19.2.3)(typescript@6.0.3) - expo-constants: 57.0.3(expo@57.0.4)(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3)) - expo-file-system: 57.0.0(expo@57.0.4)(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3)) - expo-font: 57.0.0(expo@57.0.4)(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3))(react@19.2.3) - expo-keep-awake: 57.0.0(expo@57.0.4)(react@19.2.3) - expo-modules-autolinking: 57.0.5(typescript@6.0.3) - expo-modules-core: 57.0.3(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3))(react@19.2.3) + '@expo/cli': 57.0.21(@expo/dom-webview@57.0.1)(@expo/metro-runtime@57.0.15)(expo-constants@57.0.17(expo@57.0.19)(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(supports-color@8.1.1))(expo-font@57.0.3(expo@57.0.19)(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3))(expo@57.0.19)(react-dom@19.2.3(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1)(typescript@7.0.2) + '@expo/config': 57.0.9(supports-color@8.1.1)(typescript@7.0.2) + '@expo/config-plugins': 57.0.9(supports-color@8.1.1)(typescript@7.0.2) + '@expo/devtools': 57.0.1(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) + '@expo/fingerprint': 0.20.12(supports-color@8.1.1) + '@expo/local-build-cache-provider': 57.0.8(supports-color@8.1.1)(typescript@7.0.2) + '@expo/log-box': 57.0.4(@expo/dom-webview@57.0.1)(expo@57.0.19)(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) + '@expo/metro': 56.0.2(supports-color@8.1.1) + '@expo/metro-config': 57.0.12(expo@57.0.19)(supports-color@8.1.1)(typescript@7.0.2) + '@ungap/structured-clone': 1.4.0 + babel-preset-expo: 57.0.10(@babel/core@7.29.7(supports-color@8.1.1))(@babel/runtime@7.29.7)(expo@57.0.19)(react-refresh@0.14.2)(supports-color@8.1.1) + expo-asset: 57.0.16(expo@57.0.19)(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)(supports-color@8.1.1)(typescript@7.0.2) + expo-constants: 57.0.17(expo@57.0.19)(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(supports-color@8.1.1) + expo-file-system: 57.0.6(expo@57.0.19)(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1)) + expo-font: 57.0.3(expo@57.0.19)(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) + expo-keep-awake: 57.0.1(expo@57.0.19)(react@19.2.3) + expo-modules-autolinking: 57.0.12(supports-color@8.1.1)(typescript@7.0.2) + expo-modules-core: 57.0.15(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) pretty-format: 29.7.0 react: 19.2.3 - react-native: 0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3) + react-native: 0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) react-refresh: 0.14.2 whatwg-url-minimum: 0.1.2 optionalDependencies: - '@expo/dom-webview': 57.0.0(expo@57.0.4)(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3))(react@19.2.3) - '@expo/metro-runtime': 57.0.3(@expo/log-box@57.0.0)(expo@57.0.4)(react-dom@19.2.3(react@19.2.3))(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3))(react@19.2.3) + '@expo/dom-webview': 57.0.1(expo@57.0.19)(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) + '@expo/metro-runtime': 57.0.15(@expo/log-box@57.0.4)(expo@57.0.19)(react-dom@19.2.3(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) react-dom: 19.2.3(react@19.2.3) react-native-web: 0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) transitivePeerDependencies: @@ -10492,7 +11175,7 @@ snapshots: dependencies: fast-string-truncated-width: 3.0.3 - fast-uri@3.1.6: {} + fast-uri@3.1.7: {} fast-wrap-ansi@0.2.2: dependencies: @@ -10538,9 +11221,9 @@ snapshots: filter-obj@1.1.0: {} - finalhandler@1.1.2: + finalhandler@1.1.2(supports-color@8.1.1): dependencies: - debug: 2.6.9 + debug: 2.6.9(supports-color@8.1.1) encodeurl: 1.0.2 escape-html: 1.0.3 on-finished: 2.3.0 @@ -10554,32 +11237,32 @@ snapshots: fontfaceobserver@2.3.0: {} - framer-motion@13.1.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + framer-motion@13.2.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8): dependencies: - motion-dom: 13.1.1 + motion-dom: 13.2.0 motion-utils: 13.0.0 tslib: 2.8.1 optionalDependencies: - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) fresh@0.5.2: {} fsevents@2.3.3: optional: true - fumadocs-core@16.15.4(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.38.0(react@19.2.3))(next@16.3.3(@types/node@24.13.3)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.5.4): + fumadocs-core@16.15.5(@mdx-js/mdx@3.1.1(supports-color@8.1.1))(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.18)(lucide-react@1.39.0(react@19.2.8))(next@16.3.4(@babel/core@8.0.1)(@types/node@26.4.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(zod@4.5.4): dependencies: '@fumari/image-size': 0.1.0 estree-util-value-to-estree: 3.5.0 github-slugger: 2.0.0 - hast-util-to-estree: 3.1.3 - hast-util-to-jsx-runtime: 2.3.6 - mdast-util-mdx: 3.0.0 + hast-util-to-estree: 3.1.3(supports-color@8.1.1) + hast-util-to-jsx-runtime: 2.3.6(supports-color@8.1.1) + mdast-util-mdx: 3.0.0(supports-color@8.1.1) mdast-util-to-markdown: 2.1.2 npm-to-yarn: 3.2.0 - remark: 15.0.1 - remark-gfm: 4.0.1 + remark: 15.0.1(supports-color@8.1.1) + remark-gfm: 4.0.1(supports-color@8.1.1) remark-rehype: 11.1.2 scroll-into-view-if-needed: 3.1.0 shiki: 4.4.3 @@ -10590,30 +11273,30 @@ snapshots: yaml: 2.9.0 zbsearch: 4.0.0 optionalDependencies: - '@mdx-js/mdx': 3.1.1 + '@mdx-js/mdx': 3.1.1(supports-color@8.1.1) '@types/estree-jsx': 1.0.5 '@types/hast': 3.0.5 '@types/mdast': 4.0.4 - '@types/react': 19.2.17 - lucide-react: 1.38.0(react@19.2.3) - next: 16.3.3(@types/node@24.13.3)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) + '@types/react': 19.2.18 + lucide-react: 1.39.0(react@19.2.8) + next: 16.3.4(@babel/core@8.0.1)(@types/node@26.4.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) zod: 4.5.4 transitivePeerDependencies: - supports-color - fumadocs-mdx@15.4.0(@types/mdast@4.0.4)(@types/mdx@2.0.14)(@types/react@19.2.17)(fumadocs-core@16.15.4(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.38.0(react@19.2.3))(next@16.3.3(@types/node@24.13.3)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.5.4))(next@16.3.3(@types/node@24.13.3)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)(rolldown@1.1.5)(vite@8.1.4(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.49.0)(tsx@4.23.13)(yaml@2.9.0)): + fumadocs-mdx@15.4.0(@types/mdast@4.0.4)(@types/mdx@2.0.14)(@types/react@19.2.18)(fumadocs-core@16.15.5(@mdx-js/mdx@3.1.1(supports-color@8.1.1))(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.18)(lucide-react@1.39.0(react@19.2.8))(next@16.3.4(@babel/core@8.0.1)(@types/node@26.4.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(zod@4.5.4))(next@16.3.4(@babel/core@8.0.1)(@types/node@26.4.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react@19.2.8)(rolldown@1.2.7)(supports-color@8.1.1)(vite@8.2.2(@types/node@26.4.1)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.13)(yaml@2.9.0)): dependencies: - '@mdx-js/mdx': 3.1.1 + '@mdx-js/mdx': 3.1.1(supports-color@8.1.1) '@standard-schema/spec': 1.1.0 chokidar: 5.0.0 esbuild: 0.28.2 estree-util-value-to-estree: 3.5.0 - fumadocs-core: 16.15.4(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.38.0(react@19.2.3))(next@16.3.3(@types/node@24.13.3)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.5.4) + fumadocs-core: 16.15.5(@mdx-js/mdx@3.1.1(supports-color@8.1.1))(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.18)(lucide-react@1.39.0(react@19.2.8))(next@16.3.4(@babel/core@8.0.1)(@types/node@26.4.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(zod@4.5.4) github-slugger: 2.0.0 magic-string: 1.2.3 - mdast-util-mdx: 3.0.0 + mdast-util-mdx: 3.0.0(supports-color@8.1.1) picocolors: 1.1.1 picomatch: 4.0.7 tinyexec: 1.3.0 @@ -10628,25 +11311,25 @@ snapshots: optionalDependencies: '@types/mdast': 4.0.4 '@types/mdx': 2.0.14 - '@types/react': 19.2.17 - next: 16.3.3(@types/node@24.13.3)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - react: 19.2.3 - rolldown: 1.1.5 - vite: 8.1.4(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.49.0)(tsx@4.23.13)(yaml@2.9.0) + '@types/react': 19.2.18 + next: 16.3.4(@babel/core@8.0.1)(@types/node@26.4.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + react: 19.2.8 + rolldown: 1.2.7 + vite: 8.2.2(@types/node@26.4.1)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.13)(yaml@2.9.0) transitivePeerDependencies: - supports-color - fumadocs-typescript@5.3.0(6fbffc4d4f66bd8dfc78c084cbb42a44): + fumadocs-typescript@5.3.0(a2d293f82b7f1920866010133281da6b): dependencies: estree-util-value-to-estree: 3.5.0 - fumadocs-core: 16.15.4(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.38.0(react@19.2.3))(next@16.3.3(@types/node@24.13.3)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.5.4) - hast-util-to-estree: 3.1.3 - hast-util-to-jsx-runtime: 2.3.6 - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) - remark: 15.0.1 + fumadocs-core: 16.15.5(@mdx-js/mdx@3.1.1(supports-color@8.1.1))(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.18)(lucide-react@1.39.0(react@19.2.8))(next@16.3.4(@babel/core@8.0.1)(@types/node@26.4.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(zod@4.5.4) + hast-util-to-estree: 3.1.3(supports-color@8.1.1) + hast-util-to-jsx-runtime: 2.3.6(supports-color@8.1.1) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + remark: 15.0.1(supports-color@8.1.1) remark-rehype: 11.1.2 - shiki: 4.3.1 + shiki: 4.4.3 ts-morph: 28.0.0 unified: 11.0.5 unist-util-visit: 5.1.0 @@ -10654,42 +11337,42 @@ snapshots: '@types/estree': 1.0.9 '@types/hast': 3.0.5 '@types/mdast': 4.0.4 - '@types/react': 19.2.17 - fumadocs-ui: 16.15.4(@types/mdx@2.0.14)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(fumadocs-core@16.15.4(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.38.0(react@19.2.3))(next@16.3.3(@types/node@24.13.3)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.5.4))(next@16.3.3(@types/node@24.13.3)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tailwindcss@4.3.3) + '@types/react': 19.2.18 + fumadocs-ui: 16.15.5(@types/mdx@2.0.14)(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(fumadocs-core@16.15.5(@mdx-js/mdx@3.1.1(supports-color@8.1.1))(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.18)(lucide-react@1.39.0(react@19.2.8))(next@16.3.4(@babel/core@8.0.1)(@types/node@26.4.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(zod@4.5.4))(next@16.3.4(@babel/core@8.0.1)(@types/node@26.4.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(tailwindcss@4.3.3) transitivePeerDependencies: - supports-color - fumadocs-ui@16.15.4(@types/mdx@2.0.14)(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(fumadocs-core@16.15.4(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.38.0(react@19.2.3))(next@16.3.3(@types/node@24.13.3)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.5.4))(next@16.3.3(@types/node@24.13.3)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(tailwindcss@4.3.3): + fumadocs-ui@16.15.5(@types/mdx@2.0.14)(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(fumadocs-core@16.15.5(@mdx-js/mdx@3.1.1(supports-color@8.1.1))(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.18)(lucide-react@1.39.0(react@19.2.8))(next@16.3.4(@babel/core@8.0.1)(@types/node@26.4.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(zod@4.5.4))(next@16.3.4(@babel/core@8.0.1)(@types/node@26.4.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(tailwindcss@4.3.3): dependencies: - '@fuma-translate/react': 1.0.2(@types/react@19.2.17)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@fuma-translate/react': 1.0.2(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@fumadocs/tailwind': 0.1.1(tailwindcss@4.3.3) - '@radix-ui/react-accordion': 1.2.20(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-collapsible': 1.1.20(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-dialog': 1.1.23(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-direction': 1.1.4(@types/react@19.2.17)(react@19.2.3) - '@radix-ui/react-navigation-menu': 1.2.22(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-popover': 1.1.23(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-scroll-area': 1.2.18(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@radix-ui/react-slot': 1.3.3(@types/react@19.2.17)(react@19.2.3) - '@radix-ui/react-tabs': 1.1.21(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@radix-ui/react-accordion': 1.2.20(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-collapsible': 1.1.20(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-dialog': 1.1.23(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-direction': 1.1.4(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-navigation-menu': 1.2.22(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-popover': 1.1.23(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-presence': 1.1.10(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-scroll-area': 1.2.18(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + '@radix-ui/react-slot': 1.3.3(@types/react@19.2.18)(react@19.2.8) + '@radix-ui/react-tabs': 1.1.21(@types/react-dom@19.2.5(@types/react@19.2.18))(@types/react@19.2.18)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) class-variance-authority: 0.7.1 cnfast: 0.1.0 - fumadocs-core: 16.15.4(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.17)(lucide-react@1.38.0(react@19.2.3))(next@16.3.3(@types/node@24.13.3)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@4.5.4) - lucide-react: 1.38.0(react@19.2.3) - motion: 13.1.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - next-themes: 0.4.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) - react-remove-scroll: 2.7.2(@types/react@19.2.17)(react@19.2.3) + fumadocs-core: 16.15.5(@mdx-js/mdx@3.1.1(supports-color@8.1.1))(@types/estree-jsx@1.0.5)(@types/hast@3.0.5)(@types/mdast@4.0.4)(@types/react@19.2.18)(lucide-react@1.39.0(react@19.2.8))(next@16.3.4(@babel/core@8.0.1)(@types/node@26.4.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(supports-color@8.1.1)(zod@4.5.4) + lucide-react: 1.39.0(react@19.2.8) + motion: 13.2.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + next-themes: 0.4.6(react-dom@19.2.8(react@19.2.8))(react@19.2.8) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + react-remove-scroll: 2.7.2(@types/react@19.2.18)(react@19.2.8) rehype-raw: 7.0.0 scroll-into-view-if-needed: 3.1.0 shiki: 4.4.3 unist-util-visit: 5.1.0 optionalDependencies: '@types/mdx': 2.0.14 - '@types/react': 19.2.17 - next: 16.3.3(@types/node@24.13.3)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@types/react': 19.2.18 + next: 16.3.4(@babel/core@8.0.1)(@types/node@26.4.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) transitivePeerDependencies: - '@types/react-dom' - tailwindcss @@ -10704,11 +11387,11 @@ snapshots: get-nonce@1.0.1: {} - get-tsconfig@4.14.0: + get-tsconfig@4.14.3: dependencies: resolve-pkg-maps: 1.0.0 - get-uri@8.0.1: + get-uri@8.0.1(supports-color@8.1.1): dependencies: basic-ftp: 5.3.1 data-uri-to-buffer: 8.0.0 @@ -10784,7 +11467,7 @@ snapshots: web-namespaces: 2.0.1 zwitch: 2.0.4 - hast-util-to-estree@3.1.3: + hast-util-to-estree@3.1.3(supports-color@8.1.1): dependencies: '@types/estree': 1.0.9 '@types/estree-jsx': 1.0.5 @@ -10794,9 +11477,9 @@ snapshots: estree-util-attach-comments: 3.0.0 estree-util-is-identifier-name: 3.0.0 hast-util-whitespace: 3.0.0 - mdast-util-mdx-expression: 2.0.1 - mdast-util-mdx-jsx: 3.2.0 - mdast-util-mdxjs-esm: 2.0.1 + mdast-util-mdx-expression: 2.0.1(supports-color@8.1.1) + mdast-util-mdx-jsx: 3.2.0(supports-color@8.1.1) + mdast-util-mdxjs-esm: 2.0.1(supports-color@8.1.1) property-information: 7.2.0 space-separated-tokens: 2.0.2 style-to-js: 1.1.21 @@ -10819,7 +11502,7 @@ snapshots: stringify-entities: 4.0.4 zwitch: 2.0.4 - hast-util-to-jsx-runtime@2.3.6: + hast-util-to-jsx-runtime@2.3.6(supports-color@8.1.1): dependencies: '@types/estree': 1.0.9 '@types/hast': 3.0.5 @@ -10828,9 +11511,9 @@ snapshots: devlop: 1.1.0 estree-util-is-identifier-name: 3.0.0 hast-util-whitespace: 3.0.0 - mdast-util-mdx-expression: 2.0.1 - mdast-util-mdx-jsx: 3.2.0 - mdast-util-mdxjs-esm: 2.0.1 + mdast-util-mdx-expression: 2.0.1(supports-color@8.1.1) + mdast-util-mdx-jsx: 3.2.0(supports-color@8.1.1) + mdast-util-mdxjs-esm: 2.0.1(supports-color@8.1.1) property-information: 7.2.0 space-separated-tokens: 2.0.2 style-to-js: 1.1.21 @@ -10861,7 +11544,7 @@ snapshots: property-information: 7.2.0 space-separated-tokens: 2.0.2 - hermes-compiler@250829098.0.14: {} + hermes-compiler@250829098.0.17: {} hermes-estree@0.35.0: {} @@ -10895,7 +11578,7 @@ snapshots: statuses: 2.0.2 toidentifier: 1.0.1 - http-proxy-agent@9.1.0: + http-proxy-agent@9.1.0(supports-color@8.1.1): dependencies: agent-base: 9.0.0 debug: 4.4.3(supports-color@8.1.1) @@ -10904,14 +11587,14 @@ snapshots: - kerberos - supports-color - https-proxy-agent@7.0.6: + https-proxy-agent@7.0.6(supports-color@8.1.1): dependencies: agent-base: 7.1.4 debug: 4.4.3(supports-color@8.1.1) transitivePeerDependencies: - supports-color - https-proxy-agent@9.1.0: + https-proxy-agent@9.1.0(supports-color@8.1.1): dependencies: agent-base: 9.0.0 debug: 4.4.3(supports-color@8.1.1) @@ -11033,7 +11716,7 @@ snapshots: jest-util@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 24.13.3 + '@types/node': 26.4.1 chalk: 4.1.2 ci-info: 3.9.0 graceful-fs: 4.2.11 @@ -11050,7 +11733,7 @@ snapshots: jest-worker@29.7.0: dependencies: - '@types/node': 24.13.3 + '@types/node': 26.4.1 jest-util: 29.7.0 merge-stream: 2.0.0 supports-color: 8.1.1 @@ -11066,10 +11749,6 @@ snapshots: js-tokens@4.0.0: {} - js-yaml@4.3.0: - dependencies: - argparse: 2.0.1 - js-yaml@4.3.2: dependencies: argparse: 2.0.1 @@ -11102,9 +11781,9 @@ snapshots: leven@3.1.0: {} - lighthouse-logger@1.4.2: + lighthouse-logger@1.4.2(supports-color@8.1.1): dependencies: - debug: 2.6.9 + debug: 2.6.9(supports-color@8.1.1) marky: 1.3.0 transitivePeerDependencies: - supports-color @@ -11115,66 +11794,111 @@ snapshots: lightningcss-android-arm64@1.33.0: optional: true + lightningcss-darwin-arm64@1.30.1: + optional: true + lightningcss-darwin-arm64@1.32.0: optional: true lightningcss-darwin-arm64@1.33.0: optional: true + lightningcss-darwin-x64@1.30.1: + optional: true + lightningcss-darwin-x64@1.32.0: optional: true lightningcss-darwin-x64@1.33.0: optional: true + lightningcss-freebsd-x64@1.30.1: + optional: true + lightningcss-freebsd-x64@1.32.0: optional: true lightningcss-freebsd-x64@1.33.0: optional: true + lightningcss-linux-arm-gnueabihf@1.30.1: + optional: true + lightningcss-linux-arm-gnueabihf@1.32.0: optional: true lightningcss-linux-arm-gnueabihf@1.33.0: optional: true + lightningcss-linux-arm64-gnu@1.30.1: + optional: true + lightningcss-linux-arm64-gnu@1.32.0: optional: true lightningcss-linux-arm64-gnu@1.33.0: optional: true + lightningcss-linux-arm64-musl@1.30.1: + optional: true + lightningcss-linux-arm64-musl@1.32.0: optional: true lightningcss-linux-arm64-musl@1.33.0: optional: true + lightningcss-linux-x64-gnu@1.30.1: + optional: true + lightningcss-linux-x64-gnu@1.32.0: optional: true lightningcss-linux-x64-gnu@1.33.0: optional: true + lightningcss-linux-x64-musl@1.30.1: + optional: true + lightningcss-linux-x64-musl@1.32.0: optional: true lightningcss-linux-x64-musl@1.33.0: optional: true + lightningcss-win32-arm64-msvc@1.30.1: + optional: true + lightningcss-win32-arm64-msvc@1.32.0: optional: true lightningcss-win32-arm64-msvc@1.33.0: optional: true + lightningcss-win32-x64-msvc@1.30.1: + optional: true + lightningcss-win32-x64-msvc@1.32.0: optional: true lightningcss-win32-x64-msvc@1.33.0: optional: true + lightningcss@1.30.1: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-darwin-arm64: 1.30.1 + lightningcss-darwin-x64: 1.30.1 + lightningcss-freebsd-x64: 1.30.1 + lightningcss-linux-arm-gnueabihf: 1.30.1 + lightningcss-linux-arm64-gnu: 1.30.1 + lightningcss-linux-arm64-musl: 1.30.1 + lightningcss-linux-x64-gnu: 1.30.1 + lightningcss-linux-x64-musl: 1.30.1 + lightningcss-win32-arm64-msvc: 1.30.1 + lightningcss-win32-x64-msvc: 1.30.1 + lightningcss@1.32.0: dependencies: detect-libc: 2.1.2 @@ -11262,9 +11986,9 @@ snapshots: lru-cache@7.18.3: {} - lucide-react@1.38.0(react@19.2.3): + lucide-react@1.39.0(react@19.2.8): dependencies: - react: 19.2.3 + react: 19.2.8 macos-release@3.5.1: {} @@ -11295,14 +12019,14 @@ snapshots: unist-util-is: 6.0.1 unist-util-visit-parents: 6.0.2 - mdast-util-from-markdown@2.0.3: + mdast-util-from-markdown@2.0.3(supports-color@8.1.1): dependencies: '@types/mdast': 4.0.4 '@types/unist': 3.0.3 decode-named-character-reference: 1.3.0 devlop: 1.1.0 mdast-util-to-string: 4.0.0 - micromark: 4.0.2 + micromark: 4.0.2(supports-color@8.1.1) micromark-util-decode-numeric-character-reference: 2.0.2 micromark-util-decode-string: 2.0.1 micromark-util-normalize-identifier: 2.0.1 @@ -11320,67 +12044,67 @@ snapshots: mdast-util-find-and-replace: 3.0.2 micromark-util-character: 2.1.1 - mdast-util-gfm-footnote@2.1.0: + mdast-util-gfm-footnote@2.1.0(supports-color@8.1.1): dependencies: '@types/mdast': 4.0.4 devlop: 1.1.0 - mdast-util-from-markdown: 2.0.3 + mdast-util-from-markdown: 2.0.3(supports-color@8.1.1) mdast-util-to-markdown: 2.1.2 micromark-util-normalize-identifier: 2.0.1 transitivePeerDependencies: - supports-color - mdast-util-gfm-strikethrough@2.0.0: + mdast-util-gfm-strikethrough@2.0.0(supports-color@8.1.1): dependencies: '@types/mdast': 4.0.4 - mdast-util-from-markdown: 2.0.3 + mdast-util-from-markdown: 2.0.3(supports-color@8.1.1) mdast-util-to-markdown: 2.1.2 transitivePeerDependencies: - supports-color - mdast-util-gfm-table@2.0.0: + mdast-util-gfm-table@2.0.0(supports-color@8.1.1): dependencies: '@types/mdast': 4.0.4 devlop: 1.1.0 markdown-table: 3.0.4 - mdast-util-from-markdown: 2.0.3 + mdast-util-from-markdown: 2.0.3(supports-color@8.1.1) mdast-util-to-markdown: 2.1.2 transitivePeerDependencies: - supports-color - mdast-util-gfm-task-list-item@2.0.0: + mdast-util-gfm-task-list-item@2.0.0(supports-color@8.1.1): dependencies: '@types/mdast': 4.0.4 devlop: 1.1.0 - mdast-util-from-markdown: 2.0.3 + mdast-util-from-markdown: 2.0.3(supports-color@8.1.1) mdast-util-to-markdown: 2.1.2 transitivePeerDependencies: - supports-color - mdast-util-gfm@3.1.0: + mdast-util-gfm@3.1.0(supports-color@8.1.1): dependencies: - mdast-util-from-markdown: 2.0.3 + mdast-util-from-markdown: 2.0.3(supports-color@8.1.1) mdast-util-gfm-autolink-literal: 2.0.1 - mdast-util-gfm-footnote: 2.1.0 - mdast-util-gfm-strikethrough: 2.0.0 - mdast-util-gfm-table: 2.0.0 - mdast-util-gfm-task-list-item: 2.0.0 + mdast-util-gfm-footnote: 2.1.0(supports-color@8.1.1) + mdast-util-gfm-strikethrough: 2.0.0(supports-color@8.1.1) + mdast-util-gfm-table: 2.0.0(supports-color@8.1.1) + mdast-util-gfm-task-list-item: 2.0.0(supports-color@8.1.1) mdast-util-to-markdown: 2.1.2 transitivePeerDependencies: - supports-color - mdast-util-mdx-expression@2.0.1: + mdast-util-mdx-expression@2.0.1(supports-color@8.1.1): dependencies: '@types/estree-jsx': 1.0.5 '@types/hast': 3.0.5 '@types/mdast': 4.0.4 devlop: 1.1.0 - mdast-util-from-markdown: 2.0.3 + mdast-util-from-markdown: 2.0.3(supports-color@8.1.1) mdast-util-to-markdown: 2.1.2 transitivePeerDependencies: - supports-color - mdast-util-mdx-jsx@3.2.0: + mdast-util-mdx-jsx@3.2.0(supports-color@8.1.1): dependencies: '@types/estree-jsx': 1.0.5 '@types/hast': 3.0.5 @@ -11388,7 +12112,7 @@ snapshots: '@types/unist': 3.0.3 ccount: 2.0.1 devlop: 1.1.0 - mdast-util-from-markdown: 2.0.3 + mdast-util-from-markdown: 2.0.3(supports-color@8.1.1) mdast-util-to-markdown: 2.1.2 parse-entities: 4.0.2 stringify-entities: 4.0.4 @@ -11397,23 +12121,23 @@ snapshots: transitivePeerDependencies: - supports-color - mdast-util-mdx@3.0.0: + mdast-util-mdx@3.0.0(supports-color@8.1.1): dependencies: - mdast-util-from-markdown: 2.0.3 - mdast-util-mdx-expression: 2.0.1 - mdast-util-mdx-jsx: 3.2.0 - mdast-util-mdxjs-esm: 2.0.1 + mdast-util-from-markdown: 2.0.3(supports-color@8.1.1) + mdast-util-mdx-expression: 2.0.1(supports-color@8.1.1) + mdast-util-mdx-jsx: 3.2.0(supports-color@8.1.1) + mdast-util-mdxjs-esm: 2.0.1(supports-color@8.1.1) mdast-util-to-markdown: 2.1.2 transitivePeerDependencies: - supports-color - mdast-util-mdxjs-esm@2.0.1: + mdast-util-mdxjs-esm@2.0.1(supports-color@8.1.1): dependencies: '@types/estree-jsx': 1.0.5 '@types/hast': 3.0.5 '@types/mdast': 4.0.4 devlop: 1.1.0 - mdast-util-from-markdown: 2.0.3 + mdast-util-from-markdown: 2.0.3(supports-color@8.1.1) mdast-util-to-markdown: 2.1.2 transitivePeerDependencies: - supports-color @@ -11482,174 +12206,513 @@ snapshots: ts-dedent: 2.3.0 uuid: 14.0.2 - metro-babel-transformer@0.84.4: + metro-babel-transformer@0.84.5(supports-color@8.1.1): dependencies: - '@babel/core': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) flow-enums-runtime: 0.0.6 hermes-parser: 0.35.0 - metro-cache-key: 0.84.4 + metro-cache-key: 0.84.5 nullthrows: 1.1.1 transitivePeerDependencies: - supports-color - metro-cache-key@0.84.4: + metro-babel-transformer@0.84.6(supports-color@8.1.1): dependencies: + '@babel/core': 7.29.7(supports-color@8.1.1) flow-enums-runtime: 0.0.6 - - metro-cache@0.84.4: - dependencies: - exponential-backoff: 3.1.3 - flow-enums-runtime: 0.0.6 - https-proxy-agent: 7.0.6 - metro-core: 0.84.4 + hermes-parser: 0.35.0 + metro-cache-key: 0.84.6 + nullthrows: 1.1.1 transitivePeerDependencies: - supports-color - metro-config@0.84.4: + metro-babel-transformer@0.87.0(supports-color@8.1.1): dependencies: - connect: 3.7.0 + '@babel/core': 7.29.7(supports-color@8.1.1) flow-enums-runtime: 0.0.6 - jest-validate: 29.7.0 - metro: 0.84.4 - metro-cache: 0.84.4 - metro-core: 0.84.4 - metro-runtime: 0.84.4 - yaml: 2.9.0 + hermes-parser: 0.36.1 + metro-cache-key: 0.87.0 + nullthrows: 1.1.1 transitivePeerDependencies: - - bufferutil - supports-color - - utf-8-validate - - metro-core@0.84.4: - dependencies: - flow-enums-runtime: 0.0.6 - lodash.throttle: 4.1.1 - metro-resolver: 0.84.4 - metro-file-map@0.84.4: + metro-cache-key@0.84.5: dependencies: - debug: 4.4.3(supports-color@8.1.1) - fb-watchman: 2.0.2 flow-enums-runtime: 0.0.6 - graceful-fs: 4.2.11 - invariant: 2.2.4 - jest-worker: 29.7.0 - micromatch: 4.0.8 - nullthrows: 1.1.1 - walker: 1.0.8 - transitivePeerDependencies: - - supports-color - metro-minify-terser@0.84.4: + metro-cache-key@0.84.6: dependencies: flow-enums-runtime: 0.0.6 - terser: 5.49.0 - metro-resolver@0.84.4: + metro-cache-key@0.87.0: dependencies: flow-enums-runtime: 0.0.6 - metro-runtime@0.84.4: + metro-cache@0.84.5(supports-color@8.1.1): dependencies: - '@babel/runtime': 7.29.7 + exponential-backoff: 3.1.3 flow-enums-runtime: 0.0.6 + https-proxy-agent: 7.0.6(supports-color@8.1.1) + metro-core: 0.84.5 + transitivePeerDependencies: + - supports-color - metro-source-map@0.84.4: + metro-cache@0.84.6(supports-color@8.1.1): dependencies: - '@babel/traverse': 7.29.7 - '@babel/types': 7.29.7 + exponential-backoff: 3.1.3 flow-enums-runtime: 0.0.6 - invariant: 2.2.4 - metro-symbolicate: 0.84.4 - nullthrows: 1.1.1 - ob1: 0.84.4 - source-map: 0.5.7 - vlq: 1.0.1 + https-proxy-agent: 7.0.6(supports-color@8.1.1) + metro-core: 0.84.6 transitivePeerDependencies: - supports-color - metro-symbolicate@0.84.4: + metro-cache@0.87.0(supports-color@8.1.1): dependencies: + exponential-backoff: 3.1.3 flow-enums-runtime: 0.0.6 - invariant: 2.2.4 - metro-source-map: 0.84.4 - nullthrows: 1.1.1 - source-map: 0.5.7 - vlq: 1.0.1 + https-proxy-agent: 7.0.6(supports-color@8.1.1) + metro-core: 0.87.0 transitivePeerDependencies: - supports-color - metro-transform-plugins@0.84.4: + metro-config@0.84.5(supports-color@8.1.1): dependencies: - '@babel/core': 7.29.7 - '@babel/generator': 7.29.7 + connect: 3.7.0(supports-color@8.1.1) + flow-enums-runtime: 0.0.6 + jest-validate: 29.7.0 + metro: 0.84.5(supports-color@8.1.1) + metro-cache: 0.84.5(supports-color@8.1.1) + metro-core: 0.84.5 + metro-runtime: 0.84.5 + yaml: 2.9.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + metro-config@0.84.6(supports-color@8.1.1): + dependencies: + connect: 3.7.0(supports-color@8.1.1) + flow-enums-runtime: 0.0.6 + jest-validate: 29.7.0 + metro: 0.84.6(supports-color@8.1.1) + metro-cache: 0.84.6(supports-color@8.1.1) + metro-core: 0.84.6 + metro-runtime: 0.84.6 + yaml: 2.9.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + metro-config@0.87.0(supports-color@8.1.1): + dependencies: + connect: 3.7.0(supports-color@8.1.1) + flow-enums-runtime: 0.0.6 + jest-validate: 29.7.0 + metro: 0.87.0(supports-color@8.1.1) + metro-cache: 0.87.0(supports-color@8.1.1) + metro-core: 0.87.0 + metro-runtime: 0.87.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + metro-core@0.84.5: + dependencies: + flow-enums-runtime: 0.0.6 + lodash.throttle: 4.1.1 + metro-resolver: 0.84.5 + + metro-core@0.84.6: + dependencies: + flow-enums-runtime: 0.0.6 + lodash.throttle: 4.1.1 + metro-resolver: 0.84.6 + + metro-core@0.87.0: + dependencies: + flow-enums-runtime: 0.0.6 + lodash.throttle: 4.1.1 + metro-resolver: 0.87.0 + + metro-file-map@0.84.5(supports-color@8.1.1): + dependencies: + debug: 4.4.3(supports-color@8.1.1) + fb-watchman: 2.0.2 + flow-enums-runtime: 0.0.6 + graceful-fs: 4.2.11 + invariant: 2.2.4 + jest-worker: 29.7.0 + micromatch: 4.0.8 + nullthrows: 1.1.1 + walker: 1.0.8 + transitivePeerDependencies: + - supports-color + + metro-file-map@0.84.6(supports-color@8.1.1): + dependencies: + debug: 4.4.3(supports-color@8.1.1) + fb-watchman: 2.0.2 + flow-enums-runtime: 0.0.6 + graceful-fs: 4.2.11 + invariant: 2.2.4 + jest-worker: 29.7.0 + micromatch: 4.0.8 + nullthrows: 1.1.1 + walker: 1.0.8 + transitivePeerDependencies: + - supports-color + + metro-file-map@0.87.0(supports-color@8.1.1): + dependencies: + debug: 4.4.3(supports-color@8.1.1) + fb-watchman: 2.0.2 + flow-enums-runtime: 0.0.6 + graceful-fs: 4.2.11 + invariant: 2.2.4 + jest-worker: 29.7.0 + micromatch: 4.0.8 + nullthrows: 1.1.1 + walker: 1.0.8 + transitivePeerDependencies: + - supports-color + + metro-minify-terser@0.84.5: + dependencies: + flow-enums-runtime: 0.0.6 + terser: 5.51.2 + + metro-minify-terser@0.84.6: + dependencies: + flow-enums-runtime: 0.0.6 + terser: 5.51.2 + + metro-minify-terser@0.87.0: + dependencies: + flow-enums-runtime: 0.0.6 + terser: 5.51.2 + + metro-resolver@0.84.5: + dependencies: + flow-enums-runtime: 0.0.6 + + metro-resolver@0.84.6: + dependencies: + flow-enums-runtime: 0.0.6 + + metro-resolver@0.87.0: + dependencies: + flow-enums-runtime: 0.0.6 + + metro-runtime@0.84.5: + dependencies: + '@babel/runtime': 7.29.7 + flow-enums-runtime: 0.0.6 + + metro-runtime@0.84.6: + dependencies: + '@babel/runtime': 7.29.7 + flow-enums-runtime: 0.0.6 + + metro-runtime@0.87.0: + dependencies: + '@babel/runtime': 7.29.7 + flow-enums-runtime: 0.0.6 + + metro-source-map@0.84.5(supports-color@8.1.1): + dependencies: + '@babel/traverse': 7.29.8(supports-color@8.1.1) + '@babel/types': 7.29.8 + flow-enums-runtime: 0.0.6 + invariant: 2.2.4 + metro-symbolicate: 0.84.5 + nullthrows: 1.1.1 + ob1: 0.84.5 + source-map: 0.5.7 + vlq: 1.0.1 + transitivePeerDependencies: + - supports-color + + metro-source-map@0.84.6(supports-color@8.1.1): + dependencies: + '@babel/traverse': 7.29.8(supports-color@8.1.1) + '@babel/types': 7.29.8 + flow-enums-runtime: 0.0.6 + invariant: 2.2.4 + metro-symbolicate: 0.84.6 + nullthrows: 1.1.1 + ob1: 0.84.6 + source-map: 0.5.7 + vlq: 1.0.1 + transitivePeerDependencies: + - supports-color + + metro-source-map@0.87.0(supports-color@8.1.1): + dependencies: + '@babel/traverse': 7.29.8(supports-color@8.1.1) + '@babel/types': 7.29.8 + flow-enums-runtime: 0.0.6 + invariant: 2.2.4 + metro-symbolicate: 0.87.0 + nullthrows: 1.1.1 + ob1: 0.87.0 + source-map: 0.5.7 + vlq: 1.0.1 + transitivePeerDependencies: + - supports-color + + metro-symbolicate@0.84.5: + dependencies: + flow-enums-runtime: 0.0.6 + invariant: 2.2.4 + metro-source-map: 0.84.5(supports-color@8.1.1) + nullthrows: 1.1.1 + source-map: 0.5.7 + vlq: 1.0.1 + + metro-symbolicate@0.84.6: + dependencies: + flow-enums-runtime: 0.0.6 + invariant: 2.2.4 + metro-source-map: 0.84.6(supports-color@8.1.1) + nullthrows: 1.1.1 + source-map: 0.5.7 + vlq: 1.0.1 + + metro-symbolicate@0.87.0: + dependencies: + flow-enums-runtime: 0.0.6 + invariant: 2.2.4 + metro-source-map: 0.87.0(supports-color@8.1.1) + nullthrows: 1.1.1 + source-map: 0.5.7 + vlq: 1.0.1 + + metro-transform-plugins@0.84.5(supports-color@8.1.1): + dependencies: + '@babel/core': 7.29.7(supports-color@8.1.1) + '@babel/generator': 7.29.8 + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.8(supports-color@8.1.1) + flow-enums-runtime: 0.0.6 + nullthrows: 1.1.1 + transitivePeerDependencies: + - supports-color + + metro-transform-plugins@0.84.6(supports-color@8.1.1): + dependencies: + '@babel/core': 7.29.7(supports-color@8.1.1) + '@babel/generator': 7.29.8 + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.8(supports-color@8.1.1) + flow-enums-runtime: 0.0.6 + nullthrows: 1.1.1 + transitivePeerDependencies: + - supports-color + + metro-transform-plugins@0.87.0(supports-color@8.1.1): + dependencies: + '@babel/core': 7.29.7(supports-color@8.1.1) + '@babel/generator': 7.29.8 '@babel/template': 7.29.7 - '@babel/traverse': 7.29.7 + '@babel/traverse': 7.29.8(supports-color@8.1.1) + flow-enums-runtime: 0.0.6 + nullthrows: 1.1.1 + transitivePeerDependencies: + - supports-color + + metro-transform-worker@0.84.5(supports-color@8.1.1): + dependencies: + '@babel/core': 7.29.7(supports-color@8.1.1) + '@babel/generator': 7.29.8 + '@babel/parser': 7.29.8 + '@babel/types': 7.29.8 + flow-enums-runtime: 0.0.6 + metro: 0.84.5(supports-color@8.1.1) + metro-babel-transformer: 0.84.5(supports-color@8.1.1) + metro-cache: 0.84.5(supports-color@8.1.1) + metro-cache-key: 0.84.5 + metro-minify-terser: 0.84.5 + metro-source-map: 0.84.5(supports-color@8.1.1) + metro-transform-plugins: 0.84.5(supports-color@8.1.1) + nullthrows: 1.1.1 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + metro-transform-worker@0.84.6(supports-color@8.1.1): + dependencies: + '@babel/core': 7.29.7(supports-color@8.1.1) + '@babel/generator': 7.29.8 + '@babel/parser': 7.29.8 + '@babel/types': 7.29.8 + flow-enums-runtime: 0.0.6 + metro: 0.84.6(supports-color@8.1.1) + metro-babel-transformer: 0.84.6(supports-color@8.1.1) + metro-cache: 0.84.6(supports-color@8.1.1) + metro-cache-key: 0.84.6 + metro-minify-terser: 0.84.6 + metro-source-map: 0.84.6(supports-color@8.1.1) + metro-transform-plugins: 0.84.6(supports-color@8.1.1) + nullthrows: 1.1.1 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + metro-transform-worker@0.87.0(supports-color@8.1.1): + dependencies: + '@babel/core': 7.29.7(supports-color@8.1.1) + '@babel/generator': 7.29.8 + '@babel/parser': 7.29.8 + '@babel/types': 7.29.8 flow-enums-runtime: 0.0.6 + metro: 0.87.0(supports-color@8.1.1) + metro-babel-transformer: 0.87.0(supports-color@8.1.1) + metro-cache: 0.87.0(supports-color@8.1.1) + metro-cache-key: 0.87.0 + metro-minify-terser: 0.87.0 + metro-source-map: 0.87.0(supports-color@8.1.1) + metro-transform-plugins: 0.87.0(supports-color@8.1.1) nullthrows: 1.1.1 transitivePeerDependencies: + - bufferutil - supports-color + - utf-8-validate - metro-transform-worker@0.84.4: + metro@0.84.5(supports-color@8.1.1): dependencies: - '@babel/core': 7.29.7 - '@babel/generator': 7.29.7 - '@babel/parser': 7.29.7 - '@babel/types': 7.29.7 + '@babel/code-frame': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) + '@babel/generator': 7.29.8 + '@babel/parser': 7.29.8 + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.8(supports-color@8.1.1) + '@babel/types': 7.29.8 + accepts: 2.0.0 + ci-info: 2.0.0 + connect: 3.7.0(supports-color@8.1.1) + debug: 4.4.3(supports-color@8.1.1) + error-stack-parser: 2.1.4 flow-enums-runtime: 0.0.6 - metro: 0.84.4 - metro-babel-transformer: 0.84.4 - metro-cache: 0.84.4 - metro-cache-key: 0.84.4 - metro-minify-terser: 0.84.4 - metro-source-map: 0.84.4 - metro-transform-plugins: 0.84.4 + graceful-fs: 4.2.11 + hermes-parser: 0.35.0 + invariant: 2.2.4 + jest-worker: 29.7.0 + jsc-safe-url: 0.2.4 + lodash.throttle: 4.1.1 + metro-babel-transformer: 0.84.5(supports-color@8.1.1) + metro-cache: 0.84.5(supports-color@8.1.1) + metro-cache-key: 0.84.5 + metro-config: 0.84.5(supports-color@8.1.1) + metro-core: 0.84.5 + metro-file-map: 0.84.5(supports-color@8.1.1) + metro-resolver: 0.84.5 + metro-runtime: 0.84.5 + metro-source-map: 0.84.5(supports-color@8.1.1) + metro-symbolicate: 0.84.5 + metro-transform-plugins: 0.84.5(supports-color@8.1.1) + metro-transform-worker: 0.84.5(supports-color@8.1.1) + mime-types: 3.0.2 nullthrows: 1.1.1 + serialize-error: 2.1.0 + source-map: 0.5.7 + throat: 5.0.0 + ws: 7.5.13 + yargs: 17.7.3 transitivePeerDependencies: - bufferutil - supports-color - utf-8-validate - metro@0.84.4: + metro@0.84.6(supports-color@8.1.1): dependencies: '@babel/code-frame': 7.29.7 - '@babel/core': 7.29.7 - '@babel/generator': 7.29.7 - '@babel/parser': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) + '@babel/generator': 7.29.8 + '@babel/parser': 7.29.8 '@babel/template': 7.29.7 - '@babel/traverse': 7.29.7 - '@babel/types': 7.29.7 + '@babel/traverse': 7.29.8(supports-color@8.1.1) + '@babel/types': 7.29.8 accepts: 2.0.0 ci-info: 2.0.0 - connect: 3.7.0 + connect: 3.7.0(supports-color@8.1.1) debug: 4.4.3(supports-color@8.1.1) error-stack-parser: 2.1.4 flow-enums-runtime: 0.0.6 graceful-fs: 4.2.11 hermes-parser: 0.35.0 + invariant: 2.2.4 + jest-worker: 29.7.0 + jsc-safe-url: 0.2.4 + lodash.throttle: 4.1.1 + metro-babel-transformer: 0.84.6(supports-color@8.1.1) + metro-cache: 0.84.6(supports-color@8.1.1) + metro-cache-key: 0.84.6 + metro-config: 0.84.6(supports-color@8.1.1) + metro-core: 0.84.6 + metro-file-map: 0.84.6(supports-color@8.1.1) + metro-resolver: 0.84.6 + metro-runtime: 0.84.6 + metro-source-map: 0.84.6(supports-color@8.1.1) + metro-symbolicate: 0.84.6 + metro-transform-plugins: 0.84.6(supports-color@8.1.1) + metro-transform-worker: 0.84.6(supports-color@8.1.1) + mime-types: 3.0.2 + nullthrows: 1.1.1 + serialize-error: 2.1.0 + source-map: 0.5.7 + throat: 5.0.0 + ws: 7.5.13 + yargs: 17.7.3 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + metro@0.87.0(supports-color@8.1.1): + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/core': 7.29.7(supports-color@8.1.1) + '@babel/generator': 7.29.8 + '@babel/parser': 7.29.8 + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.8(supports-color@8.1.1) + '@babel/types': 7.29.8 + accepts: 2.0.0 + ci-info: 2.0.0 + connect: 3.7.0(supports-color@8.1.1) + debug: 4.4.3(supports-color@8.1.1) + error-stack-parser: 2.1.4 + flow-enums-runtime: 0.0.6 + graceful-fs: 4.2.11 + hermes-parser: 0.36.1 image-size: 1.2.1 invariant: 2.2.4 jest-worker: 29.7.0 jsc-safe-url: 0.2.4 lodash.throttle: 4.1.1 - metro-babel-transformer: 0.84.4 - metro-cache: 0.84.4 - metro-cache-key: 0.84.4 - metro-config: 0.84.4 - metro-core: 0.84.4 - metro-file-map: 0.84.4 - metro-resolver: 0.84.4 - metro-runtime: 0.84.4 - metro-source-map: 0.84.4 - metro-symbolicate: 0.84.4 - metro-transform-plugins: 0.84.4 - metro-transform-worker: 0.84.4 + metro-babel-transformer: 0.87.0(supports-color@8.1.1) + metro-cache: 0.87.0(supports-color@8.1.1) + metro-cache-key: 0.87.0 + metro-config: 0.87.0(supports-color@8.1.1) + metro-core: 0.87.0 + metro-file-map: 0.87.0(supports-color@8.1.1) + metro-resolver: 0.87.0 + metro-runtime: 0.87.0 + metro-source-map: 0.87.0(supports-color@8.1.1) + metro-symbolicate: 0.87.0 + metro-transform-plugins: 0.87.0(supports-color@8.1.1) + metro-transform-worker: 0.87.0(supports-color@8.1.1) mime-types: 3.0.2 nullthrows: 1.1.1 serialize-error: 2.1.0 source-map: 0.5.7 throat: 5.0.0 - ws: 7.5.11 + ws: 7.5.13 yargs: 17.7.3 transitivePeerDependencies: - bufferutil @@ -11898,7 +12961,7 @@ snapshots: micromark-util-types@2.0.2: {} - micromark@4.0.2: + micromark@4.0.2(supports-color@8.1.1): dependencies: '@types/debug': 4.1.13 debug: 4.4.3(supports-color@8.1.1) @@ -11953,39 +13016,37 @@ snapshots: mkdirp@1.0.4: {} - motion-dom@13.1.1: + motion-dom@13.2.0: dependencies: motion-utils: 13.0.0 motion-utils@13.0.0: {} - motion@13.1.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + motion@13.2.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8): dependencies: - framer-motion: 13.1.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + framer-motion: 13.2.0(react-dom@19.2.8(react@19.2.8))(react@19.2.8) tslib: 2.8.1 optionalDependencies: - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) ms@2.0.0: {} ms@2.1.3: {} - multitars@1.0.0: {} + multitars@1.0.2: {} mute-stream@3.0.0: {} - nanoid@3.3.15: {} - - nanoid@3.3.16: {} - nanoid@3.3.18: {} negotiator@0.6.3: {} negotiator@0.6.4: {} - negotiator@1.0.0: {} + negotiator@1.1.0: + dependencies: + content-type: 2.1.0 netmask@2.1.1: {} @@ -11993,32 +13054,32 @@ snapshots: dependencies: type-fest: 2.19.0 - next-themes@0.4.6(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + next-themes@0.4.6(react-dom@19.2.8(react@19.2.8))(react@19.2.8): dependencies: - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) - next@16.3.3(@types/node@24.13.3)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3): + next@16.3.4(@babel/core@8.0.1)(@types/node@26.4.1)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.8(react@19.2.8))(react@19.2.8): dependencies: - '@next/env': 16.3.3 + '@next/env': 16.3.4 '@swc/helpers': 0.5.23 baseline-browser-mapping: 2.11.20 caniuse-lite: 1.0.30001810 postcss: 8.5.23 - react: 19.2.3 - react-dom: 19.2.3(react@19.2.3) - styled-jsx: 5.1.6(react@19.2.3) + react: 19.2.8 + react-dom: 19.2.8(react@19.2.8) + styled-jsx: 5.1.6(@babel/core@8.0.1)(react@19.2.8) optionalDependencies: - '@next/swc-darwin-arm64': 16.3.3 - '@next/swc-darwin-x64': 16.3.3 - '@next/swc-linux-arm64-gnu': 16.3.3 - '@next/swc-linux-arm64-musl': 16.3.3 - '@next/swc-linux-x64-gnu': 16.3.3 - '@next/swc-linux-x64-musl': 16.3.3 - '@next/swc-win32-arm64-msvc': 16.3.3 - '@next/swc-win32-x64-msvc': 16.3.3 + '@next/swc-darwin-arm64': 16.3.4 + '@next/swc-darwin-x64': 16.3.4 + '@next/swc-linux-arm64-gnu': 16.3.4 + '@next/swc-linux-arm64-musl': 16.3.4 + '@next/swc-linux-x64-gnu': 16.3.4 + '@next/swc-linux-x64-musl': 16.3.4 + '@next/swc-win32-arm64-msvc': 16.3.4 + '@next/swc-win32-x64-msvc': 16.3.4 babel-plugin-react-compiler: 1.0.0 - sharp: 0.35.4(@types/node@24.13.3) + sharp: 0.35.4(@types/node@26.4.1) transitivePeerDependencies: - '@babel/core' - '@types/node' @@ -12032,7 +13093,7 @@ snapshots: node-int64@0.4.0: {} - node-releases@2.0.51: {} + node-releases@2.0.54: {} npm-package-arg@11.0.3: dependencies: @@ -12045,7 +13106,15 @@ snapshots: nullthrows@1.1.1: {} - ob1@0.84.4: + ob1@0.84.5: + dependencies: + flow-enums-runtime: 0.0.6 + + ob1@0.84.6: + dependencies: + flow-enums-runtime: 0.0.6 + + ob1@0.87.0: dependencies: flow-enums-runtime: 0.0.6 @@ -12120,62 +13189,62 @@ snapshots: macos-release: 3.5.1 windows-release: 7.1.1 - oxfmt@0.65.0: + oxfmt@0.66.0: dependencies: tinypool: 2.1.0 optionalDependencies: - '@oxfmt/binding-android-arm-eabi': 0.65.0 - '@oxfmt/binding-android-arm64': 0.65.0 - '@oxfmt/binding-darwin-arm64': 0.65.0 - '@oxfmt/binding-darwin-x64': 0.65.0 - '@oxfmt/binding-freebsd-x64': 0.65.0 - '@oxfmt/binding-linux-arm-gnueabihf': 0.65.0 - '@oxfmt/binding-linux-arm-musleabihf': 0.65.0 - '@oxfmt/binding-linux-arm64-gnu': 0.65.0 - '@oxfmt/binding-linux-arm64-musl': 0.65.0 - '@oxfmt/binding-linux-ppc64-gnu': 0.65.0 - '@oxfmt/binding-linux-riscv64-gnu': 0.65.0 - '@oxfmt/binding-linux-riscv64-musl': 0.65.0 - '@oxfmt/binding-linux-s390x-gnu': 0.65.0 - '@oxfmt/binding-linux-x64-gnu': 0.65.0 - '@oxfmt/binding-linux-x64-musl': 0.65.0 - '@oxfmt/binding-openharmony-arm64': 0.65.0 - '@oxfmt/binding-win32-arm64-msvc': 0.65.0 - '@oxfmt/binding-win32-ia32-msvc': 0.65.0 - '@oxfmt/binding-win32-x64-msvc': 0.65.0 - - oxlint@1.80.0: + '@oxfmt/binding-android-arm-eabi': 0.66.0 + '@oxfmt/binding-android-arm64': 0.66.0 + '@oxfmt/binding-darwin-arm64': 0.66.0 + '@oxfmt/binding-darwin-x64': 0.66.0 + '@oxfmt/binding-freebsd-x64': 0.66.0 + '@oxfmt/binding-linux-arm-gnueabihf': 0.66.0 + '@oxfmt/binding-linux-arm-musleabihf': 0.66.0 + '@oxfmt/binding-linux-arm64-gnu': 0.66.0 + '@oxfmt/binding-linux-arm64-musl': 0.66.0 + '@oxfmt/binding-linux-ppc64-gnu': 0.66.0 + '@oxfmt/binding-linux-riscv64-gnu': 0.66.0 + '@oxfmt/binding-linux-riscv64-musl': 0.66.0 + '@oxfmt/binding-linux-s390x-gnu': 0.66.0 + '@oxfmt/binding-linux-x64-gnu': 0.66.0 + '@oxfmt/binding-linux-x64-musl': 0.66.0 + '@oxfmt/binding-openharmony-arm64': 0.66.0 + '@oxfmt/binding-win32-arm64-msvc': 0.66.0 + '@oxfmt/binding-win32-ia32-msvc': 0.66.0 + '@oxfmt/binding-win32-x64-msvc': 0.66.0 + + oxlint@1.81.0: optionalDependencies: - '@oxlint/binding-android-arm-eabi': 1.80.0 - '@oxlint/binding-android-arm64': 1.80.0 - '@oxlint/binding-darwin-arm64': 1.80.0 - '@oxlint/binding-darwin-x64': 1.80.0 - '@oxlint/binding-freebsd-x64': 1.80.0 - '@oxlint/binding-linux-arm-gnueabihf': 1.80.0 - '@oxlint/binding-linux-arm-musleabihf': 1.80.0 - '@oxlint/binding-linux-arm64-gnu': 1.80.0 - '@oxlint/binding-linux-arm64-musl': 1.80.0 - '@oxlint/binding-linux-ppc64-gnu': 1.80.0 - '@oxlint/binding-linux-riscv64-gnu': 1.80.0 - '@oxlint/binding-linux-riscv64-musl': 1.80.0 - '@oxlint/binding-linux-s390x-gnu': 1.80.0 - '@oxlint/binding-linux-x64-gnu': 1.80.0 - '@oxlint/binding-linux-x64-musl': 1.80.0 - '@oxlint/binding-openharmony-arm64': 1.80.0 - '@oxlint/binding-win32-arm64-msvc': 1.80.0 - '@oxlint/binding-win32-ia32-msvc': 1.80.0 - '@oxlint/binding-win32-x64-msvc': 1.80.0 - - pac-proxy-agent@9.1.0: + '@oxlint/binding-android-arm-eabi': 1.81.0 + '@oxlint/binding-android-arm64': 1.81.0 + '@oxlint/binding-darwin-arm64': 1.81.0 + '@oxlint/binding-darwin-x64': 1.81.0 + '@oxlint/binding-freebsd-x64': 1.81.0 + '@oxlint/binding-linux-arm-gnueabihf': 1.81.0 + '@oxlint/binding-linux-arm-musleabihf': 1.81.0 + '@oxlint/binding-linux-arm64-gnu': 1.81.0 + '@oxlint/binding-linux-arm64-musl': 1.81.0 + '@oxlint/binding-linux-ppc64-gnu': 1.81.0 + '@oxlint/binding-linux-riscv64-gnu': 1.81.0 + '@oxlint/binding-linux-riscv64-musl': 1.81.0 + '@oxlint/binding-linux-s390x-gnu': 1.81.0 + '@oxlint/binding-linux-x64-gnu': 1.81.0 + '@oxlint/binding-linux-x64-musl': 1.81.0 + '@oxlint/binding-openharmony-arm64': 1.81.0 + '@oxlint/binding-win32-arm64-msvc': 1.81.0 + '@oxlint/binding-win32-ia32-msvc': 1.81.0 + '@oxlint/binding-win32-x64-msvc': 1.81.0 + + pac-proxy-agent@9.1.0(supports-color@8.1.1): dependencies: agent-base: 9.0.0 debug: 4.4.3(supports-color@8.1.1) - get-uri: 8.0.1 - http-proxy-agent: 9.1.0 - https-proxy-agent: 9.1.0 + get-uri: 8.0.1(supports-color@8.1.1) + http-proxy-agent: 9.1.0(supports-color@8.1.1) + https-proxy-agent: 9.1.0(supports-color@8.1.1) pac-resolver: 9.0.1(quickjs-wasi@2.2.0) quickjs-wasi: 2.2.0 - socks-proxy-agent: 10.1.0 + socks-proxy-agent: 10.1.0(supports-color@8.1.1) transitivePeerDependencies: - kerberos - supports-color @@ -12259,7 +13328,7 @@ snapshots: plist@3.1.1: dependencies: - '@xmldom/xmldom': 0.9.10 + '@xmldom/xmldom': 0.9.12 base64-js: 1.5.1 xmlbuilder: 15.1.1 @@ -12280,12 +13349,6 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 - postcss@8.5.25: - dependencies: - nanoid: 3.3.16 - picocolors: 1.1.1 - source-map-js: 1.2.1 - postcss@8.5.26: dependencies: nanoid: 3.3.18 @@ -12296,7 +13359,7 @@ snapshots: powershell-utils@0.2.1: {} - prettier@3.9.5: {} + prettier@3.9.6: {} pretty-format@29.7.0: dependencies: @@ -12304,12 +13367,12 @@ snapshots: ansi-styles: 5.2.0 react-is: 18.3.1 - pretty-format@30.4.1: + pretty-format@30.5.1: dependencies: - '@jest/schemas': 30.4.1 + '@jest/react-is-18': react-is@18.3.1 + '@jest/react-is-19': react-is@19.2.8 + '@jest/schemas': 30.5.0 ansi-styles: 5.2.0 - react-is-18: react-is@18.3.1 - react-is-19: react-is@19.2.8 proc-log@4.2.0: {} @@ -12334,16 +13397,16 @@ snapshots: proxy-agent-negotiate@1.1.0: {} - proxy-agent@8.0.2: + proxy-agent@8.0.2(supports-color@8.1.1): dependencies: agent-base: 9.0.0 debug: 4.4.3(supports-color@8.1.1) - http-proxy-agent: 9.1.0 - https-proxy-agent: 9.1.0 + http-proxy-agent: 9.1.0(supports-color@8.1.1) + https-proxy-agent: 9.1.0(supports-color@8.1.1) lru-cache: 7.18.3 - pac-proxy-agent: 9.1.0 + pac-proxy-agent: 9.1.0(supports-color@8.1.1) proxy-from-env: 2.1.0 - socks-proxy-agent: 10.1.0 + socks-proxy-agent: 10.1.0(supports-color@8.1.1) transitivePeerDependencies: - kerberos - supports-color @@ -12374,8 +13437,8 @@ snapshots: react-devtools-core@6.1.5: dependencies: - shell-quote: 1.9.0 - ws: 7.5.11 + shell-quote: 1.10.0 + ws: 7.5.13 transitivePeerDependencies: - bufferutil - utf-8-validate @@ -12385,40 +13448,43 @@ snapshots: react: 19.2.3 scheduler: 0.27.0 + react-dom@19.2.8(react@19.2.8): + dependencies: + react: 19.2.8 + scheduler: 0.27.0 + react-freeze@1.0.4(react@19.2.3): dependencies: react: 19.2.3 react-is@18.3.1: {} - react-is@19.2.7: {} - react-is@19.2.8: {} - react-native-nitro-modules@0.37.1(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3))(react@19.2.3): + react-native-nitro-modules@0.37.1(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3): dependencies: react: 19.2.3 - react-native: 0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3) + react-native: 0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) - react-native-safe-area-context@5.7.0(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3))(react@19.2.3): + react-native-safe-area-context@5.7.0(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3): dependencies: react: 19.2.3 - react-native: 0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3) + react-native: 0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) - react-native-screens@4.25.2(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3))(react@19.2.3): + react-native-screens@4.26.2(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3): dependencies: react: 19.2.3 react-freeze: 1.0.4(react@19.2.3) - react-native: 0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3) + react-native: 0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) warn-once: 0.1.1 - react-native-unistyles@3.3.0(@react-native/normalize-colors@0.86.0)(react-native-nitro-modules@0.37.1(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3))(react@19.2.3))(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3))(react@19.2.3): + react-native-unistyles@3.3.0(@react-native/normalize-colors@0.87.1)(react-native-nitro-modules@0.37.1(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3))(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3): dependencies: '@babel/types': 7.29.0 - '@react-native/normalize-colors': 0.86.0 + '@react-native/normalize-colors': 0.87.1 react: 19.2.3 - react-native: 0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3) - react-native-nitro-modules: 0.37.1(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3))(react@19.2.3) + react-native: 0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) + react-native-nitro-modules: 0.37.1(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) react-native-web@0.21.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: @@ -12435,15 +13501,15 @@ snapshots: transitivePeerDependencies: - encoding - react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3): + react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1): dependencies: - '@react-native/assets-registry': 0.86.0 - '@react-native/codegen': 0.86.0(@babel/core@7.29.7) - '@react-native/community-cli-plugin': 0.86.0 - '@react-native/gradle-plugin': 0.86.0 - '@react-native/js-polyfills': 0.86.0 - '@react-native/normalize-colors': 0.86.0 - '@react-native/virtualized-lists': 0.86.0(@types/react@19.2.17)(react-native@0.86.0(@babel/core@7.29.7)(@types/react@19.2.17)(react@19.2.3))(react@19.2.3) + '@react-native/assets-registry': 0.86.3 + '@react-native/codegen': 0.86.3(@babel/core@7.29.7(supports-color@8.1.1)) + '@react-native/community-cli-plugin': 0.86.3(supports-color@8.1.1) + '@react-native/gradle-plugin': 0.86.3 + '@react-native/js-polyfills': 0.86.3 + '@react-native/normalize-colors': 0.86.3 + '@react-native/virtualized-lists': 0.86.3(@types/react@19.2.18)(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3) abort-controller: 3.0.0 anser: 1.4.10 ansi-regex: 5.0.1 @@ -12451,11 +13517,11 @@ snapshots: base64-js: 1.5.1 commander: 12.1.0 flow-enums-runtime: 0.0.6 - hermes-compiler: 250829098.0.14 + hermes-compiler: 250829098.0.17 invariant: 2.2.4 memoize-one: 5.2.1 - metro-runtime: 0.84.4 - metro-source-map: 0.84.4 + metro-runtime: 0.84.6 + metro-source-map: 0.84.6(supports-color@8.1.1) nullthrows: 1.1.1 pretty-format: 29.7.0 promise: 8.3.0 @@ -12468,10 +13534,96 @@ snapshots: stacktrace-parser: 0.1.11 tinyglobby: 0.2.17 whatwg-fetch: 3.6.20 - ws: 7.5.11 + ws: 7.5.13 + yargs: 17.7.3 + optionalDependencies: + '@types/react': 19.2.18 + transitivePeerDependencies: + - '@babel/core' + - '@react-native-community/cli' + - '@react-native/metro-config' + - bufferutil + - supports-color + - utf-8-validate + + react-native@0.87.1(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.8)(supports-color@8.1.1): + dependencies: + '@react-native/asset-utils': 0.87.1 + '@react-native/codegen': 0.87.1(@babel/core@7.29.7(supports-color@8.1.1)) + '@react-native/community-cli-plugin': 0.87.1(supports-color@8.1.1) + '@react-native/gradle-plugin': 0.87.1 + '@react-native/normalize-colors': 0.87.1 + '@react-native/virtualized-lists': 0.87.1(@types/react@19.2.18)(react-native@0.87.1(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.8)(supports-color@8.1.1))(react@19.2.8) + anser: 1.4.10 + ansi-regex: 5.0.1 + babel-plugin-syntax-hermes-parser: 0.36.1 + base64-js: 1.5.1 + commander: 12.1.0 + flow-enums-runtime: 0.0.6 + hermes-compiler: 250829098.0.17 + invariant: 2.2.4 + memoize-one: 5.2.1 + metro-runtime: 0.87.0 + metro-source-map: 0.87.0(supports-color@8.1.1) + nullthrows: 1.1.1 + pretty-format: 29.7.0 + promise: 8.3.0 + react: 19.2.8 + react-devtools-core: 6.1.5 + react-refresh: 0.14.2 + regenerator-runtime: 0.13.11 + scheduler: 0.27.0 + semver: 7.8.5 + stacktrace-parser: 0.1.11 + tinyglobby: 0.2.17 + whatwg-fetch: 3.6.20 + ws: 7.5.13 yargs: 17.7.3 optionalDependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 + transitivePeerDependencies: + - '@babel/core' + - '@react-native-community/cli' + - '@react-native/metro-config' + - bufferutil + - supports-color + - utf-8-validate + + react-native@0.87.1(@types/react@19.2.18)(react@19.2.8)(supports-color@8.1.1): + dependencies: + '@react-native/asset-utils': 0.87.1 + '@react-native/codegen': 0.87.1(@babel/core@7.29.7(supports-color@8.1.1)) + '@react-native/community-cli-plugin': 0.87.1(supports-color@8.1.1) + '@react-native/gradle-plugin': 0.87.1 + '@react-native/normalize-colors': 0.87.1 + '@react-native/virtualized-lists': 0.87.1(@types/react@19.2.18)(react-native@0.87.1(@types/react@19.2.18)(react@19.2.8)(supports-color@8.1.1))(react@19.2.8) + anser: 1.4.10 + ansi-regex: 5.0.1 + babel-plugin-syntax-hermes-parser: 0.36.1 + base64-js: 1.5.1 + commander: 12.1.0 + flow-enums-runtime: 0.0.6 + hermes-compiler: 250829098.0.17 + invariant: 2.2.4 + memoize-one: 5.2.1 + metro-runtime: 0.87.0 + metro-source-map: 0.87.0(supports-color@8.1.1) + nullthrows: 1.1.1 + pretty-format: 29.7.0 + promise: 8.3.0 + react: 19.2.8 + react-devtools-core: 6.1.5 + react-refresh: 0.14.2 + regenerator-runtime: 0.13.11 + scheduler: 0.27.0 + semver: 7.8.5 + stacktrace-parser: 0.1.11 + tinyglobby: 0.2.17 + whatwg-fetch: 3.6.20 + ws: 7.5.13 + yargs: 17.7.3 + optionalDependencies: + '@types/react': 19.2.18 transitivePeerDependencies: - '@babel/core' - '@react-native-community/cli' @@ -12482,35 +13634,43 @@ snapshots: react-refresh@0.14.2: {} - react-remove-scroll-bar@2.3.8(@types/react@19.2.17)(react@19.2.3): + react-remove-scroll-bar@2.3.8(@types/react@19.2.18)(react@19.2.8): dependencies: - react: 19.2.3 - react-style-singleton: 2.2.3(@types/react@19.2.17)(react@19.2.3) + react: 19.2.8 + react-style-singleton: 2.2.3(@types/react@19.2.18)(react@19.2.8) tslib: 2.8.1 optionalDependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 - react-remove-scroll@2.7.2(@types/react@19.2.17)(react@19.2.3): + react-remove-scroll@2.7.2(@types/react@19.2.18)(react@19.2.8): dependencies: - react: 19.2.3 - react-remove-scroll-bar: 2.3.8(@types/react@19.2.17)(react@19.2.3) - react-style-singleton: 2.2.3(@types/react@19.2.17)(react@19.2.3) + react: 19.2.8 + react-remove-scroll-bar: 2.3.8(@types/react@19.2.18)(react@19.2.8) + react-style-singleton: 2.2.3(@types/react@19.2.18)(react@19.2.8) tslib: 2.8.1 - use-callback-ref: 1.3.3(@types/react@19.2.17)(react@19.2.3) - use-sidecar: 1.1.3(@types/react@19.2.17)(react@19.2.3) + use-callback-ref: 1.3.3(@types/react@19.2.18)(react@19.2.8) + use-sidecar: 1.1.3(@types/react@19.2.18)(react@19.2.8) optionalDependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 - react-style-singleton@2.2.3(@types/react@19.2.17)(react@19.2.3): + react-style-singleton@2.2.3(@types/react@19.2.18)(react@19.2.8): dependencies: get-nonce: 1.0.1 - react: 19.2.3 + react: 19.2.8 tslib: 2.8.1 optionalDependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 + + react-test-renderer@19.2.8(react@19.2.8): + dependencies: + react: 19.2.8 + react-is: 19.2.8 + scheduler: 0.27.0 react@19.2.3: {} + react@19.2.8: {} + readable-stream@3.6.2: dependencies: inherits: 2.0.4 @@ -12587,11 +13747,11 @@ snapshots: hast-util-raw: 9.1.0 vfile: 6.0.3 - rehype-recma@1.0.0: + rehype-recma@1.0.0(supports-color@8.1.1): dependencies: '@types/estree': 1.0.9 '@types/hast': 3.0.5 - hast-util-to-estree: 3.1.3 + hast-util-to-estree: 3.1.3(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -12599,12 +13759,12 @@ snapshots: dependencies: '@-xun/debug': 2.0.2 chalk: 5.6.2 - core-js: 3.49.0 + core-js: 3.50.0 exit-hook: 4.0.0 - release-it@21.0.2(@types/node@24.13.3): + release-it@21.0.2(@types/node@26.4.1)(supports-color@8.1.1): dependencies: - '@inquirer/prompts': 8.5.2(@types/node@24.13.3) + '@inquirer/prompts': 8.5.2(@types/node@26.4.1) '@octokit/rest': 22.0.1 '@phun-ky/typeof': 2.0.3 async-retry: 1.3.3 @@ -12620,7 +13780,7 @@ snapshots: open: 11.0.0 ora: 9.4.1 os-name: 7.0.0 - proxy-agent: 8.0.2 + proxy-agent: 8.0.2(supports-color@8.1.1) semver: 7.8.5 tinyglobby: 0.2.17 undici: 7.29.0 @@ -12632,28 +13792,28 @@ snapshots: - magicast - supports-color - remark-gfm@4.0.1: + remark-gfm@4.0.1(supports-color@8.1.1): dependencies: '@types/mdast': 4.0.4 - mdast-util-gfm: 3.1.0 + mdast-util-gfm: 3.1.0(supports-color@8.1.1) micromark-extension-gfm: 3.0.0 - remark-parse: 11.0.0 + remark-parse: 11.0.0(supports-color@8.1.1) remark-stringify: 11.0.0 unified: 11.0.5 transitivePeerDependencies: - supports-color - remark-mdx@3.1.1: + remark-mdx@3.1.1(supports-color@8.1.1): dependencies: - mdast-util-mdx: 3.0.0 + mdast-util-mdx: 3.0.0(supports-color@8.1.1) micromark-extension-mdxjs: 3.0.0 transitivePeerDependencies: - supports-color - remark-parse@11.0.0: + remark-parse@11.0.0(supports-color@8.1.1): dependencies: '@types/mdast': 4.0.4 - mdast-util-from-markdown: 2.0.3 + mdast-util-from-markdown: 2.0.3(supports-color@8.1.1) micromark-util-types: 2.0.2 unified: 11.0.5 transitivePeerDependencies: @@ -12673,10 +13833,10 @@ snapshots: mdast-util-to-markdown: 2.1.2 unified: 11.0.5 - remark@15.0.1: + remark@15.0.1(supports-color@8.1.1): dependencies: '@types/mdast': 4.0.4 - remark-parse: 11.0.0 + remark-parse: 11.0.0(supports-color@8.1.1) remark-stringify: 11.0.0 unified: 11.0.5 transitivePeerDependencies: @@ -12715,44 +13875,45 @@ snapshots: robust-predicates@3.0.3: {} - rolldown@1.1.5: + rolldown@1.2.7: dependencies: - '@oxc-project/types': 0.139.0 + '@oxc-project/types': 0.148.0 '@rolldown/pluginutils': 1.0.1 optionalDependencies: - '@rolldown/binding-android-arm64': 1.1.5 - '@rolldown/binding-darwin-arm64': 1.1.5 - '@rolldown/binding-darwin-x64': 1.1.5 - '@rolldown/binding-freebsd-x64': 1.1.5 - '@rolldown/binding-linux-arm-gnueabihf': 1.1.5 - '@rolldown/binding-linux-arm64-gnu': 1.1.5 - '@rolldown/binding-linux-arm64-musl': 1.1.5 - '@rolldown/binding-linux-ppc64-gnu': 1.1.5 - '@rolldown/binding-linux-s390x-gnu': 1.1.5 - '@rolldown/binding-linux-x64-gnu': 1.1.5 - '@rolldown/binding-linux-x64-musl': 1.1.5 - '@rolldown/binding-openharmony-arm64': 1.1.5 - '@rolldown/binding-wasm32-wasi': 1.1.5 - '@rolldown/binding-win32-arm64-msvc': 1.1.5 - '@rolldown/binding-win32-x64-msvc': 1.1.5 - - rollup-plugin-dts@6.5.1(rollup@4.63.1)(typescript@6.0.3): + '@rolldown/binding-android-arm-eabi': 1.2.7 + '@rolldown/binding-android-arm64': 1.2.7 + '@rolldown/binding-darwin-arm64': 1.2.7 + '@rolldown/binding-darwin-x64': 1.2.7 + '@rolldown/binding-freebsd-x64': 1.2.7 + '@rolldown/binding-linux-arm-gnueabihf': 1.2.7 + '@rolldown/binding-linux-arm64-gnu': 1.2.7 + '@rolldown/binding-linux-arm64-musl': 1.2.7 + '@rolldown/binding-linux-ppc64-gnu': 1.2.7 + '@rolldown/binding-linux-s390x-gnu': 1.2.7 + '@rolldown/binding-linux-x64-gnu': 1.2.7 + '@rolldown/binding-linux-x64-musl': 1.2.7 + '@rolldown/binding-openharmony-arm64': 1.2.7 + '@rolldown/binding-win32-arm64-msvc': 1.2.7 + '@rolldown/binding-win32-x64-msvc': 1.2.7 + + rollup-plugin-dts@6.5.1(@typescript/typescript6@6.0.2)(rollup@4.63.1)(typescript@7.0.2): dependencies: '@jridgewell/remapping': 2.3.5 '@jridgewell/sourcemap-codec': 1.6.0 convert-source-map: 2.0.0 magic-string: 0.30.21 rollup: 4.63.1 - typescript: 6.0.3 + typescript: 7.0.2 optionalDependencies: '@babel/code-frame': 8.0.0 + '@typescript/typescript6': 6.0.2 - rollup-plugin-esbuild@6.2.1(esbuild@0.28.2)(rollup@4.63.1): + rollup-plugin-esbuild@6.2.1(esbuild@0.28.2)(rollup@4.63.1)(supports-color@8.1.1): dependencies: debug: 4.4.3(supports-color@8.1.1) es-module-lexer: 1.7.0 esbuild: 0.28.2 - get-tsconfig: 4.14.0 + get-tsconfig: 4.14.3 rollup: 4.63.1 unplugin-utils: 0.2.5 transitivePeerDependencies: @@ -12805,7 +13966,9 @@ snapshots: safer-buffer@2.1.2: {} - sax@1.6.0: {} + sandbox-cli-detector@0.2.0: {} + + sax@1.6.1: {} scheduler@0.27.0: {} @@ -12817,9 +13980,9 @@ snapshots: semver@7.8.5: {} - send@0.19.2: + send@0.19.2(supports-color@8.1.1): dependencies: - debug: 2.6.9 + debug: 2.6.9(supports-color@8.1.1) depd: 2.0.0 destroy: 1.2.0 encodeurl: 2.0.0 @@ -12837,12 +14000,12 @@ snapshots: serialize-error@2.1.0: {} - serve-static@1.16.3: + serve-static@1.16.3(supports-color@8.1.1): dependencies: encodeurl: 2.0.0 escape-html: 1.0.3 parseurl: 1.3.3 - send: 0.19.2 + send: 0.19.2(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -12852,7 +14015,7 @@ snapshots: sf-symbols-typescript@2.2.0: {} - sharp@0.35.4(@types/node@24.13.3): + sharp@0.35.4(@types/node@26.4.1): dependencies: '@img/colour': 1.1.0 detect-libc: 2.1.2 @@ -12883,7 +14046,7 @@ snapshots: '@img/sharp-win32-arm64': 0.35.4 '@img/sharp-win32-ia32': 0.35.4 '@img/sharp-win32-x64': 0.35.4 - '@types/node': 24.13.3 + '@types/node': 26.4.1 optional: true shebang-command@2.0.0: @@ -12892,18 +14055,7 @@ snapshots: shebang-regex@3.0.0: {} - shell-quote@1.9.0: {} - - shiki@4.3.1: - dependencies: - '@shikijs/core': 4.3.1 - '@shikijs/engine-javascript': 4.3.1 - '@shikijs/engine-oniguruma': 4.3.1 - '@shikijs/langs': 4.3.1 - '@shikijs/themes': 4.3.1 - '@shikijs/types': 4.3.1 - '@shikijs/vscode-textmate': 10.0.2 - '@types/hast': 3.0.5 + shell-quote@1.10.0: {} shiki@4.4.3: dependencies: @@ -12938,15 +14090,15 @@ snapshots: smart-buffer@4.2.0: {} - socks-proxy-agent@10.1.0: + socks-proxy-agent@10.1.0(supports-color@8.1.1): dependencies: agent-base: 9.0.0 debug: 4.4.3(supports-color@8.1.1) - socks: 2.8.9 + socks: 2.8.10 transitivePeerDependencies: - supports-color - socks@2.8.9: + socks@2.8.10: dependencies: ip-address: 10.7.0 smart-buffer: 4.2.0 @@ -12976,7 +14128,9 @@ snapshots: dependencies: type-fest: 0.7.1 - standard-navigation@0.0.7: {} + standard-navigation@0.0.8(react@19.2.3): + dependencies: + react: 19.2.3 statuses@1.5.0: {} @@ -13046,10 +14200,12 @@ snapshots: dependencies: inline-style-parser: 0.2.7 - styled-jsx@5.1.6(react@19.2.3): + styled-jsx@5.1.6(@babel/core@8.0.1)(react@19.2.8): dependencies: client-only: 0.0.1 - react: 19.2.3 + react: 19.2.8 + optionalDependencies: + '@babel/core': 8.0.1 styleq@0.1.3: {} @@ -13083,7 +14239,7 @@ snapshots: ansi-escapes: 4.3.2 supports-hyperlinks: 2.3.0 - terser@5.49.0: + terser@5.51.2: dependencies: '@jridgewell/source-map': 0.3.11 acorn: 8.18.0 @@ -13092,7 +14248,7 @@ snapshots: throat@5.0.0: {} - tinybench@2.9.0: {} + tinybench@6.1.4: {} tinyexec@1.3.0: {} @@ -13103,8 +14259,6 @@ snapshots: tinypool@2.1.0: {} - tinyrainbow@3.1.1: {} - tmpl@1.0.5: {} to-regex-range@5.0.1: @@ -13123,11 +14277,6 @@ snapshots: ts-dedent@2.3.0: {} - ts-morph@27.0.2: - dependencies: - '@ts-morph/common': 0.28.1 - code-block-writer: 13.0.3 - ts-morph@28.0.0: dependencies: '@ts-morph/common': 0.29.0 @@ -13153,9 +14302,32 @@ snapshots: typescript@6.0.3: {} + typescript@7.0.2: + optionalDependencies: + '@typescript/typescript-aix-ppc64': 7.0.2 + '@typescript/typescript-darwin-arm64': 7.0.2 + '@typescript/typescript-darwin-x64': 7.0.2 + '@typescript/typescript-freebsd-arm64': 7.0.2 + '@typescript/typescript-freebsd-x64': 7.0.2 + '@typescript/typescript-linux-arm': 7.0.2 + '@typescript/typescript-linux-arm64': 7.0.2 + '@typescript/typescript-linux-loong64': 7.0.2 + '@typescript/typescript-linux-mips64el': 7.0.2 + '@typescript/typescript-linux-ppc64': 7.0.2 + '@typescript/typescript-linux-riscv64': 7.0.2 + '@typescript/typescript-linux-s390x': 7.0.2 + '@typescript/typescript-linux-x64': 7.0.2 + '@typescript/typescript-netbsd-arm64': 7.0.2 + '@typescript/typescript-netbsd-x64': 7.0.2 + '@typescript/typescript-openbsd-arm64': 7.0.2 + '@typescript/typescript-openbsd-x64': 7.0.2 + '@typescript/typescript-sunos-x64': 7.0.2 + '@typescript/typescript-win32-arm64': 7.0.2 + '@typescript/typescript-win32-x64': 7.0.2 + ua-parser-js@1.0.41: {} - undici-types@7.18.2: {} + undici-types@8.3.0: {} undici@7.29.0: {} @@ -13214,6 +14386,36 @@ snapshots: universal-user-agent@7.0.3: {} + uniwind@1.12.0(@expo/metro-config@57.0.12(expo@57.0.19)(supports-color@8.1.1)(typescript@7.0.2))(metro-cache@0.87.0(supports-color@8.1.1))(metro-transform-worker@0.87.0(supports-color@8.1.1))(metro@0.87.0(supports-color@8.1.1))(react-native@0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1))(react@19.2.3)(tailwindcss@4.3.3): + dependencies: + '@tailwindcss/node': 4.3.3 + '@tailwindcss/oxide': 4.3.3 + culori: 4.0.2 + lightningcss: 1.30.1 + metro: 0.87.0(supports-color@8.1.1) + metro-cache: 0.87.0(supports-color@8.1.1) + react: 19.2.3 + react-native: 0.86.3(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.3)(supports-color@8.1.1) + tailwindcss: 4.3.3 + optionalDependencies: + '@expo/metro-config': 57.0.12(expo@57.0.19)(supports-color@8.1.1)(typescript@7.0.2) + metro-transform-worker: 0.87.0(supports-color@8.1.1) + + uniwind@1.12.0(@expo/metro-config@57.0.12(expo@57.0.19)(supports-color@8.1.1)(typescript@7.0.2))(metro-cache@0.87.0(supports-color@8.1.1))(metro-transform-worker@0.87.0(supports-color@8.1.1))(metro@0.87.0(supports-color@8.1.1))(react-native@0.87.1(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.8)(supports-color@8.1.1))(react@19.2.8)(tailwindcss@4.3.3): + dependencies: + '@tailwindcss/node': 4.3.3 + '@tailwindcss/oxide': 4.3.3 + culori: 4.0.2 + lightningcss: 1.30.1 + metro: 0.87.0(supports-color@8.1.1) + metro-cache: 0.87.0(supports-color@8.1.1) + react: 19.2.8 + react-native: 0.87.1(@babel/core@7.29.7(supports-color@8.1.1))(@types/react@19.2.18)(react@19.2.8)(supports-color@8.1.1) + tailwindcss: 4.3.3 + optionalDependencies: + '@expo/metro-config': 57.0.12(expo@57.0.19)(supports-color@8.1.1)(typescript@7.0.2) + metro-transform-worker: 0.87.0(supports-color@8.1.1) + unpipe@1.0.0: {} unplugin-utils@0.2.5: @@ -13221,32 +14423,32 @@ snapshots: pathe: 2.0.3 picomatch: 4.0.7 - update-browserslist-db@1.2.3(browserslist@4.28.5): + update-browserslist-db@1.3.2(browserslist@4.28.8): dependencies: - browserslist: 4.28.5 + browserslist: 4.28.8 escalade: 3.2.0 picocolors: 1.1.1 url-join@5.0.0: {} - use-callback-ref@1.3.3(@types/react@19.2.17)(react@19.2.3): + use-callback-ref@1.3.3(@types/react@19.2.18)(react@19.2.8): dependencies: - react: 19.2.3 + react: 19.2.8 tslib: 2.8.1 optionalDependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 use-latest-callback@0.2.6(react@19.2.3): dependencies: react: 19.2.3 - use-sidecar@1.1.3(@types/react@19.2.17)(react@19.2.3): + use-sidecar@1.1.3(@types/react@19.2.18)(react@19.2.8): dependencies: detect-node-es: 1.1.0 - react: 19.2.3 + react: 19.2.8 tslib: 2.8.1 optionalDependencies: - '@types/react': 19.2.17 + '@types/react': 19.2.18 use-sync-external-store@1.6.0(react@19.2.3): dependencies: @@ -13279,46 +14481,40 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - vite@8.1.4(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.49.0)(tsx@4.23.13)(yaml@2.9.0): + vite@8.2.2(@types/node@26.4.1)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.13)(yaml@2.9.0): dependencies: lightningcss: 1.33.0 picomatch: 4.0.7 postcss: 8.5.26 - rolldown: 1.1.5 + rolldown: 1.2.7 tinyglobby: 0.2.17 optionalDependencies: - '@types/node': 24.13.3 + '@types/node': 26.4.1 esbuild: 0.28.2 fsevents: 2.3.3 jiti: 2.7.0 - terser: 5.49.0 + terser: 5.51.2 tsx: 4.23.13 yaml: 2.9.0 - vitest@4.1.11(@types/node@24.13.3)(vite@8.1.4(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.49.0)(tsx@4.23.13)(yaml@2.9.0)): + vitest@5.0.0(@types/node@26.4.1)(vite@8.2.2(@types/node@26.4.1)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.13)(yaml@2.9.0)): dependencies: - '@vitest/expect': 4.1.11 - '@vitest/mocker': 4.1.11(vite@8.1.4(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.49.0)(tsx@4.23.13)(yaml@2.9.0)) - '@vitest/pretty-format': 4.1.11 - '@vitest/runner': 4.1.11 - '@vitest/snapshot': 4.1.11 - '@vitest/spy': 4.1.11 - '@vitest/utils': 4.1.11 + '@types/chai': 5.2.3 + '@vitest/mocker': 5.0.0(vite@8.2.2(@types/node@26.4.1)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.13)(yaml@2.9.0)) + chai: 6.2.2 es-module-lexer: 2.3.2 expect-type: 1.4.0 - magic-string: 0.30.21 + magic-string: 1.2.3 obug: 2.1.4 - pathe: 2.0.3 picomatch: 4.0.7 std-env: 4.2.0 - tinybench: 2.9.0 + tinybench: 6.1.4 tinyexec: 1.3.0 tinyglobby: 0.2.17 - tinyrainbow: 3.1.1 - vite: 8.1.4(@types/node@24.13.3)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.49.0)(tsx@4.23.13)(yaml@2.9.0) + vite: 8.2.2(@types/node@26.4.1)(esbuild@0.28.2)(jiti@2.7.0)(terser@5.51.2)(tsx@4.23.13)(yaml@2.9.0) why-is-node-running: 2.3.0 optionalDependencies: - '@types/node': 24.13.3 + '@types/node': 26.4.1 transitivePeerDependencies: - msw @@ -13376,9 +14572,9 @@ snapshots: string-width: 7.2.0 strip-ansi: 7.2.0 - ws@7.5.11: {} + ws@7.5.13: {} - ws@8.21.0: {} + ws@8.21.3: {} wsl-utils@0.3.1: dependencies: @@ -13392,7 +14588,7 @@ snapshots: xml2js@0.6.0: dependencies: - sax: 1.6.0 + sax: 1.6.1 xmlbuilder: 11.0.1 xmlbuilder@11.0.1: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 8270db7c..afae2ce7 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -3,9 +3,9 @@ packages: - packages/* - tooling/* -onlyBuiltDependencies: - - core-js - - core-js-pure - - deasync - - esbuild - - sharp +pmOnFail: download +minimumReleaseAgeStrict: true + +allowBuilds: + core-js: true + esbuild: true diff --git a/tooling/benchmark/package.json b/tooling/benchmark/package.json index 2113fec1..be240012 100644 --- a/tooling/benchmark/package.json +++ b/tooling/benchmark/package.json @@ -13,9 +13,9 @@ "typecheck": "tsc --noEmit" }, "devDependencies": { - "@types/node": "^24", + "@types/node": "^26.4.1", "tsx": "^4.23.13", - "typescript": "~6.0.3", - "vitest": "^4.1.11" + "typescript": "~7.0.2", + "vitest": "^5.0.0" } } diff --git a/tooling/benchmark/src/report/__tests__/analysis.test.ts b/tooling/benchmark/src/report/__tests__/analysis.test.ts index 2209edb9..b1e4f84e 100644 --- a/tooling/benchmark/src/report/__tests__/analysis.test.ts +++ b/tooling/benchmark/src/report/__tests__/analysis.test.ts @@ -7,8 +7,6 @@ import { convergencePoints, coreValidAt, fpsAt, - hasProfile, - isThermalRun, median, peakBoostGain, peakCoreGain, @@ -58,7 +56,6 @@ const twoProfile = (coreScale: number): FpsResult => describe('anchoredCore', () => { it('is undefined without both profiles (single-profile / legacy run)', () => { const legacy = result([m(100, 'default', 'off', 40), m(100, 'default', 'on', 60)]); - expect(hasProfile(legacy, 'core')).toBe(false); expect(anchoredCore(legacy)).toBeUndefined(); expect(peakCoreGain(legacy)).toBeUndefined(); expect(peakBoostGain(legacy)).toBeCloseTo(50); // (60-40)/40 @@ -145,10 +142,9 @@ describe('regime: thermal (direct) vs legacy (anchor)', () => { m(300, 'default', 'off', 30, 0, 'nominal'), m(300, 'default', 'on', 60, 0, 'nominal'), m(300, 'core', 'off', 42, 0, 'nominal'), - m(300, 'core', 'on', 60, 0, 'nominal'), // agrees with default_on β†’ load 300 is validated + m(300, 'core', 'on', 57, 0, 'nominal'), // 5% divergence stays within validation tolerance ]); - expect(isThermalRun(r)).toBe(true); - expect(peakCoreGain(r)).toBeCloseTo(40); // (42-30)/30 direct, no anchor + expect(peakCoreGain(r)).toBeCloseTo(40); // direct; anchoring would produce about 47% }); it('legacy run (all-unknown thermal) β†’ anchored core gain', () => { @@ -158,7 +154,6 @@ describe('regime: thermal (direct) vs legacy (anchor)', () => { m(300, 'core', 'off', 42, 0, 'unknown'), m(300, 'core', 'on', 50, 0, 'unknown'), // anchor Γ—60/50=1.2 β†’ 42 lifts to 50.4 β†’ +68% ]); - expect(isThermalRun(r)).toBe(false); expect(peakCoreGain(r)).toBeCloseTo(68); }); }); @@ -173,11 +168,11 @@ describe('convergencePoints', () => { m(100, 'default', 'off', 40), m(100, 'default', 'on', 60), m(100, 'core', 'off', 44), - m(100, 'core', 'on', 60), + m(100, 'core', 'on', 57), ]); const points = convergencePoints(r)!; const heavy = points.find((p) => p.load === 100)!; - expect(heavy.baselineOptimized).toBeCloseTo(44); // direct median, not anchored + expect(heavy.baselineOptimized).toBeCloseTo(44); // direct; anchoring would produce about 46 expect(heavy.informative).toBe(true); expect(points.find((p) => p.load === 10)!.informative).toBe(false); // pinned at ceiling }); @@ -194,7 +189,6 @@ describe('validate (per-load)', () => { ]) ); expect(report.allValid).toBe(true); - expect(report.invalidLoads.size).toBe(0); }); it('drops a load with a capture above the floor', () => { diff --git a/tooling/benchmark/view-context.mjs b/tooling/benchmark/view-context.mjs new file mode 100644 index 00000000..eac96dac --- /dev/null +++ b/tooling/benchmark/view-context.mjs @@ -0,0 +1,34 @@ +import { createServer } from 'node:http'; +import { mkdirSync, writeFileSync } from 'node:fs'; +import { dirname, resolve } from 'node:path'; + +const output = resolve(process.argv[2] ?? 'benchmarks/view-context.json'); +const plan = { loads: [500, 2000], replicates: 13 }; +const samples = []; +const server = createServer(async (request, response) => { + if (request.method === 'GET' && request.url === '/plan') { + response.writeHead(200, { 'content-type': 'application/json' }); + response.end(JSON.stringify(plan)); + return; + } + let body = ''; + for await (const chunk of request) body += chunk; + if (request.method === 'POST' && request.url === '/measure') { + const sample = JSON.parse(body); + samples.push(sample); + console.log(JSON.stringify(sample)); + } else if (request.method === 'POST' && request.url === '/done') { + const result = JSON.parse(body); + if (result.development || !result.lifecycleParity || result.samples.length !== samples.length) { + throw new Error('Invalid benchmark result'); + } + mkdirSync(dirname(output), { recursive: true }); + writeFileSync(output, `${JSON.stringify({ plan, ...result }, null, 2)}\n`); + console.log(`Saved ${samples.length} samples to ${output}`); + server.close(); + } else { + response.writeHead(404); + } + response.end(); +}); +server.listen(8099, '127.0.0.1', () => console.log('Waiting for the release benchmark on port 8099'));