Repository: kuatsu/react-native-boost
Branch: main
Commit: 950c0234e327
Files: 202
Total size: 240.4 KB
Directory structure:
gitextract_wntfy7wt/
├── .github/
│ ├── FUNDING.yml
│ ├── actions/
│ │ └── setup/
│ │ └── action.yml
│ └── workflows/
│ ├── release.yml
│ ├── stale.yml
│ └── test.yml
├── .gitignore
├── .husky/
│ ├── commit-msg
│ └── pre-commit
├── .lintstagedrc
├── .npmrc
├── .nvmrc
├── .oxfmtrc.json
├── .oxlintrc.json
├── .zed/
│ └── settings.json
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── apps/
│ ├── docs/
│ │ ├── .gitignore
│ │ ├── README.md
│ │ ├── app/
│ │ │ ├── (home)/
│ │ │ │ ├── layout.tsx
│ │ │ │ └── page.tsx
│ │ │ ├── api/
│ │ │ │ └── search/
│ │ │ │ └── route.ts
│ │ │ ├── docs/
│ │ │ │ ├── [[...slug]]/
│ │ │ │ │ └── page.tsx
│ │ │ │ └── layout.tsx
│ │ │ ├── global.css
│ │ │ ├── layout.tsx
│ │ │ ├── llms-full.txt/
│ │ │ │ └── route.ts
│ │ │ ├── llms.mdx/
│ │ │ │ └── docs/
│ │ │ │ └── [[...slug]]/
│ │ │ │ └── route.ts
│ │ │ ├── llms.txt/
│ │ │ │ └── route.ts
│ │ │ └── og/
│ │ │ └── docs/
│ │ │ └── [...slug]/
│ │ │ └── route.tsx
│ │ ├── components/
│ │ │ ├── ai/
│ │ │ │ └── page-actions.tsx
│ │ │ └── docs/
│ │ │ ├── auto-option-sections.tsx
│ │ │ ├── auto-runtime-reference.tsx
│ │ │ └── reference-sections.tsx
│ │ ├── content/
│ │ │ └── docs/
│ │ │ ├── configuration/
│ │ │ │ ├── boost-decorator.mdx
│ │ │ │ ├── configure.mdx
│ │ │ │ └── nativewind.mdx
│ │ │ ├── index.mdx
│ │ │ ├── information/
│ │ │ │ ├── benchmarks.mdx
│ │ │ │ ├── how-it-works.mdx
│ │ │ │ ├── optimization-coverage.mdx
│ │ │ │ └── troubleshooting.mdx
│ │ │ ├── meta.json
│ │ │ └── runtime-library/
│ │ │ └── index.mdx
│ │ ├── lib/
│ │ │ ├── cn.ts
│ │ │ ├── layout.shared.tsx
│ │ │ ├── source.ts
│ │ │ └── type-generator.ts
│ │ ├── mdx-components.tsx
│ │ ├── next.config.mjs
│ │ ├── package.json
│ │ ├── postcss.config.mjs
│ │ ├── source.config.ts
│ │ └── tsconfig.json
│ └── example/
│ ├── .gitignore
│ ├── app.json
│ ├── babel.config.js
│ ├── index.ts
│ ├── package.json
│ ├── src/
│ │ ├── app.tsx
│ │ ├── components/
│ │ │ └── measure-component.tsx
│ │ ├── screens/
│ │ │ └── home.tsx
│ │ ├── types/
│ │ │ └── index.ts
│ │ └── utils/
│ │ └── helpers.ts
│ └── tsconfig.json
├── commitlint.config.mjs
├── package.json
├── packages/
│ ├── react-native-boost/
│ │ ├── .gitignore
│ │ ├── CHANGELOG.md
│ │ ├── LICENSE
│ │ ├── package.json
│ │ ├── rollup.config.mjs
│ │ ├── src/
│ │ │ ├── plugin/
│ │ │ │ ├── index.ts
│ │ │ │ ├── optimizers/
│ │ │ │ │ ├── text/
│ │ │ │ │ │ ├── __tests__/
│ │ │ │ │ │ │ ├── fixtures/
│ │ │ │ │ │ │ │ ├── basic/
│ │ │ │ │ │ │ │ │ ├── code.js
│ │ │ │ │ │ │ │ │ └── output.js
│ │ │ │ │ │ │ │ ├── complex-example/
│ │ │ │ │ │ │ │ │ ├── code.js
│ │ │ │ │ │ │ │ │ └── output.js
│ │ │ │ │ │ │ │ ├── default-props/
│ │ │ │ │ │ │ │ │ ├── code.js
│ │ │ │ │ │ │ │ │ └── output.js
│ │ │ │ │ │ │ │ ├── expo-router-link-alias-as-child/
│ │ │ │ │ │ │ │ │ ├── code.js
│ │ │ │ │ │ │ │ │ └── output.js
│ │ │ │ │ │ │ │ ├── expo-router-link-as-child/
│ │ │ │ │ │ │ │ │ ├── code.js
│ │ │ │ │ │ │ │ │ └── output.js
│ │ │ │ │ │ │ │ ├── expo-router-link-as-child-false-static/
│ │ │ │ │ │ │ │ │ ├── code.js
│ │ │ │ │ │ │ │ │ └── output.js
│ │ │ │ │ │ │ │ ├── expo-router-link-as-child-nested-view/
│ │ │ │ │ │ │ │ │ ├── code.js
│ │ │ │ │ │ │ │ │ └── output.js
│ │ │ │ │ │ │ │ ├── expo-router-link-namespace-as-child/
│ │ │ │ │ │ │ │ │ ├── code.js
│ │ │ │ │ │ │ │ │ └── output.js
│ │ │ │ │ │ │ │ ├── flattens-styles-at-runtime/
│ │ │ │ │ │ │ │ │ ├── code.js
│ │ │ │ │ │ │ │ │ └── output.js
│ │ │ │ │ │ │ │ ├── force-comment/
│ │ │ │ │ │ │ │ │ ├── code.js
│ │ │ │ │ │ │ │ │ └── output.js
│ │ │ │ │ │ │ │ ├── ignore-comment/
│ │ │ │ │ │ │ │ │ ├── code.js
│ │ │ │ │ │ │ │ │ └── output.js
│ │ │ │ │ │ │ │ ├── mixed-children-types/
│ │ │ │ │ │ │ │ │ ├── code.js
│ │ │ │ │ │ │ │ │ └── output.js
│ │ │ │ │ │ │ │ ├── nested-in-object-with-ignore-comment/
│ │ │ │ │ │ │ │ │ ├── code.js
│ │ │ │ │ │ │ │ │ └── output.js
│ │ │ │ │ │ │ │ ├── non-react-native-import/
│ │ │ │ │ │ │ │ │ ├── code.js
│ │ │ │ │ │ │ │ │ └── output.js
│ │ │ │ │ │ │ │ ├── normalize-accessibility-props/
│ │ │ │ │ │ │ │ │ ├── code.js
│ │ │ │ │ │ │ │ │ └── output.js
│ │ │ │ │ │ │ │ ├── normalize-accessibility-props-and-flatten-styles/
│ │ │ │ │ │ │ │ │ ├── code.js
│ │ │ │ │ │ │ │ │ └── output.js
│ │ │ │ │ │ │ │ ├── number-of-lines/
│ │ │ │ │ │ │ │ │ ├── code.js
│ │ │ │ │ │ │ │ │ └── output.js
│ │ │ │ │ │ │ │ ├── pressables/
│ │ │ │ │ │ │ │ │ ├── code.js
│ │ │ │ │ │ │ │ │ └── output.js
│ │ │ │ │ │ │ │ ├── resolvable-spread-props/
│ │ │ │ │ │ │ │ │ ├── code.js
│ │ │ │ │ │ │ │ │ └── output.js
│ │ │ │ │ │ │ │ ├── text-content/
│ │ │ │ │ │ │ │ │ ├── code.js
│ │ │ │ │ │ │ │ │ └── output.js
│ │ │ │ │ │ │ │ ├── text-content-as-explicit-child-prop/
│ │ │ │ │ │ │ │ │ ├── code.js
│ │ │ │ │ │ │ │ │ └── output.js
│ │ │ │ │ │ │ │ ├── two-components/
│ │ │ │ │ │ │ │ │ ├── code.js
│ │ │ │ │ │ │ │ │ └── output.js
│ │ │ │ │ │ │ │ ├── unresolvable-spread-props/
│ │ │ │ │ │ │ │ │ ├── code.js
│ │ │ │ │ │ │ │ │ └── output.js
│ │ │ │ │ │ │ │ └── variable-child-no-string/
│ │ │ │ │ │ │ │ ├── code.js
│ │ │ │ │ │ │ │ └── output.js
│ │ │ │ │ │ │ └── index.test.ts
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ └── view/
│ │ │ │ │ ├── __tests__/
│ │ │ │ │ │ ├── fixtures/
│ │ │ │ │ │ │ ├── basic/
│ │ │ │ │ │ │ │ ├── code.js
│ │ │ │ │ │ │ │ └── output.js
│ │ │ │ │ │ │ ├── force-comment/
│ │ │ │ │ │ │ │ ├── code.js
│ │ │ │ │ │ │ │ └── output.js
│ │ │ │ │ │ │ ├── ignore-comment/
│ │ │ │ │ │ │ │ ├── code.js
│ │ │ │ │ │ │ │ └── output.js
│ │ │ │ │ │ │ ├── indirect-text-ancestor/
│ │ │ │ │ │ │ │ ├── code.js
│ │ │ │ │ │ │ │ └── output.js
│ │ │ │ │ │ │ ├── react-native-namespace-text-ancestor/
│ │ │ │ │ │ │ │ ├── code.js
│ │ │ │ │ │ │ │ └── output.js
│ │ │ │ │ │ │ ├── same-file-safe-ancestor/
│ │ │ │ │ │ │ │ ├── code.js
│ │ │ │ │ │ │ │ └── output.js
│ │ │ │ │ │ │ ├── same-file-unknown-ancestor/
│ │ │ │ │ │ │ │ ├── code.js
│ │ │ │ │ │ │ │ └── output.js
│ │ │ │ │ │ │ ├── text-ancestor/
│ │ │ │ │ │ │ │ ├── code.js
│ │ │ │ │ │ │ │ └── output.js
│ │ │ │ │ │ │ ├── unknown-imported-ancestor/
│ │ │ │ │ │ │ │ ├── code.js
│ │ │ │ │ │ │ │ ├── dangerous-output.js
│ │ │ │ │ │ │ │ └── output.js
│ │ │ │ │ │ │ └── unresolvable-spread-props/
│ │ │ │ │ │ │ ├── code.js
│ │ │ │ │ │ │ └── output.js
│ │ │ │ │ │ └── index.test.ts
│ │ │ │ │ └── index.ts
│ │ │ │ ├── types/
│ │ │ │ │ └── index.ts
│ │ │ │ └── utils/
│ │ │ │ ├── __tests__/
│ │ │ │ │ └── logger.test.ts
│ │ │ │ ├── common/
│ │ │ │ │ ├── attributes.ts
│ │ │ │ │ ├── base.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── validation.ts
│ │ │ │ ├── constants.ts
│ │ │ │ ├── format-test-result.ts
│ │ │ │ ├── generate-test-plugin.ts
│ │ │ │ ├── helpers.ts
│ │ │ │ ├── logger.ts
│ │ │ │ └── plugin-error.ts
│ │ │ └── runtime/
│ │ │ ├── __tests__/
│ │ │ │ ├── index.test.ts
│ │ │ │ └── mocks/
│ │ │ │ └── react-native.ts
│ │ │ ├── components/
│ │ │ │ ├── native-text.tsx
│ │ │ │ └── native-view.tsx
│ │ │ ├── index.ts
│ │ │ ├── index.web.ts
│ │ │ ├── types/
│ │ │ │ ├── index.ts
│ │ │ │ └── react-native.d.ts
│ │ │ └── utils/
│ │ │ └── constants.ts
│ │ ├── tsconfig.build.json
│ │ ├── tsconfig.json
│ │ └── vitest.config.ts
│ └── react-native-time-to-render/
│ ├── .gitignore
│ ├── LICENSE
│ ├── README.md
│ ├── TimeToRender.podspec
│ ├── android/
│ │ ├── build.gradle
│ │ ├── gradle.properties
│ │ └── src/
│ │ └── main/
│ │ ├── AndroidManifest.xml
│ │ ├── AndroidManifestNew.xml
│ │ └── java/
│ │ └── com/
│ │ └── timetorender/
│ │ ├── MarkerStore.kt
│ │ ├── OnMarkerPaintedEvent.kt
│ │ ├── TimeToRenderModule.kt
│ │ ├── TimeToRenderPackage.kt
│ │ ├── TimeToRenderView.kt
│ │ └── TimeToRenderViewManager.kt
│ ├── ios/
│ │ ├── MarkerPaintComponentView.h
│ │ ├── MarkerPaintComponentView.mm
│ │ ├── MarkerStore.h
│ │ ├── MarkerStore.m
│ │ ├── PaintMarkerView.h
│ │ ├── PaintMarkerView.m
│ │ ├── TimeToRender.h
│ │ ├── TimeToRender.mm
│ │ ├── TimeToRenderManager.h
│ │ └── TimeToRenderManager.m
│ ├── package.json
│ ├── react-native.config.js
│ └── src/
│ ├── NativeTimeToRender.ts
│ ├── TimeToRenderNativeComponent.ts
│ └── index.tsx
├── pnpm-workspace.yaml
└── tsconfig.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/FUNDING.yml
================================================
github: mfkrause
================================================
FILE: .github/actions/setup/action.yml
================================================
name: Setup
description: Setup Node.js and install dependencies
runs:
using: composite
steps:
- uses: pnpm/action-setup@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
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
================================================
FILE: .github/workflows/release.yml
================================================
name: Release
on:
workflow_dispatch:
inputs:
release_type:
description: 'Release type (patch, minor, major, or a semver version)'
required: false
type: string
npm_tag:
description: 'Optional npm dist-tag override (e.g. latest, v0, v1)'
required: false
type: string
github_make_latest:
description: 'Optional GitHub latest release override (true or false)'
required: false
type: string
permissions:
contents: write
jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup
uses: ./.github/actions/setup
- name: Lint files
run: pnpm lint
- name: Test formatting
run: pnpm format --check
- name: Typecheck files
run: pnpm typecheck
test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup
uses: ./.github/actions/setup
- name: Run tests
run: pnpm test
build-release:
runs-on: ubuntu-latest
needs: [lint, test]
permissions:
contents: write
id-token: write
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup
uses: ./.github/actions/setup
- name: Build package
run: pnpm package build
- name: Configure Git
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
- name: Validate release type
if: ${{ inputs.release_type != '' }}
run: |
if [[ "${{ inputs.release_type }}" =~ ^(patch|minor|major)$ ]]; then
echo "Valid release type: ${{ inputs.release_type }}"
elif [[ "${{ inputs.release_type }}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Valid semver version: ${{ inputs.release_type }}"
else
echo "Invalid input. Must be 'patch', 'minor', 'major', or a valid semver version (e.g., 1.2.3)."
exit 1
fi
- name: Resolve release strategy
run: |
branch="${{ github.ref_name }}"
release_type="${{ inputs.release_type }}"
npm_tag_input="${{ inputs.npm_tag }}"
github_make_latest_input="${{ inputs.github_make_latest }}"
package_version=$(node -p "require('./packages/react-native-boost/package.json').version")
package_major="${package_version%%.*}"
case "$github_make_latest_input" in
""|true|false) ;;
*)
echo "Invalid github_make_latest input. Must be 'true' or 'false' when provided."
exit 1
;;
esac
if [[ "$branch" =~ ^v([0-9]+)$ ]]; then
branch_major="${BASH_REMATCH[1]}"
release_npm_tag="${npm_tag_input:-v${branch_major}}"
release_github_make_latest="${github_make_latest_input:-false}"
if [[ "$package_major" != "$branch_major" ]]; then
echo "Branch $branch expects package major $branch_major, but package.json is $package_version."
exit 1
fi
if [[ -n "$release_type" ]]; then
if [[ "$release_type" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
release_major="${release_type%%.*}"
if [[ "$release_major" != "$branch_major" ]]; then
echo "Explicit version $release_type does not match branch major $branch_major."
exit 1
fi
elif [[ "$release_type" == "major" ]]; then
echo "Major increments are not allowed on maintenance branch $branch."
exit 1
fi
fi
elif [[ "$branch" == "main" || "$branch" == "master" ]]; then
release_npm_tag="${npm_tag_input:-latest}"
release_github_make_latest="${github_make_latest_input:-true}"
else
if [[ -z "$npm_tag_input" || -z "$github_make_latest_input" ]]; then
echo "Releases from branch '$branch' require explicit npm_tag and github_make_latest inputs."
exit 1
fi
release_npm_tag="$npm_tag_input"
release_github_make_latest="$github_make_latest_input"
fi
if [[ ! "$release_npm_tag" =~ ^[A-Za-z0-9._-]+$ ]]; then
echo "Invalid npm tag '$release_npm_tag'. Use only letters, numbers, dots, underscores, and dashes."
exit 1
fi
echo "RELEASE_NPM_TAG=$release_npm_tag" >> "$GITHUB_ENV"
echo "RELEASE_GITHUB_MAKE_LATEST=$release_github_make_latest" >> "$GITHUB_ENV"
echo "Release branch: $branch"
echo "npm tag: $release_npm_tag"
echo "GitHub make_latest: $release_github_make_latest"
- name: Release
run: |
if [ -n "${{ inputs.release_type }}" ]; then
pnpm package release --increment "${{ inputs.release_type }}" --npm.tag "$RELEASE_NPM_TAG" --github.makeLatest "$RELEASE_GITHUB_MAKE_LATEST"
else
pnpm package release --npm.tag "$RELEASE_NPM_TAG" --github.makeLatest "$RELEASE_GITHUB_MAKE_LATEST"
fi
env:
GITHUB_TOKEN: ${{ github.token }}
================================================
FILE: .github/workflows/stale.yml
================================================
name: 'Close stale issues and PRs'
on:
schedule:
- cron: '30 1 * * *'
jobs:
stale:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- uses: actions/stale@v9
with:
stale-issue-message: 'This issue is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 7 days.'
stale-pr-message: 'This PR is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 7 days.'
days-before-stale: 60
days-before-close: 7
stale-issue-label: stale
stale-pr-label: stale
exempt-issue-labels: 'help wanted,in progress,pinned'
exempt-pr-labels: 'in progress,pinned'
stale-missing-info:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- uses: actions/stale@v9
with:
any-of-labels: 'repro-missing'
stale-issue-message: 'This issue is stale because it is missing information. Please add the requested information or this will be closed in 7 days.'
stale-pr-message: 'This PR is stale because it is missing information. Please add the requested information or this will be closed in 7 days.'
days-before-stale: 14
days-before-close: 7
stale-issue-label: stale
stale-pr-label: stale
================================================
FILE: .github/workflows/test.yml
================================================
name: Test & build
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup
uses: ./.github/actions/setup
- name: Lint files
run: pnpm lint
- name: Test formatting
run: pnpm format --check
- name: Verify formatted code is unchanged
run: git diff --exit-code HEAD
- name: Typecheck files
run: pnpm typecheck
test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup
uses: ./.github/actions/setup
- name: Run tests
run: pnpm test
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup
uses: ./.github/actions/setup
- name: Build plugin
run: pnpm package build
================================================
FILE: .gitignore
================================================
*.tgz
package-lock.json
yarn.lock
# OSX
.DS_Store
# VSCode
.vscode/
jsconfig.json
# node.js
#
node_modules/
npm-debug.log
yarn-debug.log
yarn-error.log
# pnpm
.pnpm-store/
# Turborepo
.turbo/
================================================
FILE: .husky/commit-msg
================================================
npx --no-install commitlint --edit $1
================================================
FILE: .husky/pre-commit
================================================
npx --no-install lint-staged
================================================
FILE: .lintstagedrc
================================================
{
"*.{js,mjs,cjs,jsx,ts,tsx}": ["pnpm exec oxlint --fix", "pnpm exec oxfmt --write"],
"*.json": "pnpm exec oxfmt --write"
}
================================================
FILE: .npmrc
================================================
node-linker=hoisted
================================================
FILE: .nvmrc
================================================
v24
================================================
FILE: .oxfmtrc.json
================================================
{
"$schema": "./node_modules/oxfmt/configuration_schema.json",
"printWidth": 120,
"tabWidth": 2,
"useTabs": false,
"semi": true,
"singleQuote": true,
"quoteProps": "consistent",
"jsxSingleQuote": false,
"trailingComma": "es5",
"bracketSpacing": true,
"bracketSameLine": true,
"arrowParens": "always",
"endOfLine": "lf",
"sortPackageJson": false,
"ignorePatterns": ["CHANGELOG.md", "apps/docs/**/*.md", "apps/docs/**/*.mdx", "apps/docs/.docusaurus/**"]
}
================================================
FILE: .oxlintrc.json
================================================
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["eslint", "typescript", "unicorn"],
"categories": {
"correctness": "error"
},
"env": {
"node": true
},
"ignorePatterns": [
"**/fixtures",
"**/*.config.{js,mjs,cjs}",
"**/scripts",
"**/__tests__",
"apps/docs/.docusaurus/**"
],
"rules": {
"no-unused-vars": [
"error",
{
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_",
"caughtErrorsIgnorePattern": "^_"
}
],
"@typescript-eslint/ban-ts-comment": "error",
"no-array-constructor": "error",
"@typescript-eslint/no-empty-object-type": "error",
"@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/no-namespace": "error",
"@typescript-eslint/no-require-imports": "error",
"@typescript-eslint/no-unnecessary-type-constraint": "error",
"@typescript-eslint/no-unsafe-function-type": "error",
"unicorn/catch-error-name": "error",
"unicorn/consistent-assert": "error",
"unicorn/consistent-date-clone": "error",
"unicorn/consistent-empty-array-spread": "error",
"unicorn/consistent-existence-index-check": "error",
"unicorn/consistent-function-scoping": "error",
"unicorn/empty-brace-spaces": "error",
"unicorn/error-message": "error",
"unicorn/escape-case": "error",
"unicorn/explicit-length-check": "error",
"unicorn/filename-case": "error",
"unicorn/new-for-builtins": "error",
"unicorn/no-accessor-recursion": "error",
"unicorn/no-anonymous-default-export": "error",
"unicorn/no-array-callback-reference": "error",
"unicorn/no-array-for-each": "error",
"unicorn/no-array-method-this-argument": "error",
"unicorn/no-array-reduce": "error",
"unicorn/no-await-expression-member": "error",
"unicorn/no-console-spaces": "error",
"unicorn/no-document-cookie": "error",
"unicorn/no-hex-escape": "error",
"unicorn/no-instanceof-builtins": "error",
"unicorn/no-length-as-slice-end": "error",
"unicorn/no-lonely-if": "error",
"unicorn/no-magic-array-flat-depth": "error",
"unicorn/no-negated-condition": "error",
"unicorn/no-negation-in-equality-check": "error",
"unicorn/no-new-buffer": "error",
"unicorn/no-object-as-default-parameter": "error",
"unicorn/no-process-exit": "error",
"unicorn/no-static-only-class": "error",
"unicorn/no-this-assignment": "error",
"unicorn/no-typeof-undefined": "error",
"unicorn/no-unreadable-array-destructuring": "error",
"unicorn/no-unreadable-iife": "error",
"unicorn/no-useless-promise-resolve-reject": "error",
"unicorn/no-useless-switch-case": "error",
"unicorn/no-useless-undefined": "error",
"unicorn/no-null": "off",
"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",
"unicorn/prefer-array-flat-map": "error",
"unicorn/prefer-array-flat": "error",
"unicorn/prefer-array-index-of": "error",
"unicorn/prefer-array-some": "error",
"unicorn/prefer-at": "error",
"unicorn/prefer-blob-reading-methods": "error",
"unicorn/prefer-code-point": "error",
"unicorn/prefer-date-now": "error",
"unicorn/prefer-default-parameters": "error",
"unicorn/prefer-dom-node-append": "error",
"unicorn/prefer-dom-node-dataset": "error",
"unicorn/prefer-dom-node-remove": "error",
"unicorn/prefer-dom-node-text-content": "error",
"unicorn/prefer-event-target": "error",
"unicorn/prefer-global-this": "error",
"unicorn/prefer-includes": "error",
"unicorn/prefer-keyboard-event-key": "error",
"unicorn/prefer-logical-operator-over-ternary": "error",
"unicorn/prefer-math-min-max": "error",
"unicorn/prefer-math-trunc": "error",
"unicorn/prefer-modern-dom-apis": "error",
"unicorn/prefer-modern-math-apis": "error",
"unicorn/prefer-module": "error",
"unicorn/prefer-native-coercion-functions": "error",
"unicorn/prefer-negative-index": "error",
"unicorn/prefer-node-protocol": "error",
"unicorn/prefer-number-properties": "error",
"unicorn/prefer-object-from-entries": "error",
"unicorn/prefer-optional-catch-binding": "error",
"unicorn/prefer-prototype-methods": "error",
"unicorn/prefer-query-selector": "error",
"unicorn/prefer-reflect-apply": "error",
"unicorn/prefer-regexp-test": "error",
"unicorn/prefer-set-has": "error",
"unicorn/prefer-spread": "error",
"unicorn/prefer-string-raw": "error",
"unicorn/prefer-string-replace-all": "error",
"unicorn/prefer-string-slice": "error",
"unicorn/prefer-string-trim-start-end": "error",
"unicorn/prefer-structured-clone": "error",
"unicorn/prefer-ternary": "error",
"unicorn/prefer-top-level-await": "off",
"unicorn/prefer-type-error": "error",
"unicorn/relative-url-style": "error",
"unicorn/require-array-join-separator": "error",
"unicorn/require-number-to-fixed-digits-argument": "error",
"unicorn/switch-case-braces": "error",
"unicorn/text-encoding-identifier-case": "error",
"unicorn/throw-new-error": "error"
},
"overrides": [
{
"files": ["packages/react-native-time-to-render/src/*.ts"],
"rules": {
"unicorn/filename-case": "off"
}
}
]
}
================================================
FILE: .zed/settings.json
================================================
{
"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"
}
}
]
}
}
}
================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, caste, color, religion, or sexual
identity and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
- Demonstrating empathy and kindness toward other people
- Being respectful of differing opinions, viewpoints, and experiences
- Giving and gracefully accepting constructive feedback
- Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
- Focusing on what is best not just for us as individuals, but for the overall
community
Examples of unacceptable behavior include:
- The use of sexualized language or imagery, and sexual attention or advances of
any kind
- Trolling, insulting or derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or email address,
without their explicit permission
- Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
[INSERT CONTACT METHOD].
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series of
actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or permanent
ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within the
community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.1, available at
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
Community Impact Guidelines were inspired by
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
For answers to common questions about this code of conduct, see the FAQ at
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
[https://www.contributor-covenant.org/translations][translations].
[homepage]: https://www.contributor-covenant.org
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
[Mozilla CoC]: https://github.com/mozilla/diversity
[FAQ]: https://www.contributor-covenant.org/faq
[translations]: https://www.contributor-covenant.org/translations
================================================
FILE: CONTRIBUTING.md
================================================
# Contributing
Contributions are always welcome, no matter how large or small!
We want this community to be friendly and respectful to each other. Please follow it in all your interactions with the project. Before contributing, please read the [code of conduct](./CODE_OF_CONDUCT.md).
## Development workflow
To get started with the project, run `pnpm install` in the root directory to install the required dependencies for each package:
```sh
pnpm install
```
> While it's possible to use [`npm`](https://github.com/npm/cli), the tooling is built around [`pnpm`](https://pnpm.io/), so you'll have an easier time if you use `pnpm` for development.
While developing, you can run the [example app](/example/) to test your changes.
To have package changes automatically reflected in the example app, run the package build watcher and Expo together:
```sh
pnpm dev
```
This runs `rollup -w` for `packages/react-native-boost` and `expo start` for `apps/example` in parallel.
If you change native code, you'll still need to rebuild the example app.
To start only the example app packager:
```sh
pnpm example start
```
To run the example app on Android:
```sh
pnpm example android
```
To run the example app on iOS:
```sh
pnpm example ios
```
Make sure your code passes TypeScript and Oxlint. Run the following to verify:
```sh
pnpm typecheck
pnpm lint
```
To fix formatting errors, run the following:
```sh
pnpm lint -- --fix
```
Remember to add tests for your change if possible. Run the unit tests by:
```sh
pnpm test
```
### Commit message convention
We follow the [conventional commits specification](https://www.conventionalcommits.org/en) for our commit messages:
- `fix`: bug fixes, e.g. fix crash due to deprecated method.
- `feat`: new features, e.g. add new method to the module.
- `refactor`: code refactor, e.g. migrate from class components to hooks.
- `docs`: changes into documentation, e.g. add usage example for the module..
- `test`: adding or updating tests, e.g. add integration tests using detox.
- `chore`: tooling changes, e.g. change CI config.
Our pre-commit hooks verify that your commit message matches this format when committing.
### Linting and tests
[Oxlint](https://oxc.rs/docs/guide/usage/linter), [Oxfmt](https://oxc.rs/docs/guide/usage/formatter), [TypeScript](https://www.typescriptlang.org/)
We use [TypeScript](https://www.typescriptlang.org/) for type checking, and [Oxlint](https://oxc.rs/docs/guide/usage/linter) with [Oxfmt](https://oxc.rs/docs/guide/usage/formatter) for linting and formatting the code.
Our pre-commit hooks verify that the linter and tests pass when committing.
### Publishing to npm
We use [release-it](https://github.com/release-it/release-it) to make it easier to publish new versions. It handles common tasks like bumping version based on semver, creating tags and releases etc.
To publish new versions, run the following:
```sh
pnpm package release
```
### Scripts
The `package.json` file contains various scripts for common tasks:
- `pnpm install`: install all workspace dependencies.
- `pnpm typecheck`: type-check files with TypeScript.
- `pnpm lint`: lint files with Oxlint.
- `pnpm example start`: start the Metro server for the example app.
- `pnpm dev`: start the example app and watch/rebuild `react-native-boost` package changes.
- `pnpm example android`: run the example app on Android.
- `pnpm example ios`: run the example app on iOS.
### Sending a pull request
> **Working on your first pull request?** You can learn how from this _free_ series: [How to Contribute to an Open Source Project on GitHub](https://app.egghead.io/playlists/how-to-contribute-to-an-open-source-project-on-github).
When you're sending a pull request:
- Prefer small pull requests focused on one change.
- Verify that linters and tests are passing.
- Review the documentation to make sure it looks good.
- Follow the pull request template when opening a pull request.
- For pull requests that change the API or implementation, discuss with maintainers first by opening an issue.
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) Kuatsu App Agency
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: README.md
================================================
# 🚀 react-native-boost
  
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.
- ⚡ 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
## Documentation
The documentation is available at [react-native-boost.oss.kuatsu.de](https://react-native-boost.oss.kuatsu.de).
## Benchmark
The app in the `apps/example` directory serves as a benchmark for the performance of the plugin.
More benchmarks are available in the [docs](https://react-native-boost.oss.kuatsu.de/docs/information/benchmarks).
## Compatibility
| `react-native-boost` | React Native |
| -------------------- | ---------------- |
| `0.x` | All versions[^1] |
| `1.x` | `>=0.83` |
[^1]: Starting from React Native `0.80`, `react-native-boost@0` prints import deprecation warnings.
## 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/).
```sh
npm install react-native-boost
# or
yarn add react-native-boost
```
Then, add the plugin to your Babel configuration (`babel.config.js`):
```js
module.exports = {
plugins: ['react-native-boost/plugin'],
};
```
If you're using Expo and don't see the `babel.config.js` file, run the following command to create it:
```sh
npx expo customize babel.config.js
```
Finally, restart your React Native development server and clear the bundler cache:
```sh
npm start --clear
# or
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).
## How it works
A technical rundown of how the plugin works can be found in the [docs](https://react-native-boost.oss.kuatsu.de/docs/information/how-it-works).
## Contributing
See the [contributing guide](CONTRIBUTING.md) to learn how to contribute to the repository and the development workflow.
## License
MIT
================================================
FILE: apps/docs/.gitignore
================================================
# deps
/node_modules
# generated content
.source
# test & build
/coverage
/.next/
/out/
/build
*.tsbuildinfo
# misc
.DS_Store
*.pem
/.pnp
.pnp.js
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# others
.env*.local
.vercel
next-env.d.ts
================================================
FILE: apps/docs/README.md
================================================
# React Native Boost Docs
This app hosts the Fumadocs site for `react-native-boost`.
## Development
```bash
pnpm --filter react-native-boost-docs dev
```
## Build
```bash
pnpm --filter react-native-boost-docs build
```
## Content Layout
- `content/docs`: documentation pages and `meta.json` navigation files
- `app/docs`: docs layout and page rendering routes
- `lib/source.ts`: Fumadocs source loader configuration
================================================
FILE: apps/docs/app/(home)/layout.tsx
================================================
import { HomeLayout } from 'fumadocs-ui/layouts/home';
import { baseOptions } from '@/lib/layout.shared';
export default function Layout({ children }: LayoutProps<'/'>) {
return {children} ;
}
================================================
FILE: apps/docs/app/(home)/page.tsx
================================================
import Link from 'next/link';
import { Rocket, ShieldCheck, Zap } from 'lucide-react';
export default function HomePage() {
return (
React Native Boost
Improve your app's performance with one line of code.
A Babel plugin that replaces analyzes your code and performs safe optimizations to reduce unnecessary runtime
overhead in React Native apps.
Read docs
Faster renders
Removes runtime overhead from wrapper components to improve UI-heavy screens.
Safety first
Conservative analysis skips uncertain optimizations to reduce behavioral risk.
Minimal setup
Install, add the Babel plugin, get instant improvements. No code changes required.
);
}
================================================
FILE: apps/docs/app/api/search/route.ts
================================================
import { source } from '@/lib/source';
import { createFromSource } from 'fumadocs-core/search/server';
export const { GET } = createFromSource(source, {
// https://docs.orama.com/docs/orama-js/supported-languages
language: 'english',
});
================================================
FILE: apps/docs/app/docs/[[...slug]]/page.tsx
================================================
import { getPageImage, source } from '@/lib/source';
import { DocsBody, DocsDescription, DocsPage, DocsTitle } from 'fumadocs-ui/layouts/docs/page';
import { notFound } from 'next/navigation';
import { getMDXComponents } from '@/mdx-components';
import type { Metadata } from 'next';
import { createRelativeLink } from 'fumadocs-ui/mdx';
import type { TOCItemType } from 'fumadocs-core/toc';
import { LLMCopyButton, ViewOptions } from '@/components/ai/page-actions';
import { gitConfig } from '@/lib/layout.shared';
import { getAutoOptionSectionsToc } from '@/components/docs/auto-option-sections';
import { getRuntimeReferenceToc } from '@/components/docs/auto-runtime-reference';
const runtimeReferencePath = '../../packages/react-native-boost/src/runtime/index.ts';
const pluginTypesPath = '../../packages/react-native-boost/src/plugin/types/index.ts';
type TocInsertion = {
afterUrl: string;
items: TOCItemType[];
};
async function getTocInsertions(slug: string): Promise {
if (slug === 'runtime-library') {
return [
{
afterUrl: '#api-reference',
items: getRuntimeReferenceToc(runtimeReferencePath),
},
];
}
if (slug === 'configuration/configure') {
const [pluginOptionsToc, pluginOptimizationOptionsToc] = await Promise.all([
getAutoOptionSectionsToc({
path: pluginTypesPath,
name: 'PluginOptions',
idPrefix: 'plugin-options',
depth: 3,
}),
getAutoOptionSectionsToc({
path: pluginTypesPath,
name: 'PluginOptimizationOptions',
idPrefix: 'plugin-optimization-options',
depth: 3,
}),
]);
return [
{
afterUrl: '#plugin-options',
items: pluginOptionsToc,
},
{
afterUrl: '#plugin-optimization-options',
items: pluginOptimizationOptionsToc,
},
];
}
return [];
}
function mergeToc(baseToc: TOCItemType[], insertions: TocInsertion[]): TOCItemType[] {
if (insertions.length === 0) {
return baseToc;
}
const remainingInsertions = [...insertions];
const mergedToc: TOCItemType[] = [];
for (const item of baseToc) {
mergedToc.push(item);
for (let index = 0; index < remainingInsertions.length; index += 1) {
const insertion = remainingInsertions[index];
if (insertion.afterUrl !== item.url) {
continue;
}
mergedToc.push(...insertion.items);
remainingInsertions.splice(index, 1);
index -= 1;
}
}
return mergedToc;
}
export default async function Page(props: PageProps<'/docs/[[...slug]]'>) {
const params = await props.params;
const page = source.getPage(params.slug);
if (!page) notFound();
const MDX = page.data.body;
const slug = page.slugs.join('/');
const tocInsertions = await getTocInsertions(slug);
const toc = mergeToc(page.data.toc ?? [], tocInsertions);
return (
{page.data.title}
{page.data.description}
);
}
export async function generateStaticParams() {
return source.generateParams();
}
export async function generateMetadata(props: PageProps<'/docs/[[...slug]]'>): Promise {
const params = await props.params;
const page = source.getPage(params.slug);
if (!page) notFound();
return {
title: page.data.title,
description: page.data.description,
openGraph: {
images: getPageImage(page).url,
},
};
}
================================================
FILE: apps/docs/app/docs/layout.tsx
================================================
import { source } from '@/lib/source';
import { DocsLayout } from 'fumadocs-ui/layouts/docs';
import { baseOptions } from '@/lib/layout.shared';
export default function Layout({ children }: LayoutProps<'/docs'>) {
return (
{children}
);
}
================================================
FILE: apps/docs/app/global.css
================================================
@import 'tailwindcss';
@import 'fumadocs-ui/css/neutral.css';
@import 'fumadocs-ui/css/preset.css';
:root {
--color-fd-primary: #ff9800;
--color-fd-primary-foreground: #2f1d00;
--color-fd-ring: #ff9800;
}
.dark {
--color-fd-primary: #ffb74d;
--color-fd-primary-foreground: #2f1d00;
--color-fd-ring: #ffb74d;
}
================================================
FILE: apps/docs/app/layout.tsx
================================================
import { RootProvider } from 'fumadocs-ui/provider/next';
import './global.css';
import { Inter } from 'next/font/google';
import type { Metadata } from 'next';
const inter = Inter({
subsets: ['latin'],
});
export const metadata: Metadata = {
metadataBase: new URL('https://react-native-boost.oss.kuatsu.de'),
title: {
default: 'React Native Boost',
template: '%s | React Native Boost',
},
description: 'A Babel plugin and runtime toolkit for React Native performance optimizations.',
};
export default function Layout({ children }: LayoutProps<'/'>) {
return (
{children}
);
}
================================================
FILE: apps/docs/app/llms-full.txt/route.ts
================================================
import { getLLMText, source } from '@/lib/source';
export const revalidate = false;
export async function GET() {
const scan = [];
for (const page of source.getPages()) {
scan.push(getLLMText(page));
}
const scanned = await Promise.all(scan);
return new Response(scanned.join('\n\n'));
}
================================================
FILE: apps/docs/app/llms.mdx/docs/[[...slug]]/route.ts
================================================
import { getLLMText, source } from '@/lib/source';
import { notFound } from 'next/navigation';
export const revalidate = false;
export async function GET(_req: Request, { params }: RouteContext<'/llms.mdx/docs/[[...slug]]'>) {
const { slug } = await params;
const page = source.getPage(slug);
if (!page) notFound();
return new Response(await getLLMText(page), {
headers: {
'Content-Type': 'text/markdown',
},
});
}
export function generateStaticParams() {
return source.generateParams();
}
================================================
FILE: apps/docs/app/llms.txt/route.ts
================================================
import { source } from '@/lib/source';
export const revalidate = false;
export async function GET() {
const lines: string[] = [];
lines.push('# Documentation');
lines.push('');
for (const page of source.getPages()) {
lines.push(`- [${page.data.title}](${page.url}): ${page.data.description}`);
}
return new Response(lines.join('\n'));
}
================================================
FILE: apps/docs/app/og/docs/[...slug]/route.tsx
================================================
import { getPageImage, source } from '@/lib/source';
import { notFound } from 'next/navigation';
import { ImageResponse } from 'next/og';
import { generate as DefaultImage } from 'fumadocs-ui/og';
export const revalidate = false;
export async function GET(_req: Request, { params }: RouteContext<'/og/docs/[...slug]'>) {
const { slug } = await params;
const page = source.getPage(slug.slice(0, -1));
if (!page) notFound();
return new ImageResponse( , {
width: 1200,
height: 630,
});
}
export function generateStaticParams() {
return source.getPages().map((page) => ({
lang: page.locale,
slug: getPageImage(page).segments,
}));
}
================================================
FILE: apps/docs/components/ai/page-actions.tsx
================================================
'use client';
import { useMemo, useState } from 'react';
import { Check, ChevronDown, Copy, ExternalLinkIcon, TextIcon } from 'lucide-react';
import { cn } from '@/lib/cn';
import { useCopyButton } from 'fumadocs-ui/utils/use-copy-button';
import { buttonVariants } from 'fumadocs-ui/components/ui/button';
import { Popover, PopoverContent, PopoverTrigger } from 'fumadocs-ui/components/ui/popover';
const cache = new Map();
export function LLMCopyButton({
/**
* A URL to fetch the raw Markdown/MDX content of page
*/
markdownUrl,
}: {
markdownUrl: string;
}) {
const [isLoading, setLoading] = useState(false);
const [checked, onClick] = useCopyButton(async () => {
const cached = cache.get(markdownUrl);
if (cached) return navigator.clipboard.writeText(cached);
setLoading(true);
try {
await navigator.clipboard.write([
new ClipboardItem({
'text/plain': fetch(markdownUrl).then(async (res) => {
const content = await res.text();
cache.set(markdownUrl, content);
return content;
}),
}),
]);
} finally {
setLoading(false);
}
});
return (
{checked ? : }
Copy Markdown
);
}
export function ViewOptions({
markdownUrl,
githubUrl,
}: {
/**
* A URL to the raw Markdown/MDX content of page
*/
markdownUrl: string;
/**
* Source file URL on GitHub
*/
githubUrl: string;
}) {
const items = useMemo(() => {
const pageUrl = (globalThis as { location?: { href: string } }).location?.href ?? 'loading';
const q = `Read ${pageUrl}, I want to ask questions about it.`;
return [
{
title: 'Open in GitHub',
href: githubUrl,
icon: (
GitHub
),
},
{
title: 'View as Markdown',
href: markdownUrl,
icon: ,
},
{
title: 'Open in Scira AI',
href: `https://scira.ai/?${new URLSearchParams({
q,
})}`,
icon: (
Scira AI
),
},
{
title: 'Open in ChatGPT',
href: `https://chatgpt.com/?${new URLSearchParams({
hints: 'search',
q,
})}`,
icon: (
OpenAI
),
},
{
title: 'Open in Claude',
href: `https://claude.ai/new?${new URLSearchParams({
q,
})}`,
icon: (
Anthropic
),
},
{
title: 'Open in Cursor',
icon: (
Cursor
),
href: `https://cursor.com/link/prompt?${new URLSearchParams({
text: q,
})}`,
},
];
}, [githubUrl, markdownUrl]);
return (
Open
{items.map((item) => (
{item.icon}
{item.title}
))}
);
}
================================================
FILE: apps/docs/components/docs/auto-option-sections.tsx
================================================
import type { DocEntry, TypeTableProps } from 'fumadocs-typescript';
import { typeGenerator } from '../../lib/type-generator';
import { ReferenceSections, buildEntryToc, renderInlineCode, toSlug, type HeadingLevel } from './reference-sections';
type AutoOptionSectionsProps = Pick & {
headingLevel?: HeadingLevel;
idPrefix?: string;
};
type AutoOptionSectionsTocProps = Pick & {
idPrefix?: string;
depth?: HeadingLevel;
};
const optionEntriesCache = new Map>();
function getEntriesCacheKey({ path, name, type }: Pick): string {
return `${path ?? ''}|${name ?? ''}|${type ?? ''}`;
}
async function getOptionEntries(props: Pick): Promise {
const cacheKey = getEntriesCacheKey(props);
const cachedEntries = optionEntriesCache.get(cacheKey);
if (cachedEntries != null) {
return cachedEntries;
}
const entriesPromise = typeGenerator.generateTypeTable(props).then((docs) => docs.flatMap((doc) => doc.entries));
optionEntriesCache.set(cacheKey, entriesPromise);
return entriesPromise;
}
function resolveIdPrefix(props: Pick, idPrefix?: string): string {
const prefixSource = props.name ?? props.type ?? 'options';
return idPrefix ?? toSlug(prefixSource);
}
function readTagValues(entry: DocEntry, tagName: string): string[] {
const values: string[] = [];
for (const tag of entry.tags) {
if (tag.name !== tagName) {
continue;
}
const value = tag.text.trim();
if (value.length > 0) {
values.push(value);
}
}
return values;
}
export async function AutoOptionSections({ headingLevel = 3, idPrefix, ...props }: AutoOptionSectionsProps) {
const entries = await getOptionEntries(props);
const resolvedIdPrefix = resolveIdPrefix(props, idPrefix);
return (
{
const defaultValues = readTagValues(entry, 'default');
const extraTags = entry.tags.filter((tag) => tag.name !== 'default');
return (
<>
Type: {entry.type}
{defaultValues.length > 0 ? (
Default: {defaultValues[0]}
) : null}
{entry.required ? (
Required: true
) : null}
{entry.deprecated ? Deprecated.
: null}
{extraTags.length > 0 ? (
<>
Additional Notes
{extraTags.map((tag, index) => (
@{tag.name}: {' '}
{renderInlineCode(tag.text, `${entry.name}-tag-${tag.name}-${index}`)}
))}
>
) : null}
>
);
}}
/>
);
}
export async function getAutoOptionSectionsToc({ idPrefix, depth = 3, ...props }: AutoOptionSectionsTocProps) {
const entries = await getOptionEntries(props);
const resolvedIdPrefix = resolveIdPrefix(props, idPrefix);
return buildEntryToc({
entries,
idPrefix: resolvedIdPrefix,
depth,
});
}
================================================
FILE: apps/docs/components/docs/auto-runtime-reference.tsx
================================================
import { existsSync } from 'node:fs';
import nodePath from 'node:path';
import type { TOCItemType } from 'fumadocs-core/toc';
import { Node, Project, type ExportedDeclarations, type JSDocTag } from 'ts-morph';
import {
ReferenceSections,
buildEntryToc,
getEntryId,
renderInlineCode,
type BaseReferenceEntry,
} from './reference-sections';
type RuntimeExportKind = 'function' | 'component' | 'constant' | 'type';
type RuntimeParameter = {
name: string;
type: string;
description: string;
};
type RuntimeTag = {
name: string;
text: string;
};
type RuntimeExportEntry = BaseReferenceEntry & {
kind: RuntimeExportKind;
typeText: string;
parameters: RuntimeParameter[];
returnType?: string;
returnDescription?: string;
tags: RuntimeTag[];
sourceOrder: number;
declarationOrder: number;
};
type AutoRuntimeReferenceProps = {
path: string;
};
type SupportedDeclaration = Extract<
ExportedDeclarations,
| import('ts-morph').FunctionDeclaration
| import('ts-morph').VariableDeclaration
| import('ts-morph').TypeAliasDeclaration
>;
type ParsedDeclarationDocumentation = {
description: string;
parameterDescriptions: Map;
returnDescription?: string;
tags: RuntimeTag[];
};
const GROUP_TITLES: Record = {
function: 'Functions',
component: 'Components',
constant: 'Constants',
type: 'Types',
};
const GROUP_ORDER: RuntimeExportKind[] = ['function', 'component', 'constant', 'type'];
const RUNTIME_GROUP_ID_PREFIX = 'runtime-group';
const RUNTIME_EXPORT_ID_PREFIX = 'runtime-export';
let cachedProject: Project | undefined;
let cachedTsConfigPath: string | undefined;
const runtimeEntriesCache = new Map();
function resolveRepositoryRoot(startPath: string): string {
const candidates = [
startPath,
nodePath.resolve(startPath, '..'),
nodePath.resolve(startPath, '..', '..'),
nodePath.resolve(startPath, '..', '..', '..'),
];
for (const candidate of candidates) {
if (existsSync(nodePath.join(candidate, 'packages/react-native-boost/src/runtime/index.ts'))) {
return candidate;
}
}
throw new Error('Could not resolve repository root for runtime docs generation.');
}
function getProject(tsConfigPath: string): Project {
if (cachedProject != null && cachedTsConfigPath === tsConfigPath) {
return cachedProject;
}
cachedProject = new Project({
tsConfigFilePath: tsConfigPath,
});
cachedTsConfigPath = tsConfigPath;
return cachedProject;
}
function isSupportedDeclaration(declaration: ExportedDeclarations): declaration is SupportedDeclaration {
return (
Node.isFunctionDeclaration(declaration) ||
Node.isVariableDeclaration(declaration) ||
Node.isTypeAliasDeclaration(declaration)
);
}
function readTagComment(tag: JSDocTag): string {
const comment = tag.getComment();
if (typeof comment === 'string') {
return comment.trim();
}
if (!Array.isArray(comment)) {
return '';
}
return comment
.map((part) => {
if (part == null) {
return '';
}
if (typeof part === 'string') {
return part;
}
if (typeof part.getText === 'function') {
return part.getText();
}
return '';
})
.join('')
.trim();
}
function normalizeParameterDescription(value: string): string {
return value.replace(/^\s*-\s*/, '').trim();
}
function readDeclarationDocumentation(declaration: SupportedDeclaration): ParsedDeclarationDocumentation {
const jsDocs = Node.isVariableDeclaration(declaration)
? (declaration.getVariableStatement()?.getJsDocs() ?? [])
: declaration.getJsDocs();
const descriptions: string[] = [];
const parameterDescriptions = new Map();
const tags: RuntimeTag[] = [];
let returnDescription: string | undefined;
for (const jsDoc of jsDocs) {
const description = jsDoc.getDescription().trim();
if (description.length > 0) {
descriptions.push(description);
}
for (const tag of jsDoc.getTags()) {
const tagName = tag.getTagName();
const text = readTagComment(tag);
if (Node.isJSDocParameterTag(tag)) {
parameterDescriptions.set(tag.getName(), normalizeParameterDescription(text));
continue;
}
if (Node.isJSDocReturnTag(tag)) {
returnDescription = text;
continue;
}
tags.push({
name: tagName,
text,
});
}
}
return {
description: descriptions.join('\n\n'),
parameterDescriptions,
returnDescription,
tags,
};
}
function readKind(declaration: SupportedDeclaration): RuntimeExportKind {
if (Node.isFunctionDeclaration(declaration)) {
return 'function';
}
if (Node.isTypeAliasDeclaration(declaration)) {
return 'type';
}
const sourceFilePath = declaration.getSourceFile().getFilePath().replaceAll('\\', '/');
if (sourceFilePath.includes('/runtime/components/')) {
return 'component';
}
return 'constant';
}
function readTypeText(declaration: SupportedDeclaration): string {
if (Node.isFunctionDeclaration(declaration)) {
const parameterSignature = declaration
.getParameters()
.map((parameter) => `${parameter.getName()}: ${parameter.getType().getText(parameter)}`)
.join(', ');
const returnType = declaration.getReturnType().getText(declaration);
return `(${parameterSignature}) => ${returnType}`;
}
if (Node.isTypeAliasDeclaration(declaration)) {
return declaration.getTypeNode()?.getText() ?? declaration.getType().getText(declaration);
}
return declaration.getType().getText(declaration);
}
function readParameters(
declaration: SupportedDeclaration,
parameterDescriptions: Map
): RuntimeParameter[] {
if (!Node.isFunctionDeclaration(declaration)) {
return [];
}
return declaration.getParameters().map((parameter) => ({
name: parameter.getName(),
type: parameter.getType().getText(parameter),
description: parameterDescriptions.get(parameter.getName()) ?? '',
}));
}
function selectDeclaration(
declarations: ExportedDeclarations[],
sourceOrderByPath: Map
): SupportedDeclaration | undefined {
const supportedDeclarations = declarations.filter((declaration) => isSupportedDeclaration(declaration));
if (supportedDeclarations.length === 0) {
return undefined;
}
return [...supportedDeclarations].sort((left, right) => {
const leftOrder = sourceOrderByPath.get(left.getSourceFile().getFilePath()) ?? Number.MAX_SAFE_INTEGER;
const rightOrder = sourceOrderByPath.get(right.getSourceFile().getFilePath()) ?? Number.MAX_SAFE_INTEGER;
if (leftOrder !== rightOrder) {
return leftOrder - rightOrder;
}
return left.getStart() - right.getStart();
})[0];
}
function getGroupHeadingId(group: RuntimeExportKind): string {
return getEntryId(RUNTIME_GROUP_ID_PREFIX, group);
}
function getRuntimeExportEntries(indexFilePath: string): RuntimeExportEntry[] {
const repositoryRoot = resolveRepositoryRoot(process.cwd());
const packageTsConfigPath = nodePath.join(repositoryRoot, 'packages/react-native-boost/tsconfig.json');
const docsRoot = nodePath.join(repositoryRoot, 'apps/docs');
const project = getProject(packageTsConfigPath);
const absoluteIndexFilePath = nodePath.resolve(docsRoot, indexFilePath);
const cachedEntries = runtimeEntriesCache.get(absoluteIndexFilePath);
if (cachedEntries != null) {
return cachedEntries;
}
const indexFile = project.getSourceFile(absoluteIndexFilePath) ?? project.addSourceFileAtPath(absoluteIndexFilePath);
const sourceOrderByPath = new Map();
sourceOrderByPath.set(indexFile.getFilePath(), 0);
let sourceOrder = 1;
for (const exportDeclaration of indexFile.getExportDeclarations()) {
const sourceFile = exportDeclaration.getModuleSpecifierSourceFile();
if (sourceFile == null) {
continue;
}
const sourceFilePath = sourceFile.getFilePath();
if (!sourceOrderByPath.has(sourceFilePath)) {
sourceOrderByPath.set(sourceFilePath, sourceOrder);
sourceOrder += 1;
}
}
const entries: RuntimeExportEntry[] = [];
for (const [name, declarations] of indexFile.getExportedDeclarations()) {
const declaration = selectDeclaration(declarations, sourceOrderByPath);
if (declaration == null) {
continue;
}
const docs = readDeclarationDocumentation(declaration);
const kind = readKind(declaration);
const typeText = readTypeText(declaration);
const parameters = readParameters(declaration, docs.parameterDescriptions);
const returnType = Node.isFunctionDeclaration(declaration)
? declaration.getReturnType().getText(declaration)
: undefined;
entries.push({
name,
kind,
typeText,
description: docs.description,
parameters,
returnType,
returnDescription: docs.returnDescription,
tags: docs.tags,
sourceOrder: sourceOrderByPath.get(declaration.getSourceFile().getFilePath()) ?? Number.MAX_SAFE_INTEGER,
declarationOrder: declaration.getStart(),
});
}
const sortedEntries = entries.sort((left, right) => {
if (left.sourceOrder !== right.sourceOrder) {
return left.sourceOrder - right.sourceOrder;
}
return left.declarationOrder - right.declarationOrder;
});
runtimeEntriesCache.set(absoluteIndexFilePath, sortedEntries);
return sortedEntries;
}
export function getRuntimeReferenceToc(path: string): TOCItemType[] {
const entries = getRuntimeExportEntries(path);
const toc: TOCItemType[] = [];
for (const group of GROUP_ORDER) {
const groupEntries = entries.filter((entry) => entry.kind === group);
if (groupEntries.length === 0) {
continue;
}
toc.push({
title: GROUP_TITLES[group],
url: `#${getGroupHeadingId(group)}`,
depth: 3,
});
toc.push(
...buildEntryToc({
entries: groupEntries,
idPrefix: RUNTIME_EXPORT_ID_PREFIX,
depth: 4,
})
);
}
return toc;
}
export function AutoRuntimeReference({ path }: AutoRuntimeReferenceProps) {
const exports = getRuntimeExportEntries(path);
if (exports.length === 0) {
return Could not generate runtime reference from the provided entry file.
;
}
return (
<>
{GROUP_ORDER.map((group) => {
const groupEntries = exports.filter((entry) => entry.kind === group);
if (groupEntries.length === 0) {
return null;
}
return (
{GROUP_TITLES[group]}
{
const remarks = entry.tags.filter((tag) => tag.name === 'remarks' && tag.text.length > 0);
const additionalTags = entry.tags.filter((tag) => tag.name !== 'remarks' && tag.text.length > 0);
return (
<>
{entry.parameters.length > 0 ? (
<>
Parameters
{entry.parameters.map((parameter) => (
{parameter.name}: {parameter.type}
{parameter.description.length > 0 ? ' - ' : ''}
{parameter.description.length > 0
? renderInlineCode(parameter.description, `${entry.name}-parameter-${parameter.name}`)
: null}
))}
>
) : null}
{entry.returnType == null ? null : (
<>
Returns
{entry.returnType}
{entry.returnDescription != null && entry.returnDescription.length > 0 ? ': ' : ''}
{entry.returnDescription != null && entry.returnDescription.length > 0
? renderInlineCode(entry.returnDescription, `${entry.name}-returns`)
: null}
>
)}
{remarks.length > 0 ? (
<>
Notes
{remarks.map((tag, index) => (
{renderInlineCode(tag.text, `${entry.name}-remark-${index}`)}
))}
>
) : null}
{additionalTags.length > 0 ? (
<>
Additional Tags
{additionalTags.map((tag, index) => (
@{tag.name}: {' '}
{renderInlineCode(tag.text, `${entry.name}-tag-${tag.name}-${index}`)}
))}
>
) : null}
>
);
}}
/>
);
})}
>
);
}
================================================
FILE: apps/docs/components/docs/reference-sections.tsx
================================================
import type { TOCItemType } from 'fumadocs-core/toc';
import type { ReactNode } from 'react';
export type HeadingLevel = 2 | 3 | 4 | 5 | 6;
export type BaseReferenceEntry = {
name: string;
description: string;
};
type ReferenceSectionsProps = {
entries: TEntry[];
idPrefix: string;
emptyMessage: string;
headingLevel?: HeadingLevel;
renderMeta: (entry: TEntry) => ReactNode;
};
type BuildEntryTocProps = {
entries: TEntry[];
idPrefix: string;
depth: HeadingLevel;
};
export function toSlug(value: string): string {
const slug = value
.toLowerCase()
.replaceAll(/[^a-z0-9]+/g, '-')
.replace(/^-+/, '')
.replace(/-+$/, '');
return slug.length > 0 ? slug : 'reference-entry';
}
export function getEntryId(idPrefix: string, value: string): string {
return `${idPrefix}-${toSlug(value)}`;
}
function toParagraphs(value: string): string[] {
return value
.split(/\n{2,}/)
.map((paragraph) => paragraph.replaceAll('\n', ' ').trim())
.filter((paragraph) => paragraph.length > 0);
}
export function renderInlineCode(value: string, keyPrefix: string): ReactNode {
const parts = value.split(/`([^`]+)`/g);
return parts.map((part, index) => {
if (index % 2 === 1) {
return {part};
}
return {part} ;
});
}
export function buildEntryToc({
entries,
idPrefix,
depth,
}: BuildEntryTocProps): TOCItemType[] {
return entries.map((entry) => ({
title: entry.name,
url: `#${getEntryId(idPrefix, entry.name)}`,
depth,
}));
}
export function ReferenceSections({
entries,
idPrefix,
emptyMessage,
headingLevel = 3,
renderMeta,
}: ReferenceSectionsProps) {
if (entries.length === 0) {
return {emptyMessage}
;
}
const HeadingTag = `h${headingLevel}` as 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
return (
<>
{entries.map((entry) => {
const descriptionParagraphs = toParagraphs(entry.description);
return (
{entry.name}
{descriptionParagraphs.length > 0 ? (
descriptionParagraphs.map((paragraph, index) => (
{renderInlineCode(paragraph, `${entry.name}-description-${index}`)}
))
) : (
No description provided.
)}
{renderMeta(entry)}
);
})}
>
);
}
================================================
FILE: apps/docs/content/docs/configuration/boost-decorator.mdx
================================================
---
title: "Decorators"
description: Control optimization on individual JSX elements with @boost-ignore and @boost-force.
---
## @boost-ignore
Use `@boost-ignore` to disable optimization on a specific element.
If a line containing `@boost-ignore` appears immediately before a JSX opening tag, that component is skipped.
```jsx
This will be optimized.
{/* @boost-ignore */}
This will not be optimized.
```
## @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).
The only check that `@boost-force` does **not** override is the `react-native` import check — the component must still be imported from `react-native`.
```jsx
const Component = ({ props }) => {
return (
{/* @boost-force */}
This will be optimized despite having unresolvable spread props.
)
}
```
`@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 — for example,
when a wrapper component filters or handles props before passing them to the underlying native component.
================================================
FILE: apps/docs/content/docs/configuration/configure.mdx
================================================
---
title: Configure the Babel Plugin
description: Control logging, ignores, and optimization behavior.
---
The Babel plugin (`react-native-boost/plugin`) is the core of React Native Boost.
Defaults are safe and usable out of the box, but you can tune behavior for your app.
## Example Configuration
```js
// babel.config.js
module.exports = {
plugins: [
[
'react-native-boost/plugin',
{
verbose: false,
silent: false,
ignores: ['node_modules/**'],
optimizations: {
text: true,
view: true,
},
},
],
],
};
```
## Plugin Options
## Plugin Optimization Options
## Environment-Specific Enablement
You can enable React Native Boost by environment with Babel `env` config:
```js
module.exports = {
env: {
development: {
plugins: ['react-native-boost/plugin'],
},
},
};
```
See Babel docs: https://babeljs.io/docs/options#env
================================================
FILE: apps/docs/content/docs/configuration/nativewind.mdx
================================================
---
title: Nativewind Support
description: Configure cssInterop for optimized components.
---
If your app uses Nativewind, configure `cssInterop` for optimized components from
`react-native-boost/runtime`.
## Example
```jsx
import { cssInterop } from 'nativewind';
import { NativeText, NativeView } from 'react-native-boost/runtime';
cssInterop(NativeText, { className: 'style' });
cssInterop(NativeView, { className: 'style' });
```
This mirrors Nativewind's own mapping for `Text`/`View`.
## Known Limitations
### `select-*`
Nativewind maps `select-*` classes to `userSelect`. Native `Text` does not accept `userSelect` directly and uses the
`selectable` prop.
#### Recommended Usage
```jsx
// Avoid
Hello world
// Use
Hello world
// or
Hello world
```
You can map values with `userSelectToSelectableMap` from the runtime package.
### `align-*`
Nativewind maps `align-*` classes to `verticalAlign`, while native `Text` expects `textAlignVertical`.
#### Recommended Usage
```jsx
// Avoid
Hello world
// Use
Hello world
// or
Hello world
```
You can map values with `verticalAlignToTextAlignVerticalMap` from the runtime package.
================================================
FILE: apps/docs/content/docs/index.mdx
================================================
---
title: Getting Started
description: Install React Native Boost to boost your app's performance with one line of code.
---
import { Tab, Tabs } from 'fumadocs-ui/components/tabs'
## Introduction
React Native Boost consists of two pieces:
- A Babel plugin that statically analyzes your source code and replaces safe `Text` and `View` components with their direct native counterparts, leading to significant performance improvements compared to the JS-based wrapper components.
- A runtime package used by the plugin for cross-platform-safe imports and helper utilities.
The analyzer is intentionally strict and skips any optimizations that could lead to behavioral changes or other bugs.
## Compatibility
| `react-native-boost` | React Native |
| --- | --- |
| `0.x` | All versions[^1] |
| `1.x` | `>=0.83` |
[^1]: Starting from React Native `0.80`, `react-native-boost@0` prints import deprecation warnings. [See here.](https://github.com/react-native-community/discussions-and-proposals/discussions/893)
## Getting Started
1. Install React Native Boost:
```bash
npm install react-native-boost
```
```bash
pnpm add react-native-boost
```
```bash
yarn add react-native-boost
```
```bash
bun add react-native-boost
```
2. If you use Expo and do not have a `babel.config.js` yet:
```bash
npx expo customize babel.config.js
```
3. Add the plugin:
```js
// babel.config.js
module.exports = {
plugins: ['react-native-boost/plugin'],
};
```
4. Restart the development server and clear cache:
```bash
npm start -- --clear
```
```bash
pnpm start -- --clear
```
```bash
yarn start --clear
```
```bash
bun run start --clear
```
The Babel plugin imports optimized components via `react-native-boost/runtime`, so `react-native-boost` must be
available at runtime and can therefore **not** be installed as a dev dependency.
## Platform Support
React Native Boost supports iOS and Android projects. On Web, React Native Boost safely falls back to the default components, providing full cross-platform support.
================================================
FILE: apps/docs/content/docs/information/benchmarks.mdx
================================================
---
title: Benchmarks
description: Benchmark results from the repository example app.
---
We run benchmarks with the example app available in the repository to measure render-time improvements.
In recent runs, React Native Boost improved rendering performance on both iOS and Android, with gains up to ~50%
depending on component mix and screen structure.


The more `Text` and `View` components your UI renders (especially in lists), the more measurable the gains are likely
to be.
================================================
FILE: apps/docs/content/docs/information/how-it-works.mdx
================================================
---
title: How It Works
description: Understand why React Native Boost makes your app faster, and how the Babel optimizer decides when transformations are safe.
---
React Native components such as `Text` and `View` are JavaScript wrappers around their native implementations.
Those wrappers handle many edge cases, but they also add a considerable amount of runtime overhead.
React Native Boost analyzes your code at build time and replaces these components with their native equivalents fully automatically. The plugin performs static analysis on your code to determine when it's safe to optimize a component and when it's not.
Optimized components are imported from `react-native-boost/runtime`, not directly from `react-native`, which allows graceful fallback for web targets
and other non-native environments when native internals are unavailable.
## Static Analysis
For each candidate component, the plugin verifies conditions such as:
- Import source checks (for example, imported from `react-native`)
- Prop compatibility checks
- Ancestor and context safety checks
- Children/structure checks
If any safety requirement is not met, the component is left unchanged in order to avoid behavioral changes or other bugs.
================================================
FILE: apps/docs/content/docs/information/optimization-coverage.mdx
================================================
---
title: Optimization Coverage
description: What React Native Boost can optimize today, what it skips, and why.
---
React Native Boost is conservative by design. If it cannot prove an optimization is safe within the possibilities of a Babel plugin, it skips it. While this means it'll often skip optimizations that would be safe in practice, it also means that you can trust that optimizations that do happen are safe and won't cause behavioral changes or other regressions.
## At a Glance
| Component | Optimized when... | Common bailout reasons |
| --- | --- | --- |
| `Text` | Imported from `react-native`, no blacklisted props, string-safe children | `contains blacklisted props`, `contains non-string children`, `is a direct child of expo-router Link with asChild` |
| `View` | Imported from `react-native`, no blacklisted props, safe ancestor chain | `contains blacklisted props`, `has Text ancestor`, `has unresolved ancestor and dangerous optimization is disabled` |
## Global Bailouts
These skip optimization before component-specific checks:
- File path matches `ignores`
- Line is marked with `@boost-ignore`
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`, etc.)
- `selectionColor`
- `id`, `nativeID`
### Text structure checks
- Children must be string-safe.
- `Text` is skipped when used as a direct child of `expo-router` `Link` with `asChild`.
```tsx
import { Link } from 'expo-router';
import { Text } from 'react-native';
Open profile
;
```
## View Coverage
`View` has stricter safety checks because `View` inside text-like ancestors can break layout/semantics.
### View blacklisted props
If any of these are present, the `View` node is skipped:
- `style`
- Accessibility props (`accessible`, `accessibilityLabel`, `accessibilityState`, `aria-*`)
- `id`, `nativeID`
### 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.
## 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`.
================================================
FILE: apps/docs/content/docs/information/troubleshooting.mdx
================================================
---
title: Troubleshooting
description: Common setup and optimization issues, plus fast ways to diagnose them.
---
## Quick Diagnostic Flow
1. Set `verbose: true` and `silent: false` in plugin config.
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).
## Common Issues
### No optimization logs at all
Likely causes:
- Plugin not loaded in `babel.config.js`
- `silent: true`
- File matched by `ignores`
Quick checks:
```js
module.exports = {
plugins: [
[
'react-native-boost/plugin',
{
verbose: true,
silent: false,
},
],
],
};
```
```bash
npm start -- --clear
```
### Skip reason: `contains blacklisted props`
This is expected for unsupported prop sets.
Typical cases:
- `Text` with press/responder props
- `View` with `style` or accessibility props
Fix options:
- Keep component as-is (recommended when semantics matter)
- Move unsupported behavior to a different node when possible
- Use `@boost-ignore` for explicit clarity
### Skip reason: `has unresolved ancestor and dangerous optimization is disabled`
`View` is inside an ancestor React Native Boost cannot statically classify.
Options:
- Keep default behavior (safest)
- Refactor ancestor/component structure to be statically obvious
- Enable `dangerouslyOptimizeViewWithUnknownAncestors` only if you can validate behavior carefully
### Ignores do not work as expected in monorepos
`ignores` are resolved from Babel's working directory.
In nested apps, you may need explicit parent paths:
```js
ignores: ['../../node_modules/**'];
```
### Runtime import errors in app code
The plugin injects imports from `react-native-boost/runtime`.
If you installed `react-native-boost` as a dev dependency, runtime imports can fail in app builds.
Fix: install it as a regular dependency.
================================================
FILE: apps/docs/content/docs/meta.json
================================================
{
"title": "React Native Boost",
"description": "Documentation for React Native Boost",
"root": true,
"pages": [
"index",
"---Information---",
"information/how-it-works",
"information/optimization-coverage",
"information/troubleshooting",
"information/benchmarks",
"---Configuration---",
"configuration/configure",
"configuration/nativewind",
"configuration/boost-decorator",
"---Runtime Library---",
"runtime-library/index"
]
}
================================================
FILE: apps/docs/content/docs/runtime-library/index.mdx
================================================
---
title: Runtime Library
description: Runtime exports used by the Babel plugin and advanced integrations.
---
`react-native-boost/runtime` is used by the Babel plugin to apply optimizations safely across platforms.
Besides re-exporting optimized native components with web-safe fallbacks, it also exposes helper utilities.
Direct usage is supported but generally not recommended unless needed for advanced integrations (for example,
[Nativewind setup](/docs/configuration/nativewind)).
## API Reference
This section is automatically generated from runtime exports.
================================================
FILE: apps/docs/lib/cn.ts
================================================
export { twMerge as cn } from 'tailwind-merge';
================================================
FILE: apps/docs/lib/layout.shared.tsx
================================================
import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared';
export const gitConfig = {
user: 'kuatsu',
repo: 'react-native-boost',
branch: 'main',
};
export function baseOptions(): BaseLayoutProps {
return {
nav: {
title: 'React Native Boost',
},
githubUrl: `https://github.com/${gitConfig.user}/${gitConfig.repo}`,
};
}
================================================
FILE: apps/docs/lib/source.ts
================================================
import { docs } from 'fumadocs-mdx:collections/server';
import { type InferPageType, loader } from 'fumadocs-core/source';
import { lucideIconsPlugin } from 'fumadocs-core/source/lucide-icons';
// See https://fumadocs.dev/docs/headless/source-api for more info
export const source = loader({
baseUrl: '/docs',
source: docs.toFumadocsSource(),
plugins: [lucideIconsPlugin()],
});
export function getPageImage(page: InferPageType) {
const segments = [...page.slugs, 'image.png'];
return {
segments,
url: `/og/docs/${segments.join('/')}`,
};
}
export async function getLLMText(page: InferPageType) {
const processed = await page.data.getText('processed');
return `# ${page.data.title}
${processed}`;
}
================================================
FILE: apps/docs/lib/type-generator.ts
================================================
import 'server-only';
import { createFileSystemGeneratorCache, createGenerator, type Generator } from 'fumadocs-typescript';
export const typeGenerator: Generator = createGenerator({
cache: createFileSystemGeneratorCache('.next/fumadocs-typescript'),
});
================================================
FILE: apps/docs/mdx-components.tsx
================================================
import defaultMdxComponents from 'fumadocs-ui/mdx';
import { AutoTypeTable } from 'fumadocs-typescript/ui';
import type { MDXComponents } from 'mdx/types';
import { AutoOptionSections } from '@/components/docs/auto-option-sections';
import { AutoRuntimeReference } from '@/components/docs/auto-runtime-reference';
import { typeGenerator } from '@/lib/type-generator';
export function getMDXComponents(components?: MDXComponents): MDXComponents {
return {
...defaultMdxComponents,
AutoOptionSections,
AutoRuntimeReference,
AutoTypeTable: (props) => ,
...components,
};
}
================================================
FILE: apps/docs/next.config.mjs
================================================
import { createMDX } from 'fumadocs-mdx/next';
const withMDX = createMDX();
/** @type {import('next').NextConfig} */
const config = {
reactStrictMode: true,
async rewrites() {
return [
{
source: '/docs/:path*.mdx',
destination: '/llms.mdx/docs/:path*',
},
];
},
};
export default withMDX(config);
================================================
FILE: apps/docs/package.json
================================================
{
"name": "react-native-boost-docs",
"version": "0.0.0",
"private": true,
"scripts": {
"build": "next build",
"dev": "next dev",
"start": "next start",
"typecheck": "pnpm run types:check",
"types:check": "fumadocs-mdx && next typegen && tsc --noEmit",
"postinstall": "fumadocs-mdx"
},
"dependencies": {
"fumadocs-core": "16.6.7",
"fumadocs-mdx": "14.2.8",
"fumadocs-typescript": "^5.1.4",
"fumadocs-ui": "16.6.7",
"lucide-react": "^0.575.0",
"next": "16.1.6",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"ts-morph": "^27.0.2",
"tailwind-merge": "^3.5.0"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.2.1",
"@types/mdx": "^2.0.13",
"@types/node": "^25.3.1",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"postcss": "^8.5.6",
"tailwindcss": "^4.2.1",
"typescript": "^5.9.3"
}
}
================================================
FILE: apps/docs/postcss.config.mjs
================================================
const config = {
plugins: {
'@tailwindcss/postcss': {},
},
};
export default config;
================================================
FILE: apps/docs/source.config.ts
================================================
import { defineConfig, defineDocs } from 'fumadocs-mdx/config';
import { metaSchema, pageSchema } from 'fumadocs-core/source/schema';
// You can customise Zod schemas for frontmatter and `meta.json` here
// see https://fumadocs.dev/docs/mdx/collections
export const docs = defineDocs({
dir: 'content/docs',
docs: {
schema: pageSchema,
postprocess: {
includeProcessedMarkdown: true,
},
},
meta: {
schema: metaSchema,
},
});
export default defineConfig({
mdxOptions: {
// MDX options
},
});
================================================
FILE: apps/docs/tsconfig.json
================================================
{
"compilerOptions": {
"baseUrl": ".",
"target": "ESNext",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react-jsx",
"incremental": true,
"paths": {
"@/*": ["./*"],
"fumadocs-mdx:collections/*": [".source/*"]
},
"plugins": [
{
"name": "next"
}
]
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", ".next/dev/types/**/*.ts"],
"exclude": ["node_modules"]
}
================================================
FILE: apps/example/.gitignore
================================================
# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
# dependencies
node_modules/
# Expo
.expo/
dist/
web-build/
expo-env.d.ts
# Native
/ios
/android
*.orig.*
*.jks
*.p8
*.p12
*.key
*.mobileprovision
# Metro
.metro-health-check*
# debug
npm-debug.*
yarn-debug.*
yarn-error.*
# macOS
.DS_Store
*.pem
# local env files
.env*.local
# typescript
*.tsbuildinfo
================================================
FILE: apps/example/app.json
================================================
{
"expo": {
"name": "RN Boost",
"slug": "react-native-boost-example",
"version": "0.0.1",
"orientation": "portrait",
"icon": "./assets/icon.png",
"userInterfaceStyle": "light",
"newArchEnabled": true,
"splash": {
"image": "./assets/splash-icon.png",
"resizeMode": "contain",
"backgroundColor": "#ffffff"
},
"ios": {
"supportsTablet": true,
"bundleIdentifier": "com.kuatsu-mkrause.react-native-boost-example"
},
"android": {
"adaptiveIcon": {
"foregroundImage": "./assets/adaptive-icon.png",
"backgroundColor": "#ffffff"
},
"package": "com.kuatsumkrause.reactnativeboostexample"
},
"web": {
"favicon": "./assets/favicon.png"
}
}
}
================================================
FILE: apps/example/babel.config.js
================================================
module.exports = function (api) {
api.cache(true);
return {
presets: ['babel-preset-expo'],
plugins: [['react-native-boost/plugin', { ignores: ['node_modules/**', '../../node_modules/**'] }]],
};
};
================================================
FILE: apps/example/index.ts
================================================
import { registerRootComponent } from 'expo';
import App from './src/app';
// registerRootComponent calls AppRegistry.registerComponent('main', () => App);
// It also ensures that whether you load the app in Expo Go or in a native build,
// the environment is set up appropriately
registerRootComponent(App);
================================================
FILE: apps/example/package.json
================================================
{
"name": "react-native-boost-example",
"version": "0.0.1",
"main": "index.ts",
"scripts": {
"start": "expo start",
"dev": "expo start",
"android": "rm -rf android && expo run:android",
"ios": "rm -rf ios && expo run:ios",
"web": "expo start --web"
},
"dependencies": {
"@expo/metro-runtime": "~55.0.6",
"expo": "^55.0.3",
"expo-status-bar": "~55.0.4",
"react": "19.2.0",
"react-dom": "19.2.0",
"react-native": "0.83.2",
"react-native-safe-area-context": "~5.6.0",
"react-native-boost": "workspace:*",
"react-native-time-to-render": "workspace:*",
"react-native-web": "^0.21.2"
},
"devDependencies": {
"@babel/core": "^7.25.2",
"@types/react": "~19.2.14",
"typescript": "~5.9.3"
},
"private": true
}
================================================
FILE: apps/example/src/app.tsx
================================================
import { StatusBar } from 'expo-status-bar';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import HomeScreen from './screens/home';
export default function App() {
return (
);
}
================================================
FILE: apps/example/src/components/measure-component.tsx
================================================
import React from 'react';
import { TimeToRenderView } from 'react-native-time-to-render';
import { Benchmark, BenchmarkStep } from '../types';
import { View } from 'react-native';
export interface BenchmarkProperties extends Benchmark {
onRenderTimeChange: (renderTime: number) => void;
step: BenchmarkStep;
markerName: string;
}
export default function MeasureComponent(props: BenchmarkProperties) {
const optimizedViews = Array.from({ length: props.count }, (_, index) =>
React.cloneElement(props.optimizedComponent as React.ReactElement, { key: `optimized-${index}` })
);
const unoptimizedViews = Array.from({ length: props.count }, (_, index) =>
React.cloneElement(props.unoptimizedComponent as React.ReactElement, { key: `unoptimized-${index}` })
);
if (props.step === BenchmarkStep.Unoptimized) {
return (
<>
{
props.onRenderTimeChange(Math.round(event.nativeEvent.paintTime));
}}
/>
{unoptimizedViews}
>
);
}
return (
<>
{
props.onRenderTimeChange(Math.round(event.nativeEvent.paintTime));
}}
/>
{optimizedViews}
>
);
}
================================================
FILE: apps/example/src/screens/home.tsx
================================================
import { Pressable, StyleSheet, Text, View } from 'react-native';
import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
import { useMemo, useState } from 'react';
import { startMarker } from 'react-native-time-to-render';
import { Benchmark, BenchmarkStep } from '../types';
import MeasureComponent from '../components/measure-component';
import { getMarkerName } from '../utils/helpers';
const benchmarks = [
{
title: 'Text',
count: 10_000,
// @boost-ignore
unoptimizedComponent: Nice text ,
optimizedComponent: Nice text ,
},
{
title: 'View',
count: 10_000,
// @boost-ignore
unoptimizedComponent: ,
optimizedComponent: ,
},
] satisfies Benchmark[];
export default function HomeScreen() {
const insets = useSafeAreaInsets();
const [currentStepIndex, setCurrentStepIndex] = useState(0);
const [runBenchmark, setRunBenchmark] = useState(false);
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 resultRows = useMemo(() => {
return benchmarks.map((benchmark, index) => {
const value = results[index];
const unoptimized = value?.unoptimized ?? null;
const optimized = value?.optimized ?? null;
const gainPercent =
unoptimized === null || optimized === null || unoptimized === 0 ? null : (1 - optimized / unoptimized) * 100;
const gain = gainPercent === null ? 'N/A' : `${gainPercent.toFixed(2)}%`;
return {
title: benchmark.title,
unoptimizedText: unoptimized === null ? '--' : `${unoptimized}ms`,
optimizedText: optimized === null ? '--' : `${optimized}ms`,
gain,
gainPercent,
};
});
}, [results]);
const handleRun = (timestamp: number) => {
startMarker(markerName, timestamp);
setRunBenchmark(true);
};
const handleRenderTimeChange = (renderTime: number) => {
setRunBenchmark(false);
setResults((previousResults) => {
const baseResults = currentStepIndex === 0 ? {} : previousResults;
const previousBenchmarkResult = baseResults[currentBenchmark] ?? { unoptimized: null, optimized: null };
return {
...baseResults,
[currentBenchmark]:
currentStep === BenchmarkStep.Unoptimized
? { unoptimized: renderTime, optimized: null }
: { ...previousBenchmarkResult, optimized: renderTime },
};
});
setCurrentStepIndex((previousStepIndex) => (previousStepIndex + 1) % totalSteps);
};
return (
React Native Boost Benchmark
{`Step ${progress[0] + 1} / ${progress[1]}: ${benchmarks[currentBenchmark].title} (${currentStep})`}
Test
Unopt.
Opt.
Gain
{resultRows.map((row, index) => (
{row.title}
{row.unoptimizedText}
{row.optimizedText}
= 0
? styles.gainPositive
: styles.gainNegative,
]}>
{row.gain}
))}
handleRun(event.nativeEvent.timestamp)}
style={({ pressed }) => [styles.runButton, pressed && styles.runButtonPressed]}>
{buttonTitle}
{runBenchmark && (
)}
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff8ef',
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: 16,
},
content: {
width: '100%',
maxWidth: 640,
},
headerCard: {
backgroundColor: '#ffffff',
borderRadius: 14,
borderWidth: 1,
borderColor: '#f1d4a3',
paddingHorizontal: 16,
paddingVertical: 14,
marginBottom: 12,
},
title: {
fontSize: 20,
fontWeight: '700',
color: '#5e3c0c',
},
subtitle: {
marginTop: 4,
fontSize: 13,
color: '#7a4b00',
fontWeight: '600',
textTransform: 'capitalize',
},
runButton: {
backgroundColor: '#ff9800',
borderRadius: 12,
paddingVertical: 12,
alignItems: 'center',
width: '100%',
maxWidth: 640,
},
runButtonPressed: {
transform: [{ scale: 0.985 }],
opacity: 0.95,
},
runButtonText: {
color: '#2f1d00',
fontSize: 15,
fontWeight: '700',
},
tableCard: {
borderRadius: 14,
overflow: 'hidden',
borderWidth: 1,
borderColor: '#f1d4a3',
backgroundColor: '#ffffff',
},
footer: {
position: 'absolute',
left: 16,
right: 16,
alignItems: 'center',
},
tableRow: {
flexDirection: 'row',
alignItems: 'center',
minHeight: 42,
},
tableHeader: {
backgroundColor: '#fff0d6',
borderBottomWidth: 1,
borderBottomColor: '#f1d4a3',
},
tableHeaderText: {
fontSize: 12,
fontWeight: '700',
color: '#7a4b00',
},
tableStripeLight: {
backgroundColor: '#ffffff',
},
tableStripeDark: {
backgroundColor: '#fffaf2',
},
tableActiveRow: {
backgroundColor: '#ffe8c2',
},
tableCell: {
paddingHorizontal: 10,
paddingVertical: 8,
},
benchmarkColumn: {
flex: 1.4,
},
metricColumn: {
flex: 1,
alignItems: 'flex-end',
},
benchmarkText: {
fontSize: 14,
color: '#4d3311',
fontWeight: '600',
},
metricText: {
fontSize: 13,
color: '#6e4c1d',
textAlign: 'right',
},
gainPositive: {
color: '#0d7a3b',
fontWeight: '700',
},
gainNegative: {
color: '#b42318',
fontWeight: '700',
},
gainNeutral: {
color: '#6e4c1d',
},
});
================================================
FILE: apps/example/src/types/index.ts
================================================
export interface Benchmark {
title: string;
count: number;
optimizedComponent: React.ReactNode;
unoptimizedComponent: React.ReactNode;
}
export enum BenchmarkStep {
Unoptimized = 'unoptimized',
Optimized = 'optimized',
}
================================================
FILE: apps/example/src/utils/helpers.ts
================================================
import { BenchmarkStep } from '../types';
export const getMarkerName = (title: string, step: BenchmarkStep) => `${title}-${step}`;
================================================
FILE: apps/example/tsconfig.json
================================================
{
"extends": "expo/tsconfig.base",
"compilerOptions": {
"strict": true
}
}
================================================
FILE: commitlint.config.mjs
================================================
export default { extends: ['@commitlint/config-conventional'] };
================================================
FILE: package.json
================================================
{
"name": "react-native-boost-monorepo",
"version": "0.0.0",
"private": true,
"scripts": {
"build": "pnpm -r --parallel --if-present run build",
"typecheck": "pnpm -r --parallel --if-present run typecheck",
"test": "pnpm -r --parallel --if-present run test",
"example": "pnpm --filter react-native-boost-example run",
"dev": "pnpm --parallel --stream --filter react-native-boost --filter react-native-boost-example run dev",
"docs": "pnpm --filter react-native-boost-docs run",
"package": "pnpm --filter react-native-boost run",
"lint": "oxlint .",
"format": "oxfmt .",
"prepare": "husky"
},
"packageManager": "pnpm@10.28.2",
"devDependencies": {
"@commitlint/cli": "^20.4.2",
"@commitlint/config-conventional": "^20.4.2",
"husky": "^9.1.7",
"lint-staged": "^16.2.7",
"oxfmt": "^0.35.0",
"oxlint": "^1.50.0",
"typescript": "~5.9.3"
}
}
================================================
FILE: packages/react-native-boost/.gitignore
================================================
/dist
/plugin.*
/runtime.*
================================================
FILE: packages/react-native-boost/CHANGELOG.md
================================================
# Changelog
# [1.1.0](https://github.com/kuatsu/react-native-boost/compare/v1.0.0...v1.1.0) (2026-03-18)
### Features
* add `[@boost-force](https://github.com/boost-force)` decorator to enforce optimization ([eb7357c](https://github.com/kuatsu/react-native-boost/commit/eb7357c1fc296aa62658dcc010254c6f1b08e986))
# [1.0.0](https://github.com/kuatsu/react-native-boost/compare/v0.6.2...v1.0.0) (2026-02-27)
### Bug Fixes
* **example:** fix benchmark state machine ([48bb689](https://github.com/kuatsu/react-native-boost/commit/48bb6891d27c6cd7971a45f36d8eda9278b4a54f))
* **example:** fix node_modules being optimized ([9558680](https://github.com/kuatsu/react-native-boost/commit/9558680b6ce0221a367ef64f47cc0ca4dc1bbb67))
* **view:** bail on unknown ancestors by default and add opt-in flag ([#17](https://github.com/kuatsu/react-native-boost/issues/17)) ([bf24fbd](https://github.com/kuatsu/react-native-boost/commit/bf24fbd3d241b0a38d91c5b2ff736c610b7e4132))
### Features
* bail on less props ([1bb7758](https://github.com/kuatsu/react-native-boost/commit/1bb77587185fd6ab5d8d7cb238b5c4dea46553ff))
* **example:** prettier benchmark app ([51e47fd](https://github.com/kuatsu/react-native-boost/commit/51e47fd321507e9d461fd38951cbfaf058ececf2))
* improve logging ([3c9fef4](https://github.com/kuatsu/react-native-boost/commit/3c9fef482c7f20056779cc02bf6e2ea010c59f69))
* skip optimizing Text inside Expo Router Link with `asChild` ([#16](https://github.com/kuatsu/react-native-boost/issues/16)) ([ddc606d](https://github.com/kuatsu/react-native-boost/commit/ddc606d780863b199ab9bf3e6079fb00a366e066))
* **text:** bail less, statically handle userSelect, improve spread prop ([5e93ae5](https://github.com/kuatsu/react-native-boost/commit/5e93ae545d63b9258f27f6831adfa6bba899a124))
## [0.6.2](https://github.com/kuatsu/react-native-boost/compare/v0.6.1...v0.6.2) (2025-06-11)
### Bug Fixes
* fixed react-native 0.79 style flattening ([7b7a5a4](https://github.com/kuatsu/react-native-boost/commit/7b7a5a4def44676382014dfc26f9df1bc989f81e)), closes [#10](https://github.com/kuatsu/react-native-boost/issues/10)
### Reverts
* remove unused package export ([973084e](https://github.com/kuatsu/react-native-boost/commit/973084ee58f9473ffff23a62051d6d99da641456))
## [0.6.1](https://github.com/kuatsu/react-native-boost/compare/v0.6.0...v0.6.1) (2025-06-11)
### Bug Fixes
* fixed type errors in tests ([59e3925](https://github.com/kuatsu/react-native-boost/commit/59e392562331e7e150caad431adc5569f9b7a88d))
# [0.6.0](https://github.com/kuatsu/react-native-boost/compare/v0.5.7...v0.6.0) (2025-06-11)
### Bug Fixes
* fixed bundling issues on web ([efd9933](https://github.com/kuatsu/react-native-boost/commit/efd9933604a6053d5b64cb9faec714c0e3987410))
## [0.5.7](https://github.com/kuatsu/react-native-boost/compare/v0.5.6...v0.5.7) (2025-06-11)
## [0.5.6](https://github.com/kuatsu/react-native-boost/compare/v0.5.5...v0.5.6) (2025-02-26)
## [0.5.5](https://github.com/kuatsu/react-native-boost/compare/v0.5.4...v0.5.5) (2025-02-25)
### Bug Fixes
* don't optimize views with indirect text ancestor (custom component) ([a8d5ced](https://github.com/kuatsu/react-native-boost/commit/a8d5ced7ee4047e9094571257389a2680bd6214e))
## [0.5.4](https://github.com/kuatsu/react-native-boost/compare/v0.5.3...v0.5.4) (2025-02-25)
### Bug Fixes
* fixed optimizing aliased imports ([2eee88b](https://github.com/kuatsu/react-native-boost/commit/2eee88bce014744a13f75ea08d386fba61c5be7c))
## [0.5.3](https://github.com/kuatsu/react-native-boost/compare/v0.5.2...v0.5.3) (2025-02-24)
### Bug Fixes
* fixed react-native-web bundling ([56ab1b3](https://github.com/kuatsu/react-native-boost/commit/56ab1b3b0985f413691edd1ce3de9a02593f7ff8))
## [0.5.2](https://github.com/kuatsu/react-native-boost/compare/v0.5.1...v0.5.2) (2025-02-24)
### Bug Fixes
* allow text string children from variables ([622a201](https://github.com/kuatsu/react-native-boost/commit/622a2011f751f9f28cdda19bf7ea676f537b1fbf))
## [0.5.1](https://github.com/kuatsu/react-native-boost/compare/v0.5.0...v0.5.1) (2025-02-24)
### Bug Fixes
* fixed text style flattening ([83ef501](https://github.com/kuatsu/react-native-boost/commit/83ef501aeaa30c7dc8a59a78e74c7a700fc4b4a3))
# [0.5.0](https://github.com/kuatsu/react-native-boost/compare/v0.4.1...v0.5.0) (2025-02-24)
### Features
* **example:** allow running benchmarks in production ([c21ada0](https://github.com/kuatsu/react-native-boost/commit/c21ada06e1d7ae79fb77512b272da79a15f9fa32))
* optimize components with accessibility props ([d71d027](https://github.com/kuatsu/react-native-boost/commit/d71d027ec613b8baa96e22f155cec317e4c54e13))
## [0.4.1](https://github.com/kuatsu/react-native-boost/compare/v0.4.0...v0.4.1) (2025-02-24)
# [0.4.0](https://github.com/kuatsu/react-native-boost/compare/v0.3.0...v0.4.0) (2025-02-24)
### Bug Fixes
* **docs:** fixed broken link ([a3dde5d](https://github.com/kuatsu/react-native-boost/commit/a3dde5d3c400d525028e18f4cdbcf88fcc373029))
### Features
* added ` ` optimization support ([46ca834](https://github.com/kuatsu/react-native-boost/commit/46ca834b9d62f5a3abfca7061993f82cdae48deb))
* added `ignores` config option ([7d58a9f](https://github.com/kuatsu/react-native-boost/commit/7d58a9f8db759d3babf747504645b9a4d6ee61bd))
* **docs:** added documentation app w/ styled homepage ([9ce312b](https://github.com/kuatsu/react-native-boost/commit/9ce312b6b6dae38a9ccd3574e72806515a86fa21))
# [0.3.0](https://github.com/kuatsu/react-native-boost/compare/v0.2.0...v0.3.0) (2025-02-24)
### Features
* fix `numberOfLines` prop at build time ([58c2993](https://github.com/kuatsu/react-native-boost/commit/58c299393abaf3a9fcbb2ca933cfa02e4bf08fb3))
### Reverts
* **example:** removed console.log ([4facb6e](https://github.com/kuatsu/react-native-boost/commit/4facb6ed5c773e9b2fef28779d288b43b56612dc))
* removed wrong rollup config ([b4b69c0](https://github.com/kuatsu/react-native-boost/commit/b4b69c01c90a5e11659569a22c5c23805f9df753))
# [0.2.0](https://github.com/kuatsu/react-native-boost/compare/v0.1.0...v0.2.0) (2025-02-23)
### Features
* **example:** added benchmark ([6e56b2a](https://github.com/kuatsu/react-native-boost/commit/6e56b2aaa5c9510d8be0a4898e86382ee637b0c3))
* improved logging & [@boost-ignore](https://github.com/boost-ignore) decorator handling ([6f11cbb](https://github.com/kuatsu/react-native-boost/commit/6f11cbb5b1480b10cd20d2544fa334da1474f44b))
# [0.1.0](https://github.com/kuatsu/react-native-boost/compare/v0.0.5...v0.1.0) (2025-02-23)
### Bug Fixes
* fixed `main` entry ([2727e69](https://github.com/kuatsu/react-native-boost/commit/2727e6965e2d6f7d5fbe308bf5ff5d4c63b8c06d))
### Features
* added ignore decorator comment ([3f7d0dc](https://github.com/kuatsu/react-native-boost/commit/3f7d0dc4a67623fee41f473ca588d6901c5b3e97))
* allow text style prop ([e916da5](https://github.com/kuatsu/react-native-boost/commit/e916da5f6bfee0d5480b660fab70c6e0a67deace))
## [0.0.5](https://github.com/kuatsu/react-native-boost/compare/v0.0.4...v0.0.5) (2025-02-23)
### Features
* minify bundle ([8fd6687](https://github.com/kuatsu/react-native-boost/commit/8fd66878599af4313d428687557bac22a832fd78))
## [0.0.4](https://github.com/kuatsu/react-native-boost/compare/v0.0.3...v0.0.4) (2025-02-23)
### Bug Fixes
* re-add README to npm ([4f39ab5](https://github.com/kuatsu/react-native-boost/commit/4f39ab5162ab412a330aa60f0efa63604f94ec23))
## 0.0.3 (2025-02-23)
### Bug Fixes
* added prettier ([fb73927](https://github.com/kuatsu/react-native-boost/commit/fb73927f2ca613709a2eb181903f52e39903159a))
* **ci:** fixed release workflow ([42d1ce1](https://github.com/kuatsu/react-native-boost/commit/42d1ce1a0691831178a7ef2db78d0258ea4826b3))
* fixed husky hooks ([d0540c9](https://github.com/kuatsu/react-native-boost/commit/d0540c94007e9f13ecd70a22b572084afe58ee0d))
* fixed prettier ([b78e6b4](https://github.com/kuatsu/react-native-boost/commit/b78e6b4c47d0321fa2fa303d5197763aadd4f272))
* fixed tsconfig ([29abcfc](https://github.com/kuatsu/react-native-boost/commit/29abcfcb48b8194d34bdf34af2db4a85fa6a15c5))
* package resolution fix ([bb83508](https://github.com/kuatsu/react-native-boost/commit/bb8350860f2ac952e9fd00702c55357fef013438))
* try to fix lockfile ([16bf4e4](https://github.com/kuatsu/react-native-boost/commit/16bf4e4d9a7bd00c28897ab6ef74377ad307cc00))
### Features
* **example:** added android implementation of benchmarking module ([1c5c4fb](https://github.com/kuatsu/react-native-boost/commit/1c5c4fb2d7165375dffa52f8b6ab0a338e7cdaf1))
* **example:** initialized example app ([19608a9](https://github.com/kuatsu/react-native-boost/commit/19608a94f4e45cf39c13901e472f46181a95115b))
* **example:** ios implementation of benchmarking turbo module ([cd54789](https://github.com/kuatsu/react-native-boost/commit/cd547896b58046a34499b9045c407d4dcf6a5434))
* **example:** scaffolded new turbo module for benchmarks ([187650e](https://github.com/kuatsu/react-native-boost/commit/187650e2d5dc0f4e77520568c4da15c6cd4d602f))
* improved plugin import ([0e97f1e](https://github.com/kuatsu/react-native-boost/commit/0e97f1eea615a2516066fa6a94c9b3685e6576ae))
* initial commit ([3d3c0ad](https://github.com/kuatsu/react-native-boost/commit/3d3c0adcdcc35e3f641312f89292ee72b52142dc))
* optional logging ([c4ad283](https://github.com/kuatsu/react-native-boost/commit/c4ad283db3e7af3f116ba66c90897f2f94362f97))
* try to resolve spread attribute ([2aeb5a1](https://github.com/kuatsu/react-native-boost/commit/2aeb5a1d92f4600f87f6d638ae34db804640ae22))
================================================
FILE: packages/react-native-boost/LICENSE
================================================
MIT License
Copyright (c) Kuatsu App Agency
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: packages/react-native-boost/package.json
================================================
{
"name": "react-native-boost",
"description": "🚀 Boost your React Native app's performance with a single line of code",
"version": "1.1.0",
"main": "dist/index.js",
"module": "dist/esm/index.mjs",
"types": "dist/index.d.ts",
"exports": {
"./package.json": "./package.json",
"./runtime": {
"import": {
"types": "./dist/runtime/index.d.ts",
"default": "./runtime.mjs"
},
"default": "./runtime.js"
},
"./plugin": {
"import": {
"types": "./dist/plugin/index.d.ts",
"default": "./plugin.mjs"
},
"default": "./plugin.js"
}
},
"typesVersions": {
"*": {
"runtime": [
"dist/runtime/index.d.ts"
],
"plugin": [
"dist/plugin/index.d.ts"
]
}
},
"keywords": [
"react-native",
"ios",
"android",
"performance",
"optimization",
"bundle",
"optimize"
],
"scripts": {
"clean": "rm -rf dist",
"build": "pnpm clean && rollup -c",
"build:watch": "rollup -c -w",
"dev": "pnpm build:watch",
"test": "vitest",
"typecheck": "tsc --noEmit",
"release": "release-it",
"prepack": "cp ../../README.md ./README.md",
"postpack": "rm ./README.md"
},
"files": [
"src",
"dist",
"runtime.d.ts",
"runtime.js",
"runtime.mjs",
"plugin.d.ts",
"plugin.js",
"plugin.mjs",
"!**/__tests__",
"!**/__fixtures__",
"!**/__mocks__",
"!**/.*"
],
"repository": {
"type": "git",
"url": "git+https://github.com/kuatsu/react-native-boost.git"
},
"author": "Kuatsu App Agency ",
"license": "MIT",
"bugs": {
"url": "https://github.com/kuatsu/react-native-boost/issues"
},
"homepage": "https://github.com/kuatsu/react-native-boost#readme",
"packageManager": "pnpm@10.28.2",
"publishConfig": {
"registry": "https://registry.npmjs.org"
},
"dependencies": {
"@babel/core": "^7.25.0",
"@babel/helper-module-imports": "^7.25.0",
"@babel/helper-plugin-utils": "^7.25.0",
"minimatch": "^10.0.1"
},
"devDependencies": {
"@babel/plugin-syntax-jsx": "^7.25.0",
"@babel/preset-typescript": "^7.25.0",
"@release-it/conventional-changelog": "^10.0.0",
"@rollup/plugin-alias": "^6.0.0",
"@rollup/plugin-node-resolve": "^16.0.0",
"@rollup/plugin-replace": "^6.0.2",
"@rollup/plugin-typescript": "^12.1.2",
"@types/babel__helper-module-imports": "^7.0.0",
"@types/babel__helper-plugin-utils": "^7.0.0",
"@types/node": "^24",
"babel-plugin-tester": "^12.0.0",
"esbuild-node-externals": "^1.18.0",
"react-native": "0.83.2",
"release-it": "^19.2.4",
"rollup": "^4.34.8",
"rollup-plugin-dts": "^6.1.1",
"rollup-plugin-esbuild": "^6.2.0",
"typescript": "~5.9.3",
"vitest": "^4.0.18"
},
"peerDependencies": {
"react": "*",
"react-native": ">=0.83.0"
},
"release-it": {
"git": {
"commitMessage": "chore: release ${version}",
"tagName": "v${version}"
},
"npm": {
"publish": true,
"skipChecks": true,
"versionArgs": [
"--workspaces-update=false"
]
},
"github": {
"release": true
},
"plugins": {
"@release-it/conventional-changelog": {
"preset": {
"name": "angular"
},
"infile": "CHANGELOG.md"
}
}
}
}
================================================
FILE: packages/react-native-boost/rollup.config.mjs
================================================
import path from 'path';
import resolve from '@rollup/plugin-node-resolve';
import replace from '@rollup/plugin-replace';
import esbuild from 'rollup-plugin-esbuild';
import dts from 'rollup-plugin-dts';
import fs from 'fs/promises';
const extensions = ['.js', '.ts', '.tsx'];
// Treat all non-relative and non-absolute imports as external
const external = (id) => !id.startsWith('.') && !path.isAbsolute(id);
// Custom plugin to generate entry point files
function generateEntryPoints() {
return {
name: 'generate-entry-points',
writeBundle: async () => {
// Define entry point configurations
const entryPoints = [
{
name: 'runtime',
description: 'runtime',
paths: {
cjs: './dist/runtime/index',
esm: './dist/runtime/esm/index.mjs',
dts: './dist/runtime/index',
},
},
{
name: 'plugin',
description: 'plugin',
paths: {
cjs: './dist/plugin/index',
esm: './dist/plugin/esm/index.mjs',
dts: './dist/plugin/index',
},
},
];
// Helper function to create entry point files
const createEntryPoint = async (config, format) => {
const { name, description, paths } = config;
switch (format) {
case 'cjs':
await fs.writeFile(`${name}.js`, `module.exports = require('${paths.cjs}');\n`);
break;
case 'esm':
await fs.writeFile(`${name}.mjs`, `export * from '${paths.esm}';\n`);
break;
case 'dts':
await fs.writeFile(`${name}.d.ts`, `export * from '${paths.dts}';\n`);
break;
}
};
// Generate all entry points
for (const config of entryPoints) {
await createEntryPoint(config, 'cjs');
await createEntryPoint(config, 'esm');
await createEntryPoint(config, 'dts');
}
},
};
}
const commonPlugins = [
resolve({ extensions }),
replace({
'preventAssignment': true,
'import.meta.env.MODE': JSON.stringify(process.env.NODE_ENV || 'development'),
}),
esbuild({
target: 'es2018',
tsconfig: 'tsconfig.json',
}),
];
// Add the entry point generator to the last build step
const lastBuildPlugins = [...commonPlugins, generateEntryPoints()];
export default [
// Runtime Code Build (CommonJS and ESM)
{
input: 'src/runtime/index.ts',
external,
plugins: commonPlugins,
output: [
{ file: 'dist/runtime/index.js', format: 'cjs', sourcemap: true },
{ file: 'dist/runtime/esm/index.mjs', format: 'esm', sourcemap: true },
],
},
{
input: 'src/runtime/index.web.ts',
external,
plugins: commonPlugins,
output: [
{ file: 'dist/runtime/index.web.js', format: 'cjs', sourcemap: true },
{ file: 'dist/runtime/esm/index.web.mjs', format: 'esm', sourcemap: true },
],
},
// Plugin Code Build (CommonJS and ESM)
{
input: 'src/plugin/index.ts',
external,
plugins: commonPlugins,
output: [
{ file: 'dist/plugin/index.js', format: 'cjs', sourcemap: true },
{ file: 'dist/plugin/esm/index.mjs', format: 'esm', sourcemap: true },
],
},
// Runtime Type Declarations Bundle (creates a single file)
{
input: 'src/runtime/index.ts',
plugins: [dts()],
external,
output: { file: 'dist/runtime/index.d.ts', format: 'esm' },
},
{
input: 'src/runtime/index.web.ts',
plugins: [dts()],
external,
output: { file: 'dist/runtime/index.web.d.ts', format: 'esm' },
},
// Plugin Type Declarations Bundle (creates a single file)
{
input: 'src/plugin/index.ts',
plugins: [dts(), generateEntryPoints()],
external,
output: { file: 'dist/plugin/index.d.ts', format: 'esm' },
},
];
================================================
FILE: packages/react-native-boost/src/plugin/index.ts
================================================
import { declare } from '@babel/helper-plugin-utils';
import { textOptimizer } from './optimizers/text';
import { PluginLogger, PluginOptions } from './types';
import { createLogger } from './utils/logger';
import { viewOptimizer } from './optimizers/view';
import { isIgnoredFile } from './utils/common';
export type { PluginOptimizationOptions, PluginOptions } from './types';
type PluginState = {
opts?: PluginOptions;
__reactNativeBoostLogger?: PluginLogger;
};
export default declare((api) => {
api.assertVersion(7);
return {
name: 'react-native-boost',
visitor: {
JSXOpeningElement(path, state) {
const pluginState = state as PluginState;
const options = (pluginState.opts ?? {}) as PluginOptions;
const logger = getOrCreateLogger(pluginState, options);
if (isIgnoredFile(path, options.ignores ?? [])) return;
if (options.optimizations?.text !== false) textOptimizer(path, logger);
if (options.optimizations?.view !== false) viewOptimizer(path, logger, options);
},
},
};
});
function getOrCreateLogger(state: PluginState, options: PluginOptions): PluginLogger {
if (state.__reactNativeBoostLogger) {
return state.__reactNativeBoostLogger;
}
state.__reactNativeBoostLogger = createLogger({
verbose: options.verbose === true,
silent: options.silent === true,
});
return state.__reactNativeBoostLogger;
}
================================================
FILE: packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/basic/code.js
================================================
import { Text } from 'react-native';
;
================================================
FILE: packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/basic/output.js
================================================
import { NativeText as _NativeText } from 'react-native-boost/runtime';
import { Text } from 'react-native';
<_NativeText allowFontScaling={true} ellipsizeMode={'tail'} />;
================================================
FILE: packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/complex-example/code.js
================================================
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}
);
}
================================================
FILE: packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/complex-example/output.js
================================================
import { NativeText as _NativeText } from 'react-native-boost/runtime';
import { Text } from 'react-native';
export default function TextBenchmark(props) {
const optimizedViews = Array.from(
{
length: props.count,
},
(_, index) => (
<_NativeText key={index} allowFontScaling={true} ellipsizeMode={'tail'}>
Nice text
)
);
const unoptimizedViews = Array.from(
{
length: props.count,
},
(_, index) => (
// @boost-ignore
Nice text
)
);
if (props.status === 'pending')
return (
<_NativeText allowFontScaling={true} ellipsizeMode={'tail'}>
Pending...
);
return (
{optimizedViews}
{unoptimizedViews}
);
}
================================================
FILE: packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/default-props/code.js
================================================
import { Text } from 'react-native';
const someFunction = () => ({});
Hello ;
No Scaling ;
const unknownProps = someFunction();
Unknown ;
const partialProps = { color: 'blue', ellipsizeMode: 'clip' };
Partial props ;
================================================
FILE: packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/default-props/output.js
================================================
import { NativeText as _NativeText } from 'react-native-boost/runtime';
import { Text } from 'react-native';
const someFunction = () => ({});
<_NativeText allowFontScaling={true} ellipsizeMode={'tail'}>
Hello
;
<_NativeText allowFontScaling={false} ellipsizeMode={'tail'}>
No Scaling
;
const unknownProps = someFunction();
Unknown ;
const partialProps = {
color: 'blue',
ellipsizeMode: 'clip',
};
<_NativeText {...partialProps} allowFontScaling={true}>
Partial props
;
================================================
FILE: packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/expo-router-link-alias-as-child/code.js
================================================
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
This should be optimized (aliased Link without asChild)
>;
================================================
FILE: packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/expo-router-link-alias-as-child/output.js
================================================
import { NativeText as _NativeText } from 'react-native-boost/runtime';
import { Text } from 'react-native';
import { Link as RouterLink } from 'expo-router';
<>
<_NativeText allowFontScaling={true} ellipsizeMode={'tail'}>
This should be optimized
This should NOT be optimized due to aliased Link asChild
<_NativeText allowFontScaling={true} ellipsizeMode={'tail'}>
This should be optimized (aliased Link without asChild)
>;
================================================
FILE: packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/expo-router-link-as-child/code.js
================================================
import { Text } from 'react-native';
import { Link } from 'expo-router';
<>
This should be optimized
This should NOT be optimized due to Link asChild
This should be optimized (Link without asChild)
This should NOT be optimized (Link with href and asChild)
>;
================================================
FILE: packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/expo-router-link-as-child/output.js
================================================
import { NativeText as _NativeText } from 'react-native-boost/runtime';
import { Text } from 'react-native';
import { Link } from 'expo-router';
<>
<_NativeText allowFontScaling={true} ellipsizeMode={'tail'}>
This should be optimized
This should NOT be optimized due to Link asChild
<_NativeText allowFontScaling={true} ellipsizeMode={'tail'}>
This should be optimized (Link without asChild)
This should NOT be optimized (Link with href and asChild)
>;
================================================
FILE: packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/expo-router-link-as-child-false-static/code.js
================================================
import { Text } from 'react-native';
import { Link } from 'expo-router';
<>
This should be optimized because asChild is false
This should NOT be optimized because asChild is true
>;
================================================
FILE: packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/expo-router-link-as-child-false-static/output.js
================================================
import { NativeText as _NativeText } from 'react-native-boost/runtime';
import { Text } from 'react-native';
import { Link } from 'expo-router';
<>
<_NativeText allowFontScaling={true} ellipsizeMode={'tail'}>
This should be optimized because asChild is false
This should NOT be optimized because asChild is true
>;
================================================
FILE: packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/expo-router-link-as-child-nested-view/code.js
================================================
import { Text, View } from 'react-native';
import { Link } from 'expo-router';
This should be optimized because View is the direct child
;
================================================
FILE: packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/expo-router-link-as-child-nested-view/output.js
================================================
import { NativeText as _NativeText } from 'react-native-boost/runtime';
import { Text, View } from 'react-native';
import { Link } from 'expo-router';
<_NativeText allowFontScaling={true} ellipsizeMode={'tail'}>
This should be optimized because View is the direct child
;
================================================
FILE: packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/expo-router-link-namespace-as-child/code.js
================================================
import { Text } from 'react-native';
import * as ExpoRouter from 'expo-router';
<>
This should NOT be optimized for namespace Link asChild
This should be optimized without asChild
>;
================================================
FILE: packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/expo-router-link-namespace-as-child/output.js
================================================
import { NativeText as _NativeText } from 'react-native-boost/runtime';
import { Text } from 'react-native';
import * as ExpoRouter from 'expo-router';
<>
This should NOT be optimized for namespace Link asChild
<_NativeText allowFontScaling={true} ellipsizeMode={'tail'}>
This should be optimized without asChild
>;
================================================
FILE: packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/flattens-styles-at-runtime/code.js
================================================
import { Text } from 'react-native';
;
;
================================================
FILE: packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/flattens-styles-at-runtime/output.js
================================================
import { processTextStyle as _processTextStyle, NativeText as _NativeText } from 'react-native-boost/runtime';
import { Text } from 'react-native';
<_NativeText
{..._processTextStyle({
color: 'red',
})}
allowFontScaling={true}
ellipsizeMode={'tail'}
/>;
<_NativeText
{..._processTextStyle([
{
color: 'red',
},
{
fontSize: 16,
},
])}
selectable={true}
allowFontScaling={true}
ellipsizeMode={'tail'}
/>;
================================================
FILE: packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/force-comment/code.js
================================================
import { Text } from 'react-native';
<>
{
console.log('pressed');
}}>
Normally skipped due to blacklisted prop
{/* @boost-force */}
{
console.log('pressed');
}}>
Force optimized despite blacklisted prop
>;
================================================
FILE: packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/force-comment/output.js
================================================
import { NativeText as _NativeText } from 'react-native-boost/runtime';
import { Text } from 'react-native';
<>
{
console.log('pressed');
}}>
Normally skipped due to blacklisted prop
{/* @boost-force */}
<_NativeText
onPress={() => {
console.log('pressed');
}}
allowFontScaling={true}
ellipsizeMode={'tail'}>
Force optimized despite blacklisted prop
>;
================================================
FILE: packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/ignore-comment/code.js
================================================
import { Text } from 'react-native';
<>
Optimize this
{/* @boost-ignore */}
But don't optimize this
>;
================================================
FILE: packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/ignore-comment/output.js
================================================
import { NativeText as _NativeText } from 'react-native-boost/runtime';
import { Text } from 'react-native';
<>
<_NativeText allowFontScaling={true} ellipsizeMode={'tail'}>
Optimize this
{/* @boost-ignore */}
But don't optimize this
>;
================================================
FILE: packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/mixed-children-types/code.js
================================================
import { Text } from 'react-native';
const name = 'John';
Hello {name}! ;
Click here:
;
================================================
FILE: packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/mixed-children-types/output.js
================================================
import { NativeText as _NativeText } from 'react-native-boost/runtime';
import { Text } from 'react-native';
const name = 'John';
<_NativeText allowFontScaling={true} ellipsizeMode={'tail'}>
Hello {name}!
;
Click here:
;
================================================
FILE: packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/nested-in-object-with-ignore-comment/code.js
================================================
import { Text } from 'react-native';
const benchmarks = [
{
title: 'Text',
count: 10_000,
optimizedComponent: Nice text ,
// @boost-ignore
unoptimizedComponent: Nice text ,
},
];
================================================
FILE: packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/nested-in-object-with-ignore-comment/output.js
================================================
import { NativeText as _NativeText } from 'react-native-boost/runtime';
import { Text } from 'react-native';
const benchmarks = [
{
title: 'Text',
count: 10_000,
optimizedComponent: (
<_NativeText allowFontScaling={true} ellipsizeMode={'tail'}>
Nice text
),
// @boost-ignore
unoptimizedComponent: Nice text ,
},
];
================================================
FILE: packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/non-react-native-import/code.js
================================================
import { Text } from 'some-other-package';
import { Text as RNText } from 'react-native';
Hello, world! ;
This is from React Native ;
================================================
FILE: packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/non-react-native-import/output.js
================================================
import { NativeText as _NativeText } from 'react-native-boost/runtime';
import { Text } from 'some-other-package';
import { Text as RNText } from 'react-native';
Hello, world! ;
<_NativeText allowFontScaling={true} ellipsizeMode={'tail'}>
This is from React Native
;
================================================
FILE: packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/normalize-accessibility-props/code.js
================================================
import { Text } from 'react-native';
;
================================================
FILE: packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/normalize-accessibility-props/output.js
================================================
import {
processAccessibilityProps as _processAccessibilityProps,
NativeText as _NativeText,
} from 'react-native-boost/runtime';
import { Text } from 'react-native';
<_NativeText
{..._processAccessibilityProps(
Object.assign(
{},
{
'aria-label': 'test',
},
{
accessibilityLabel: 'test',
}
)
)}
allowFontScaling={true}
ellipsizeMode={'tail'}
/>;
================================================
FILE: packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/normalize-accessibility-props-and-flatten-styles/code.js
================================================
import { Text } from 'react-native';
;
================================================
FILE: packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/normalize-accessibility-props-and-flatten-styles/output.js
================================================
import {
processAccessibilityProps as _processAccessibilityProps,
processTextStyle as _processTextStyle,
NativeText as _NativeText,
} from 'react-native-boost/runtime';
import { Text } from 'react-native';
<_NativeText
{..._processAccessibilityProps(
Object.assign(
{},
{
'aria-label': 'test',
},
{
accessibilityLabel: 'test',
}
)
)}
{..._processTextStyle([
{
color: 'red',
},
{
fontSize: 16,
},
])}
allowFontScaling={true}
ellipsizeMode={'tail'}
/>;
================================================
FILE: packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/number-of-lines/code.js
================================================
import { Text } from 'react-native';
10 lines ;
-10 lines ;
0 lines ;
================================================
FILE: packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/number-of-lines/output.js
================================================
import { NativeText as _NativeText } from 'react-native-boost/runtime';
import { Text } from 'react-native';
<_NativeText numberOfLines={10} allowFontScaling={true} ellipsizeMode={'tail'}>
10 lines
;
<_NativeText numberOfLines={0} allowFontScaling={true} ellipsizeMode={'tail'}>
-10 lines
;
<_NativeText numberOfLines={0} allowFontScaling={true} ellipsizeMode={'tail'}>
0 lines
;
================================================
FILE: packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/pressables/code.js
================================================
import { Text } from 'react-native';
Hello, world! ;
{
console.log('pressed');
}}>
Hello, world!
;
================================================
FILE: packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/pressables/output.js
================================================
import { NativeText as _NativeText } from 'react-native-boost/runtime';
import { Text } from 'react-native';
<_NativeText allowFontScaling={true} ellipsizeMode={'tail'}>
Hello, world!
;
{
console.log('pressed');
}}>
Hello, world!
;
================================================
FILE: packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/resolvable-spread-props/code.js
================================================
import { Text } from 'react-native';
const props = {
children: 'Hello, world!',
};
;
================================================
FILE: packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/resolvable-spread-props/output.js
================================================
import { NativeText as _NativeText } from 'react-native-boost/runtime';
import { Text } from 'react-native';
const props = {
children: 'Hello, world!',
};
<_NativeText {...props} allowFontScaling={true} ellipsizeMode={'tail'} />;
================================================
FILE: packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/text-content/code.js
================================================
import { Text } from 'react-native';
Hello, world! ;
const text = 'Hello again, world!';
{text} ;
================================================
FILE: packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/text-content/output.js
================================================
import { NativeText as _NativeText } from 'react-native-boost/runtime';
import { Text } from 'react-native';
<_NativeText allowFontScaling={true} ellipsizeMode={'tail'}>
Hello, world!
;
const text = 'Hello again, world!';
<_NativeText allowFontScaling={true} ellipsizeMode={'tail'}>
{text}
;
================================================
FILE: packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/text-content-as-explicit-child-prop/code.js
================================================
import { Text } from 'react-native';
;
================================================
FILE: packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/text-content-as-explicit-child-prop/output.js
================================================
import { NativeText as _NativeText } from 'react-native-boost/runtime';
import { Text } from 'react-native';
<_NativeText children="Hello, world!" allowFontScaling={true} ellipsizeMode={'tail'} />;
================================================
FILE: packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/two-components/code.js
================================================
import { Text } from 'react-native';
;
;
================================================
FILE: packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/two-components/output.js
================================================
import { NativeText as _NativeText } from 'react-native-boost/runtime';
import { Text } from 'react-native';
<_NativeText allowFontScaling={true} ellipsizeMode={'tail'} />;
<_NativeText allowFontScaling={true} ellipsizeMode={'tail'}>;
================================================
FILE: packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/unresolvable-spread-props/code.js
================================================
import { Text } from 'react-native';
function MyComponent(props) {
return Hello, world! ;
}
================================================
FILE: packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/unresolvable-spread-props/output.js
================================================
import { Text } from 'react-native';
function MyComponent(props) {
return Hello, world! ;
}
================================================
FILE: packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/variable-child-no-string/code.js
================================================
import { Text } from 'react-native';
Hello, world! ;
const test = Test ;
{test} ;
================================================
FILE: packages/react-native-boost/src/plugin/optimizers/text/__tests__/fixtures/variable-child-no-string/output.js
================================================
import { NativeText as _NativeText } from 'react-native-boost/runtime';
import { Text } from 'react-native';
<_NativeText allowFontScaling={true} ellipsizeMode={'tail'}>
Hello, world!
;
const test = (
<_NativeText allowFontScaling={true} ellipsizeMode={'tail'}>
Test
);
{test} ;
================================================
FILE: packages/react-native-boost/src/plugin/optimizers/text/__tests__/index.test.ts
================================================
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,
});
================================================
FILE: packages/react-native-boost/src/plugin/optimizers/text/index.ts
================================================
import { NodePath, types as t } from '@babel/core';
import { HubFile, Optimizer, PluginLogger } from '../../types';
import PluginError from '../../utils/plugin-error';
import { BailoutCheck, getFirstBailoutReason } from '../../utils/helpers';
import {
addDefaultProperty,
addFileImportHint,
buildPropertiesFromAttributes,
hasAccessibilityProperty,
hasBlacklistedProperty,
isForcedLine,
isIgnoredLine,
isValidJSXComponent,
isReactNativeImport,
replaceWithNativeComponent,
isStringNode,
hasExpoRouterLinkParentWithAsChild,
} from '../../utils/common';
import { RUNTIME_MODULE_NAME } from '../../utils/constants';
import { ACCESSIBILITY_PROPERTIES } from '../../utils/constants';
import { extractStyleAttribute, extractSelectableAndUpdateStyle } from '../../utils/common';
export const textBlacklistedProperties = new Set([
'id',
'nativeID',
'onLongPress',
'onPress',
'onPressIn',
'onPressOut',
'onResponderGrant',
'onResponderMove',
'onResponderRelease',
'onResponderTerminate',
'onResponderTerminationRequest',
'onStartShouldSetResponder',
'pressRetentionOffset',
'suppressHighlighting',
'selectionColor', // TODO: we can use react-native's internal `processColor` to process this at runtime
]);
export const textOptimizer: Optimizer = (path, logger) => {
if (!isValidJSXComponent(path, 'Text')) return;
if (!isReactNativeImport(path, 'Text')) return;
const parent = path.parent as t.JSXElement;
const forced = isForcedLine(path);
const overridableChecks: BailoutCheck[] = [
{
reason: 'contains blacklisted props',
shouldBail: () => hasBlacklistedProperty(path, textBlacklistedProperties),
},
{
reason: 'is a direct child of expo-router Link with asChild',
shouldBail: () => hasExpoRouterLinkParentWithAsChild(path),
},
{
reason: 'contains non-string children',
shouldBail: () => hasInvalidChildren(path, parent),
},
];
if (forced) {
const overriddenReason = getFirstBailoutReason(overridableChecks);
if (overriddenReason) {
logger.forced({ component: 'Text', path, reason: overriddenReason });
}
} else {
const skipReason = getFirstBailoutReason([
{
reason: 'line is marked with @boost-ignore',
shouldBail: () => isIgnoredLine(path),
},
...overridableChecks,
]);
if (skipReason) {
logger.skipped({ component: 'Text', 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');
}
logger.optimized({
component: 'Text',
path,
});
// Process props
fixNegativeNumberOfLines({ path, logger });
addDefaultProperty(path, 'allowFontScaling', t.booleanLiteral(true));
addDefaultProperty(path, 'ellipsizeMode', t.stringLiteral('tail'));
processProps(path, file);
// Replace the Text component with NativeText
replaceWithNativeComponent(path, parent, file, 'NativeText');
};
/**
* 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.
*
* @param path - The path to the JSXOpeningElement.
* @param parent - The parent JSX element.
* @returns true if the component has invalid children or blacklisted properties.
*/
function hasInvalidChildren(path: NodePath, parent: t.JSXElement): boolean {
for (const attribute of path.node.attributes) {
if (t.isJSXSpreadAttribute(attribute)) continue; // Spread attributes are handled in hasBlacklistedProperty
if (
t.isJSXIdentifier(attribute.name) &&
attribute.value &&
// For a "children" attribute, optimization is allowed only if it is a string
attribute.name.name === 'children' &&
!isStringNode(path, attribute.value)
) {
return true;
}
}
// Return true if any child is not a string node
return !parent.children.every((child) => isStringNode(path, child));
}
/**
* Fixes negative numberOfLines values by setting them to 0.
*/
function fixNegativeNumberOfLines({ path, logger }: { path: NodePath; logger: PluginLogger }) {
for (const attribute of path.node.attributes) {
if (
t.isJSXAttribute(attribute) &&
t.isJSXIdentifier(attribute.name, { name: 'numberOfLines' }) &&
attribute.value &&
t.isJSXExpressionContainer(attribute.value)
) {
let originalValue: number | undefined;
if (t.isNumericLiteral(attribute.value.expression)) {
originalValue = attribute.value.expression.value;
} else if (
t.isUnaryExpression(attribute.value.expression) &&
attribute.value.expression.operator === '-' &&
t.isNumericLiteral(attribute.value.expression.argument)
) {
originalValue = -attribute.value.expression.argument.value;
}
if (originalValue !== undefined && originalValue < 0) {
logger.warning({
component: 'Text',
path,
message: `'numberOfLines' must be a non-negative number, received: ${originalValue}. The value will be set to 0.`,
});
attribute.value.expression = t.numericLiteral(0);
}
}
}
}
/**
* Processes style and accessibility attributes, replacing them with optimized versions.
*/
function processProps(path: NodePath, file: HubFile) {
// Grab the up-to-date list of attributes
const currentAttributes = [...path.node.attributes];
const { styleExpr, styleAttribute } = extractStyleAttribute(currentAttributes);
const hasA11y = hasAccessibilityProperty(path, currentAttributes);
// ============================================
// 1. Prepare spread attributes (style / a11y)
// ============================================
const spreadAttributes: t.JSXSpreadAttribute[] = [];
// --- Accessibility ---
if (hasA11y) {
const accessibilityAttributes = currentAttributes.filter((attribute) => {
if (!t.isJSXAttribute(attribute)) return false;
return t.isJSXIdentifier(attribute.name) && ACCESSIBILITY_PROPERTIES.has(attribute.name.name as string);
});
const normalizeIdentifier = addFileImportHint({
file,
nameHint: 'processAccessibilityProps',
path,
importName: 'processAccessibilityProps',
moduleName: RUNTIME_MODULE_NAME,
});
const accessibilityObject = buildPropertiesFromAttributes(accessibilityAttributes);
const accessibilityExpr = t.callExpression(t.identifier(normalizeIdentifier.name), [accessibilityObject]);
spreadAttributes.push(t.jsxSpreadAttribute(accessibilityExpr));
}
// --- Style ---
let selectableAttribute: t.JSXAttribute | undefined;
if (styleExpr) {
// Attempt a compile-time extraction of `userSelect`
const selectableValue = extractSelectableAndUpdateStyle(styleExpr);
if (selectableValue != null) {
selectableAttribute = t.jsxAttribute(
t.jsxIdentifier('selectable'),
t.jsxExpressionContainer(t.booleanLiteral(selectableValue))
);
}
const flattenIdentifier = addFileImportHint({
file,
nameHint: 'processTextStyle',
path,
importName: 'processTextStyle',
moduleName: RUNTIME_MODULE_NAME,
});
const flattenedStyleExpr = t.callExpression(t.identifier(flattenIdentifier.name), [styleExpr]);
spreadAttributes.push(t.jsxSpreadAttribute(flattenedStyleExpr));
}
// ============================================
// 2. Collect the remaining (non-processed) attributes
// ============================================
const remainingAttributes: (t.JSXAttribute | t.JSXSpreadAttribute)[] = [];
for (const attribute of currentAttributes) {
// Skip the style attribute (we have replaced it with a spread)
if (styleAttribute && attribute === styleAttribute) continue;
// Skip accessibility attributes if we processed them
if (
hasA11y &&
t.isJSXAttribute(attribute) &&
t.isJSXIdentifier(attribute.name) &&
ACCESSIBILITY_PROPERTIES.has(attribute.name.name as string)
) {
continue;
}
remainingAttributes.push(attribute);
}
path.node.attributes = [...spreadAttributes, selectableAttribute, ...remainingAttributes].filter(
(attribute): attribute is t.JSXAttribute | t.JSXSpreadAttribute => attribute !== undefined
);
}
================================================
FILE: packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/basic/code.js
================================================
import { View } from 'react-native';
;
================================================
FILE: packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/basic/output.js
================================================
import { NativeView as _NativeView } from 'react-native-boost/runtime';
import { View } from 'react-native';
<_NativeView />;
================================================
FILE: packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/force-comment/code.js
================================================
import { View } from 'react-native';
<>
{/* @boost-force */}
>;
================================================
FILE: packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/force-comment/output.js
================================================
import { NativeView as _NativeView } from 'react-native-boost/runtime';
import { View } from 'react-native';
<>
{/* @boost-force */}
<_NativeView
style={{
flex: 1,
}}>
>;
================================================
FILE: packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/ignore-comment/code.js
================================================
import { View } from 'react-native';
<>
{/* @boost-ignore */}
>;
================================================
FILE: packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/ignore-comment/output.js
================================================
import { NativeView as _NativeView } from 'react-native-boost/runtime';
import { View } from 'react-native';
<>
<_NativeView>
{/* @boost-ignore */}
>;
================================================
FILE: packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/indirect-text-ancestor/code.js
================================================
import { Text, View } from 'react-native';
const Custom = ({ children }) => {
return {children} ;
};
<>
>;
================================================
FILE: packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/indirect-text-ancestor/output.js
================================================
import { NativeView as _NativeView } from 'react-native-boost/runtime';
import { Text, View } from 'react-native';
const Custom = ({ children }) => {
return {children} ;
};
<>
<_NativeView>
>;
================================================
FILE: packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/react-native-namespace-text-ancestor/code.js
================================================
import * as ReactNative from 'react-native';
import { View } from 'react-native';
;
================================================
FILE: packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/react-native-namespace-text-ancestor/output.js
================================================
import * as ReactNative from 'react-native';
import { View } from 'react-native';
;
================================================
FILE: packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/same-file-safe-ancestor/code.js
================================================
import { View } from 'react-native';
const Wrapper = ({ children }) => <>{children}>;
;
================================================
FILE: packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/same-file-safe-ancestor/output.js
================================================
import { NativeView as _NativeView } from 'react-native-boost/runtime';
import { View } from 'react-native';
const Wrapper = ({ children }) => <>{children}>;
<_NativeView>
;
================================================
FILE: packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/same-file-unknown-ancestor/code.js
================================================
import { View } from 'react-native';
const Wrapper = ({ children }) => {children} ;
;
================================================
FILE: packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/same-file-unknown-ancestor/output.js
================================================
import { View } from 'react-native';
const Wrapper = ({ children }) => {children} ;
;
================================================
FILE: packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/text-ancestor/code.js
================================================
import { Text, View } from 'react-native';
<>
>;
================================================
FILE: packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/text-ancestor/output.js
================================================
import { NativeView as _NativeView } from 'react-native-boost/runtime';
import { Text, View } from 'react-native';
<>
<_NativeView>
>;
================================================
FILE: packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/unknown-imported-ancestor/code.js
================================================
import { View } from 'react-native';
import { ExternalWrapper } from './ExternalWrapper';
<>
>;
================================================
FILE: packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/unknown-imported-ancestor/dangerous-output.js
================================================
import { NativeView as _NativeView } from 'react-native-boost/runtime';
import { View } from 'react-native';
import { ExternalWrapper } from './ExternalWrapper';
<>
<_NativeView>
<_NativeView>
>;
================================================
FILE: packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/unknown-imported-ancestor/output.js
================================================
import { NativeView as _NativeView } from 'react-native-boost/runtime';
import { View } from 'react-native';
import { ExternalWrapper } from './ExternalWrapper';
<>
<_NativeView>
>;
================================================
FILE: packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/unresolvable-spread-props/code.js
================================================
import { View } from 'react-native';
function MyComponent(props) {
return (
);
}
================================================
FILE: packages/react-native-boost/src/plugin/optimizers/view/__tests__/fixtures/unresolvable-spread-props/output.js
================================================
import { View } from 'react-native';
function MyComponent(props) {
return (
);
}
================================================
FILE: packages/react-native-boost/src/plugin/optimizers/view/__tests__/index.test.ts
================================================
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 '..';
pluginTester({
plugin: generateTestPlugin(viewOptimizer),
title: 'view',
fixtures: path.resolve(import.meta.dirname, 'fixtures'),
babelOptions: {
plugins: ['@babel/plugin-syntax-jsx'],
},
formatResult: formatTestResult,
});
pluginTester({
plugin: generateTestPlugin(viewOptimizer, {
dangerouslyOptimizeViewWithUnknownAncestors: true,
}),
title: 'view dangerous unknown ancestors',
babelOptions: {
plugins: ['@babel/plugin-syntax-jsx'],
},
formatResult: formatTestResult,
tests: [
{
title: 'optimizes View 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'),
},
],
});
================================================
FILE: packages/react-native-boost/src/plugin/optimizers/view/index.ts
================================================
import { types as t } from '@babel/core';
import { HubFile, Optimizer } from '../../types';
import PluginError from '../../utils/plugin-error';
import { BailoutCheck, getFirstBailoutReason } from '../../utils/helpers';
import {
hasBlacklistedProperty,
isForcedLine,
isIgnoredLine,
isValidJSXComponent,
isReactNativeImport,
replaceWithNativeComponent,
getViewAncestorClassification,
ViewAncestorClassification,
} from '../../utils/common';
export const viewBlacklistedProperties = new Set([
// TODO: process a11y props at runtime
'accessible',
'accessibilityLabel',
'accessibilityState',
'aria-busy',
'aria-checked',
'aria-disabled',
'aria-expanded',
'aria-label',
'aria-selected',
'id',
'nativeID',
'style', // TODO: process style at runtime
]);
export const viewOptimizer: Optimizer = (path, logger, options) => {
if (!isValidJSXComponent(path, 'View')) return;
if (!isReactNativeImport(path, 'View')) return;
let ancestorClassification: ViewAncestorClassification | undefined;
const getAncestorClassification = () => {
if (!ancestorClassification) {
ancestorClassification = getViewAncestorClassification(path);
}
return ancestorClassification;
};
const forced = isForcedLine(path);
const overridableChecks: BailoutCheck[] = [
{
reason: 'contains blacklisted props',
shouldBail: () => hasBlacklistedProperty(path, viewBlacklistedProperties),
},
{
reason: 'has Text ancestor',
shouldBail: () => getAncestorClassification() === 'text',
},
{
reason: 'has unresolved ancestor and dangerous optimization is disabled',
shouldBail: () =>
getAncestorClassification() === 'unknown' && options?.dangerouslyOptimizeViewWithUnknownAncestors !== true,
},
];
if (forced) {
const overriddenReason = getFirstBailoutReason(overridableChecks);
if (overriddenReason) {
logger.forced({ component: 'View', path, reason: overriddenReason });
}
} else {
const skipReason = getFirstBailoutReason([
{
reason: 'line is marked with @boost-ignore',
shouldBail: () => isIgnoredLine(path),
},
...overridableChecks,
]);
if (skipReason) {
logger.skipped({ component: 'View', 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');
}
logger.optimized({
component: 'View',
path,
});
const parent = path.parent as t.JSXElement;
replaceWithNativeComponent(path, parent, file, 'NativeView');
};
================================================
FILE: packages/react-native-boost/src/plugin/types/index.ts
================================================
import { NodePath, types as t } from '@babel/core';
export interface PluginOptimizationOptions {
/**
* Whether to optimize the `Text` component.
* @default true
*/
text?: boolean;
/**
* Whether to optimize the `View` component.
* @default true
*/
view?: boolean;
}
export interface PluginOptions {
/**
* 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 []
*/
ignores?: string[];
/**
* 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
*/
verbose?: boolean;
/**
* Disables all plugin logs.
*
* When set to `true`, this overrides `verbose`.
* @default false
*/
silent?: boolean;
/**
* Toggle individual optimizers.
*
* If omitted, all available optimizers are enabled.
*/
optimizations?: PluginOptimizationOptions;
/**
* 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
*/
dangerouslyOptimizeViewWithUnknownAncestors?: boolean;
}
export type OptimizableComponent = 'Text' | 'View';
export interface OptimizationLogPayload {
component: OptimizableComponent;
path: NodePath;
}
export interface SkippedOptimizationLogPayload extends OptimizationLogPayload {
reason: string;
}
export interface WarningLogPayload {
message: string;
component?: OptimizableComponent;
path?: NodePath;
}
export interface PluginLogger {
optimized: (payload: OptimizationLogPayload) => void;
skipped: (payload: SkippedOptimizationLogPayload) => void;
forced: (payload: SkippedOptimizationLogPayload) => void;
warning: (payload: WarningLogPayload) => void;
}
export type Optimizer = (path: NodePath, logger: PluginLogger, options?: PluginOptions) => void;
export type HubFile = t.File & {
opts: {
filename: string;
};
__hasImports?: Record;
__optimized?: boolean;
};
/**
* Options for adding a file import hint.
*/
export interface FileImportOptions {
file: HubFile;
/** The name hint which also acts as the cache key to ensure the import is only added once (e.g. 'processAccessibilityProps') */
nameHint: string;
/** The current Babel NodePath */
path: NodePath;
/**
* The named import string (e.g. 'processAccessibilityProps'). Ignored if importType is "default".
*/
importName: string;
/** The module to import from (e.g. 'react-native-boost/runtime') */
moduleName: string;
/**
* Determines which helper to use:
* - "named" (default) uses addNamed (requires importName)
* - "default" uses addDefault
*/
importType?: 'named' | 'default';
}
================================================
FILE: packages/react-native-boost/src/plugin/utils/__tests__/logger.test.ts
================================================
import { NodePath, types as t } from '@babel/core';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { createLogger } from '../logger';
const originalEnv = {
NO_COLOR: process.env.NO_COLOR,
FORCE_COLOR: process.env.FORCE_COLOR,
CLICOLOR: process.env.CLICOLOR,
CLICOLOR_FORCE: process.env.CLICOLOR_FORCE,
COLORTERM: process.env.COLORTERM,
TERM: process.env.TERM,
};
describe('logger', () => {
afterEach(() => {
restoreEnvVar('NO_COLOR', originalEnv.NO_COLOR);
restoreEnvVar('FORCE_COLOR', originalEnv.FORCE_COLOR);
restoreEnvVar('CLICOLOR', originalEnv.CLICOLOR);
restoreEnvVar('CLICOLOR_FORCE', originalEnv.CLICOLOR_FORCE);
restoreEnvVar('COLORTERM', originalEnv.COLORTERM);
restoreEnvVar('TERM', originalEnv.TERM);
vi.restoreAllMocks();
});
it('logs optimized components by default', () => {
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
const logger = createLogger({
verbose: false,
silent: false,
});
const path = createMockPath('/app/screens/LoginScreen.tsx', 42);
logger.optimized({
component: 'Text',
path,
});
logger.skipped({
component: 'Text',
path,
reason: 'contains non-string children',
});
expect(consoleSpy).toHaveBeenCalledTimes(1);
expect(stripAnsi(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', () => {
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
const logger = createLogger({
verbose: true,
silent: false,
});
const path = createMockPath('/app/screens/Settings.tsx', 10);
logger.skipped({
component: 'View',
path,
reason: 'has unresolved ancestor and dangerous optimization is disabled',
});
expect(consoleSpy).toHaveBeenCalledTimes(1);
expect(stripAnsi(String(consoleSpy.mock.calls[0][0]))).toContain(
'Skipped View in /app/screens/Settings.tsx:10 (has unresolved ancestor and dangerous optimization is disabled)'
);
});
it('disables all logs when silent is enabled', () => {
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
const logger = createLogger({
verbose: true,
silent: true,
});
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',
});
expect(consoleSpy).not.toHaveBeenCalled();
});
it('colorizes log levels when TERM supports colors even without TTY', () => {
delete process.env.NO_COLOR;
delete process.env.FORCE_COLOR;
delete process.env.CLICOLOR;
delete process.env.CLICOLOR_FORCE;
delete process.env.COLORTERM;
process.env.TERM = 'xterm-256color';
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
const logger = createLogger({
verbose: false,
silent: false,
});
const path = createMockPath('/app/screens/Color.tsx', 1);
logger.optimized({
component: 'Text',
path,
});
expect(consoleSpy).toHaveBeenCalledTimes(1);
expect(String(consoleSpy.mock.calls[0][0])).toContain('\u001B[32m[optimized]\u001B[0m');
});
it('does not colorize when NO_COLOR is set', () => {
process.env.NO_COLOR = '1';
process.env.TERM = 'xterm-256color';
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
const logger = createLogger({
verbose: false,
silent: false,
});
const path = createMockPath('/app/screens/NoColor.tsx', 1);
logger.optimized({
component: 'Text',
path,
});
expect(consoleSpy).toHaveBeenCalledTimes(1);
expect(String(consoleSpy.mock.calls[0][0])).not.toContain('\u001B[32m[optimized]\u001B[0m');
});
});
function createMockPath(filename: string, lineNumber: number): NodePath {
return {
hub: {
file: {
opts: {
filename,
},
},
},
node: {
loc: {
start: {
line: lineNumber,
},
},
},
} as unknown as NodePath;
}
function stripAnsi(value: string): string {
return value.replace(/\u001B\[[0-9;]*m/g, '');
}
function restoreEnvVar(
key: 'NO_COLOR' | 'FORCE_COLOR' | 'CLICOLOR' | 'CLICOLOR_FORCE' | 'COLORTERM' | 'TERM',
value: string | undefined
): void {
if (value === undefined) {
delete process.env[key];
return;
}
process.env[key] = value;
}
================================================
FILE: packages/react-native-boost/src/plugin/utils/common/attributes.ts
================================================
import { NodePath, types as t } from '@babel/core';
import { ACCESSIBILITY_PROPERTIES } from '../constants';
import { USER_SELECT_STYLE_TO_SELECTABLE_PROP } from '../constants';
/**
* Checks if the JSX element has a blacklisted property.
*
* @param path - The path to the JSXOpeningElement.
* @param blacklist - The set of blacklisted properties.
* @returns true if the JSX element has a blacklisted property.
*/
export const hasBlacklistedProperty = (path: NodePath, blacklist: Set): boolean => {
return path.node.attributes.some((attribute) => {
// Check direct attributes (e.g., onPress={handler})
if (t.isJSXAttribute(attribute) && t.isJSXIdentifier(attribute.name) && blacklist.has(attribute.name.name)) {
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;
}
// For other attribute types, assume no blacklisting
return false;
});
};
/**
* Adds a default property to a JSX element if it's not already defined. It avoids adding a default
* if it cannot statically determine whether the property is already set.
*
* @param path - The path to the JSXOpeningElement.
* @param key - The property key.
* @param value - The default value expression.
*/
export const addDefaultProperty = (path: NodePath, key: string, value: t.Expression) => {
let propertyIsFound = false;
let hasUnresolvableSpread = false;
for (const attribute of path.node.attributes) {
if (t.isJSXAttribute(attribute) && attribute.name.name === key) {
propertyIsFound = true;
break;
}
if (t.isJSXSpreadAttribute(attribute)) {
if (t.isObjectExpression(attribute.argument)) {
const propertyInSpread = attribute.argument.properties.some(
(p) =>
(t.isObjectProperty(p) && t.isIdentifier(p.key) && p.key.name === key) ||
(t.isObjectProperty(p) && t.isStringLiteral(p.key) && p.key.value === key)
);
if (propertyInSpread) {
propertyIsFound = true;
break;
}
} else if (t.isIdentifier(attribute.argument)) {
const binding = path.scope.getBinding(attribute.argument.name);
if (
binding?.path.node &&
t.isVariableDeclarator(binding.path.node) &&
t.isObjectExpression(binding.path.node.init)
) {
const propertyInSpread = binding.path.node.init.properties.some(
(p) =>
(t.isObjectProperty(p) && t.isIdentifier(p.key) && p.key.name === key) ||
(t.isObjectProperty(p) && t.isStringLiteral(p.key) && p.key.value === key)
);
if (propertyInSpread) {
propertyIsFound = true;
break;
}
} else {
hasUnresolvableSpread = true;
break;
}
} else {
hasUnresolvableSpread = true;
break;
}
}
}
if (!propertyIsFound && !hasUnresolvableSpread) {
path.node.attributes.push(t.jsxAttribute(t.jsxIdentifier(key), t.jsxExpressionContainer(value)));
}
};
/**
* Helper that builds an Object.assign expression out of the existing JSX attributes.
* It handles both plain JSXAttributes and spread attributes.
*
* @param attributes - The attributes to build the expression from.
* @returns The Object.assign expression.
*/
export const buildPropertiesFromAttributes = (attributes: (t.JSXAttribute | t.JSXSpreadAttribute)[]): t.Expression => {
const arguments_: t.Expression[] = [];
for (const attribute of attributes) {
if (t.isJSXSpreadAttribute(attribute)) {
arguments_.push(attribute.argument);
} else if (t.isJSXAttribute(attribute)) {
const key = attribute.name.name;
let value: t.Expression;
if (!attribute.value) {
value = t.booleanLiteral(true);
} else if (t.isStringLiteral(attribute.value)) {
value = attribute.value;
} else if (t.isJSXExpressionContainer(attribute.value)) {
value = t.isJSXEmptyExpression(attribute.value.expression)
? t.booleanLiteral(true)
: attribute.value.expression;
} else {
value = t.nullLiteral();
}
// If the key is not a valid JavaScript identifier (e.g. "aria-label"), use a string literal.
const validIdentifierRegex = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
const keyNode =
typeof key === 'string' && validIdentifierRegex.test(key) ? t.identifier(key) : t.stringLiteral(key.toString());
arguments_.push(t.objectExpression([t.objectProperty(keyNode, value)]));
}
}
if (arguments_.length === 0) {
return t.objectExpression([]);
}
return t.callExpression(t.memberExpression(t.identifier('Object'), t.identifier('assign')), [
t.objectExpression([]),
...arguments_,
]);
};
/**
* Checks if the JSX element has an accessibility property.
*
* @param path - The NodePath for the JSXOpeningElement, used for scope lookup.
* @param attributes - The attributes to check.
* @returns true if the JSX element has an accessibility property.
*/
export const hasAccessibilityProperty = (
path: NodePath,
attributes: (t.JSXAttribute | t.JSXSpreadAttribute)[]
): boolean => {
for (const attribute of attributes) {
if (t.isJSXAttribute(attribute)) {
const key = attribute.name.name;
if (typeof key === 'string' && ACCESSIBILITY_PROPERTIES.has(key)) {
return true;
}
} else if (t.isJSXSpreadAttribute(attribute)) {
if (t.isObjectExpression(attribute.argument)) {
for (const property of attribute.argument.properties) {
if (
t.isObjectProperty(property) &&
t.isIdentifier(property.key) &&
ACCESSIBILITY_PROPERTIES.has(property.key.name)
) {
return true;
}
}
} else if (t.isIdentifier(attribute.argument)) {
const binding = path.scope.getBinding(attribute.argument.name);
if (binding && t.isVariableDeclarator(binding.path.node)) {
const declarator = binding.path.node as t.VariableDeclarator;
if (declarator.init && t.isObjectExpression(declarator.init)) {
for (const property of declarator.init.properties) {
if (
t.isObjectProperty(property) &&
t.isIdentifier(property.key) &&
ACCESSIBILITY_PROPERTIES.has(property.key.name)
) {
return true;
}
}
continue;
}
}
return true;
} else {
return true;
}
}
}
return false;
};
/**
* Extracts the `style` attribute from a JSX attributes list.
*
* @returns An object containing the attribute node itself (if found) and the expression inside
*/
export function extractStyleAttribute(attributes: Array): {
styleAttribute?: t.JSXAttribute;
styleExpr?: t.Expression;
} {
for (const attribute of attributes) {
if (t.isJSXAttribute(attribute) && t.isJSXIdentifier(attribute.name, { name: 'style' })) {
if (
attribute.value &&
t.isJSXExpressionContainer(attribute.value) &&
!t.isJSXEmptyExpression(attribute.value.expression)
) {
return {
styleAttribute: attribute,
styleExpr: attribute.value.expression,
};
}
return { styleAttribute: attribute };
}
}
return {};
}
/**
* Attempts to statically extract the `userSelect` style property from a style expression.
*
* If the `userSelect` value can be resolved at compile-time, the property is removed from the
* object literal (or array element) and its mapped boolean value for the native `selectable`
* prop is returned. When the value is unknown or the expression is not statically analysable,
* `undefined` is returned and no modification is made.
*/
export function extractSelectableAndUpdateStyle(styleExpr: t.Expression): boolean | undefined {
// Helper to process a single ObjectExpression
const handleObjectExpression = (objectExpr: t.ObjectExpression): boolean | undefined => {
let selectableValue: boolean | undefined;
objectExpr.properties = objectExpr.properties.filter((property) => {
if (
!t.isObjectProperty(property) ||
(!t.isIdentifier(property.key, { name: 'userSelect' }) &&
!(t.isStringLiteral(property.key) && property.key.value === 'userSelect'))
) {
return true; // keep property
}
if (t.isStringLiteral(property.value)) {
const mapped = USER_SELECT_STYLE_TO_SELECTABLE_PROP[property.value.value];
if (mapped !== undefined) {
selectableValue = mapped;
}
}
// Remove the `userSelect` property
return false;
});
return selectableValue;
};
if (t.isObjectExpression(styleExpr)) {
return handleObjectExpression(styleExpr);
}
if (t.isArrayExpression(styleExpr)) {
let selectableValue: boolean | undefined;
for (const element of styleExpr.elements) {
if (element && t.isObjectExpression(element)) {
const value = handleObjectExpression(element);
if (value !== undefined) {
selectableValue = value; // prefer last defined value
}
}
}
return selectableValue;
}
return undefined; // not statically analysable
}
/**
* Checks if a node represents a string value.
*/
export const isStringNode = (path: NodePath, child: t.Node): boolean => {
if (t.isJSXText(child) || t.isStringLiteral(child)) return true;
if (t.isJSXExpressionContainer(child)) {
const expression = child.expression;
if (t.isIdentifier(expression)) {
const binding = path.scope.getBinding(expression.name);
if (binding && binding.path.node && t.isVariableDeclarator(binding.path.node)) {
return !!binding.path.node.init && t.isStringLiteral(binding.path.node.init);
}
return false;
}
if (t.isStringLiteral(expression)) return true;
}
return false;
};
================================================
FILE: packages/react-native-boost/src/plugin/utils/common/base.ts
================================================
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 } from '../constants';
/**
* Adds a hint to the file object to ensure that a specific import is added only once and cached on the file object.
*
* @param opts - Object containing the function arguments:
* - file: The Babel file object (e.g. HubFile)
* - nameHint: The name hint which also acts as the cache key to ensure the import is only added once (e.g. 'processAccessibilityProps')
* - path: The current Babel NodePath
* - importName: The named import string (e.g. 'processAccessibilityProps'), used when importType is 'named'
* - moduleName: The module to import from (e.g. 'react-native-boost/runtime')
* - importType: Either 'named' (default) or 'default' to determine the type of import to use.
*
* @returns The identifier returned by addNamed or addDefault.
*/
export function addFileImportHint({
file,
nameHint,
path,
importName,
moduleName,
importType = 'named',
}: FileImportOptions): t.Identifier {
if (!file.__hasImports?.[nameHint]) {
file.__hasImports = file.__hasImports || {};
file.__hasImports[nameHint] =
importType === 'default'
? addDefault(path, moduleName, { nameHint })
: addNamed(path, importName, moduleName, { nameHint });
}
return file.__hasImports[nameHint];
}
/**
* Replaces a component with its native counterpart.
* This function handles both the opening and closing tags.
*
* @param path - The path to the JSXOpeningElement.
* @param parent - The parent JSX element.
* @param file - The Babel file object.
* @param nativeComponentName - The name of the native component to import.
* @param moduleName - The module to import the native component from.
* @returns The identifier for the imported native component.
*/
export const replaceWithNativeComponent = (
path: NodePath,
parent: t.JSXElement,
file: HubFile,
nativeComponentName: string
): t.Identifier => {
// Add native component import (cached on file) to prevent duplicate imports
const nativeIdentifier = addFileImportHint({
file,
nameHint: nativeComponentName,
path,
importName: nativeComponentName,
moduleName: RUNTIME_MODULE_NAME,
importType: 'named',
});
// Get the current name of the component, which may be aliased (i.e. Text -> RNText)
const currentName = (path.node.name as t.JSXIdentifier).name;
// Replace the component with its native counterpart
const jsxName = path.node.name as t.JSXIdentifier;
jsxName.name = nativeIdentifier.name;
// If the element is not self-closing, update the closing element as well
if (
!path.node.selfClosing &&
parent.closingElement &&
t.isJSXIdentifier(parent.closingElement.name) &&
parent.closingElement.name.name === currentName
) {
parent.closingElement.name.name = nativeIdentifier.name;
}
return nativeIdentifier;
};
================================================
FILE: packages/react-native-boost/src/plugin/utils/common/index.ts
================================================
export * from './validation';
export * from './attributes';
export * from './base';
================================================
FILE: packages/react-native-boost/src/plugin/utils/common/validation.ts
================================================
import { NodePath, types as t } from '@babel/core';
import { ensureArray } from '../helpers';
import { HubFile } from '../../types';
import { minimatch } from 'minimatch';
import nodePath from 'node:path';
import PluginError from '../plugin-error';
/**
* 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;
// 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();
// 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);
// Check if the file name matches the glob pattern.
if (minimatch(fileName, absolutePattern, { dot: true })) {
return true;
}
}
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;
}
const jsxElementPath = path.parentPath;
if (jsxElementPath.node.leadingComments?.some((comment) => comment.value.includes(decorator))) {
return true;
}
// Check leading comments on the ObjectProperty (if the JSX element is a value inside an object literal).
const propertyPath = jsxElementPath.parentPath;
if (
propertyPath &&
propertyPath.isObjectProperty() &&
propertyPath.node.leadingComments?.some((comment) => comment.value.includes(decorator))
) {
return true;
}
if (!jsxElementPath.parentPath) return false;
const containerPath = jsxElementPath.parentPath;
const siblings = ensureArray(containerPath.get('children'));
const index = siblings.findIndex((sibling) => sibling.node === jsxElementPath.node);
if (index === -1) return false;
for (let index_ = index - 1; index_ >= 0; index_--) {
const sibling = siblings[index_];
if (sibling.isJSXText() && sibling.node.value.trim() === '') {
continue;
}
if (sibling.isJSXExpressionContainer()) {
const expression = sibling.get('expression');
if (expression && expression.node) {
const comments = [
...(expression.node.leadingComments || []),
...(expression.node.trailingComments || []),
...(expression.node.innerComments || []),
].map((comment) => comment.value.trim());
if (comments.some((comment) => comment.includes(decorator))) {
return true;
}
}
}
if (
sibling.node.leadingComments &&
sibling.node.leadingComments.some((comment) => comment.value.includes(decorator))
) {
return true;
}
break;
}
return false;
}
/**
* Checks if the path represents a valid JSX component with the specified name.
*
* @param path - The NodePath to check.
* @param componentName - The name of the component to validate against.
* @returns true if the path is a valid JSX component with the specified name.
*/
export const isValidJSXComponent = (path: NodePath, componentName: string): boolean => {
// Check if the node name is a JSX identifier
if (!t.isJSXIdentifier(path.node.name)) return false;
// Check if the parent is a JSX element
const parent = path.parent;
if (!t.isJSXElement(parent)) return false;
// For aliasing, we check if the underlying imported name matches the expected name
const componentIdentifier = path.node.name.name;
const binding = path.scope.getBinding(componentIdentifier);
if (!binding) return false;
if (
binding.kind === 'module' &&
t.isImportDeclaration(binding.path.parent) &&
t.isImportSpecifier(binding.path.node)
) {
const imported = binding.path.node.imported;
if (t.isIdentifier(imported)) {
return imported.name === componentName;
}
}
// Fallback to string match if binding is not available
return path.node.name.name === componentName;
};
/**
* Checks if the component is imported from 'react-native' and not from a custom module.
*
* @param path - The NodePath to check.
* @param expectedImportedName - The expected import name of the component (we'll also check for aliased imports).
* @returns true if the component is imported from 'react-native'
*/
export const isReactNativeImport = (path: NodePath, expectedImportedName: string): boolean => {
if (!t.isJSXIdentifier(path.node.name)) return false;
const localName = path.node.name.name;
const binding = path.scope.getBinding(localName);
if (!binding) return false;
if (binding.kind === 'module') {
const importDeclaration = binding.path.parent;
if (!t.isImportDeclaration(importDeclaration)) return false;
// Verify it's imported from 'react-native'
if (importDeclaration.source.value !== 'react-native') return false;
// For named imports, check the imported name (not the alias)
if (t.isImportSpecifier(binding.path.node)) {
const imported = binding.path.node.imported;
if (t.isIdentifier(imported)) {
return imported.name === expectedImportedName;
}
}
// For default imports, we just assume it's valid if imported from react-native.
if (t.isImportDefaultSpecifier(binding.path.node)) {
return true;
}
}
return false;
};
type AncestorClassification = 'safe' | 'text' | 'unknown';
export type ViewAncestorClassification = AncestorClassification;
type ScopeBinding = NonNullable['scope']['getBinding']>>;
type AncestorAnalysisContext = {
componentCache: WeakMap;
componentInProgress: WeakSet;
renderExpressionInProgress: WeakSet;
};
export const getViewAncestorClassification = (path: NodePath): ViewAncestorClassification => {
return classifyViewAncestors(path);
};
function classifyViewAncestors(path: NodePath): AncestorClassification {
const context: AncestorAnalysisContext = {
componentCache: new WeakMap(),
componentInProgress: new WeakSet(),
renderExpressionInProgress: new WeakSet(),
};
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;
}
function classifyJSXElementAsAncestor(
path: NodePath,
context: AncestorAnalysisContext
): AncestorClassification {
const openingElementName = path.node.openingElement.name;
if (t.isJSXIdentifier(openingElementName)) {
return classifyJSXIdentifierAsAncestor(path, openingElementName.name, context);
}
if (t.isJSXMemberExpression(openingElementName)) {
return classifyJSXMemberExpressionAsAncestor(path, openingElementName);
}
return 'unknown';
}
function classifyJSXIdentifierAsAncestor(
path: NodePath,
identifierName: string,
context: AncestorAnalysisContext
): AncestorClassification {
if (identifierName === 'Fragment') return 'safe';
const binding = path.scope.getBinding(identifierName);
if (!binding) return 'unknown';
return classifyBindingAsAncestor(binding, context);
}
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';
}
if (importDeclaration.source.value === 'react' && expression.property.name === 'Fragment') {
return 'safe';
}
return 'unknown';
}
function classifyBindingAsAncestor(binding: ScopeBinding, context: AncestorAnalysisContext): AncestorClassification {
if (binding.kind === 'module') {
return classifyModuleBindingAsAncestor(binding);
}
return classifyLocalBindingAsAncestor(binding, context);
}
function classifyModuleBindingAsAncestor(binding: ScopeBinding): AncestorClassification {
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';
}
return 'unknown';
}
if (source === 'react' && t.isImportSpecifier(binding.path.node)) {
const importedName = getImportSpecifierImportedName(binding.path.node);
if (importedName === 'Fragment') return 'safe';
}
return 'unknown';
}
function classifyLocalBindingAsAncestor(
binding: ScopeBinding,
context: AncestorAnalysisContext
): AncestorClassification {
const cacheKey = binding.path.node;
const cached = context.componentCache.get(cacheKey);
if (cached) return cached;
if (context.componentInProgress.has(cacheKey)) {
return 'unknown';
}
context.componentInProgress.add(cacheKey);
let classification: AncestorClassification;
if (binding.path.isFunctionDeclaration()) {
classification = analyzeFunctionComponent(binding.path, context);
} else if (binding.path.isVariableDeclarator()) {
classification = analyzeVariableDeclaratorComponent(binding.path, context);
} else {
classification = 'unknown';
}
context.componentInProgress.delete(cacheKey);
context.componentCache.set(cacheKey, classification);
return classification;
}
function analyzeVariableDeclaratorComponent(
path: NodePath,
context: AncestorAnalysisContext
): AncestorClassification {
const initPath = path.get('init');
if (!initPath.node) return 'unknown';
if (initPath.isArrowFunctionExpression() || initPath.isFunctionExpression()) {
return analyzeFunctionComponent(initPath, context);
}
if (initPath.isCallExpression()) {
return analyzeCallWrappedComponent(initPath, context);
}
if (initPath.isIdentifier()) {
const aliasBinding = path.scope.getBinding(initPath.node.name);
if (!aliasBinding) return 'unknown';
return classifyBindingAsAncestor(aliasBinding, context);
}
return 'unknown';
}
function analyzeCallWrappedComponent(
path: NodePath,
context: AncestorAnalysisContext
): AncestorClassification {
if (!isReactMemoOrForwardRefCall(path)) return 'unknown';
const [firstArgumentPath] = path.get('arguments');
if (!firstArgumentPath?.node) return 'unknown';
if (firstArgumentPath.isArrowFunctionExpression() || firstArgumentPath.isFunctionExpression()) {
return analyzeFunctionComponent(firstArgumentPath, context);
}
if (firstArgumentPath.isIdentifier()) {
const wrappedComponentBinding = path.scope.getBinding(firstArgumentPath.node.name);
if (!wrappedComponentBinding) return 'unknown';
return classifyBindingAsAncestor(wrappedComponentBinding, context);
}
if (firstArgumentPath.isCallExpression()) {
return analyzeCallWrappedComponent(firstArgumentPath, context);
}
return 'unknown';
}
function isReactMemoOrForwardRefCall(path: NodePath): boolean {
const calleePath = path.get('callee');
if (calleePath.isIdentifier()) {
if (!isMemoOrForwardRefName(calleePath.node.name)) return false;
const binding = path.scope.getBinding(calleePath.node.name);
return isReactImportBinding(binding);
}
if (calleePath.isMemberExpression()) {
const objectPath = calleePath.get('object');
const propertyPath = calleePath.get('property');
if (!objectPath.isIdentifier() || !propertyPath.isIdentifier()) return false;
if (!isMemoOrForwardRefName(propertyPath.node.name)) return false;
const objectBinding = path.scope.getBinding(objectPath.node.name);
return isReactImportBinding(objectBinding);
}
return false;
}
function isMemoOrForwardRefName(name: string): boolean {
return name === 'memo' || name === 'forwardRef';
}
function isReactImportBinding(binding: ScopeBinding | undefined): binding is ScopeBinding {
if (!binding || binding.kind !== 'module') return false;
const importDeclaration = binding.path.parent;
return t.isImportDeclaration(importDeclaration) && importDeclaration.source.value === 'react';
}
function analyzeFunctionComponent(
path: NodePath,
context: AncestorAnalysisContext
): AncestorClassification {
const bodyPath = path.get('body');
if (!bodyPath.isBlockStatement()) {
return analyzeRenderExpression(bodyPath as NodePath, 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);
}
if (path.isMemberExpression() && isPropsChildrenMemberExpression(path.node)) {
return 'safe';
}
if (
path.isNullLiteral() ||
path.isBooleanLiteral() ||
path.isNumericLiteral() ||
path.isStringLiteral() ||
path.isBigIntLiteral()
) {
return 'safe';
}
return 'unknown';
}
function analyzeJSXChildren(
children: Array>,
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');
}
if (classification === 'text') {
return classification;
}
}
return classification;
}
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';
}
if (!binding.path.isVariableDeclarator()) return 'unknown';
const cacheKey = binding.path.node;
if (context.renderExpressionInProgress.has(cacheKey)) {
return '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;
}
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 mergeAncestorClassification(
current: AncestorClassification,
next: AncestorClassification
): AncestorClassification {
if (current === 'text' || next === 'text') return 'text';
if (current === 'unknown' || next === 'unknown') return 'unknown';
return 'safe';
}
function getImportSpecifierImportedName(specifier: t.ImportSpecifier): string | undefined {
if (t.isIdentifier(specifier.imported)) {
return specifier.imported.name;
}
if (t.isStringLiteral(specifier.imported)) {
return specifier.imported.value;
}
return undefined;
}
/**
* Checks whether the closest JSX element ancestor is expo-router Link with a truthy asChild prop.
*
* We only bail on Text optimization when Link is effectively slotting that Text as the clickable child.
*/
export const hasExpoRouterLinkParentWithAsChild = (path: NodePath): boolean => {
const textElementPath = path.parentPath;
if (!textElementPath.isJSXElement()) return false;
let ancestorPath: NodePath | null = textElementPath.parentPath;
while (ancestorPath) {
if (ancestorPath.isJSXElement()) {
if (!isExpoRouterLinkElement(ancestorPath)) return false;
return hasTruthyAsChildAttribute(ancestorPath.node.openingElement.attributes);
}
ancestorPath = ancestorPath.parentPath;
}
return false;
};
function isExpoRouterLinkElement(path: NodePath): boolean {
const openingElementName = path.node.openingElement.name;
if (t.isJSXIdentifier(openingElementName)) {
const binding = path.scope.getBinding(openingElementName.name);
if (!binding || binding.kind !== 'module') return false;
if (!t.isImportSpecifier(binding.path.node)) return false;
const importDeclaration = binding.path.parent;
if (!t.isImportDeclaration(importDeclaration) || importDeclaration.source.value !== 'expo-router') return false;
const imported = binding.path.node.imported;
return t.isIdentifier(imported, { name: 'Link' }) || (t.isStringLiteral(imported) && imported.value === 'Link');
}
if (t.isJSXMemberExpression(openingElementName)) {
if (!t.isJSXIdentifier(openingElementName.object)) return false;
if (!t.isJSXIdentifier(openingElementName.property, { name: 'Link' })) return false;
const namespaceBinding = path.scope.getBinding(openingElementName.object.name);
if (!namespaceBinding || namespaceBinding.kind !== 'module') return false;
if (!t.isImportNamespaceSpecifier(namespaceBinding.path.node)) return false;
const importDeclaration = namespaceBinding.path.parent;
return t.isImportDeclaration(importDeclaration) && importDeclaration.source.value === 'expo-router';
}
return false;
}
function hasTruthyAsChildAttribute(attributes: (t.JSXAttribute | t.JSXSpreadAttribute)[]): boolean {
let asChildAttribute: t.JSXAttribute | undefined;
for (const attribute of attributes) {
if (t.isJSXAttribute(attribute) && t.isJSXIdentifier(attribute.name, { name: 'asChild' })) {
asChildAttribute = attribute;
}
}
if (!asChildAttribute) return false;
return isJSXAttributeValueTruthy(asChildAttribute.value);
}
function isJSXAttributeValueTruthy(value: t.JSXAttribute['value']): boolean {
if (!value) return true;
if (t.isStringLiteral(value)) return value.value.length > 0;
if (t.isJSXElement(value) || t.isJSXFragment(value)) return true;
if (t.isJSXExpressionContainer(value)) {
const staticTruthiness = getStaticExpressionTruthiness(value.expression);
return staticTruthiness ?? true;
}
return true;
}
function getStaticExpressionTruthiness(expression: t.Expression | t.JSXEmptyExpression): boolean | undefined {
if (t.isJSXEmptyExpression(expression)) return false;
if (t.isBooleanLiteral(expression)) return expression.value;
if (t.isNullLiteral(expression)) return false;
if (t.isStringLiteral(expression)) return expression.value.length > 0;
if (t.isNumericLiteral(expression)) return expression.value !== 0 && !Number.isNaN(expression.value);
if (t.isBigIntLiteral(expression)) return expression.value !== '0';
if (t.isIdentifier(expression, { name: 'undefined' })) return false;
if (t.isTemplateLiteral(expression) && expression.expressions.length === 0) {
return (expression.quasis[0]?.value.cooked ?? '').length > 0;
}
if (t.isUnaryExpression(expression, { operator: '!' })) {
const staticTruthiness = getStaticExpressionTruthiness(expression.argument);
return staticTruthiness === undefined ? undefined : !staticTruthiness;
}
return undefined;
}
================================================
FILE: packages/react-native-boost/src/plugin/utils/constants.ts
================================================
export const RUNTIME_MODULE_NAME = 'react-native-boost/runtime';
/**
* The set of accessibility properties that need to be normalized.
*/
export const ACCESSIBILITY_PROPERTIES = new Set([
'accessibilityLabel',
'aria-label',
'accessibilityState',
'aria-busy',
'aria-checked',
'aria-disabled',
'aria-expanded',
'aria-selected',
'accessible',
]);
// Maps the `userSelect` values to the corresponding boolean for the `selectable` prop
export const USER_SELECT_STYLE_TO_SELECTABLE_PROP: Record = {
auto: true,
text: true,
none: false,
contain: true,
all: true,
};
================================================
FILE: packages/react-native-boost/src/plugin/utils/format-test-result.ts
================================================
import type { ResultFormatter } from 'babel-plugin-tester';
import { format, type FormatOptions } from 'oxfmt';
import oxfmtConfig from '../../../../../.oxfmtrc.json';
type RootOxfmtConfig = FormatOptions & {
$schema?: string;
ignorePatterns?: string[];
};
const oxfmtOptions = buildOxfmtOptions(oxfmtConfig as RootOxfmtConfig);
export const formatTestResult: ResultFormatter = async (code, options) => {
const filepath = options?.filepath ?? 'output.js';
const result = await format(filepath, code, oxfmtOptions);
if (result.errors.length > 0) {
throw new Error(result.errors[0].message);
}
return result.code;
};
function buildOxfmtOptions(config: RootOxfmtConfig): FormatOptions {
const { $schema, ignorePatterns, ...oxfmtOptions } = config;
void $schema;
void ignorePatterns;
return oxfmtOptions;
}
================================================
FILE: packages/react-native-boost/src/plugin/utils/generate-test-plugin.ts
================================================
import { declare } from '@babel/helper-plugin-utils';
import { Optimizer, PluginOptions } from '../types';
import { createLogger } from './logger';
export const generateTestPlugin = (optimizer: Optimizer, options: PluginOptions = {}) => {
const logger = createLogger({
verbose: false,
silent: true,
});
return declare((api) => {
api.assertVersion(7);
return {
name: 'react-native-boost',
visitor: {
JSXOpeningElement(path) {
optimizer(path, logger, options);
},
},
};
});
};
================================================
FILE: packages/react-native-boost/src/plugin/utils/helpers.ts
================================================
export const ensureArray = (value: T | T[]): T[] => {
if (Array.isArray(value)) return value;
return [value];
};
export type BailoutCheck = {
reason: string;
shouldBail: () => boolean;
};
export const getFirstBailoutReason = (checks: readonly BailoutCheck[]): string | null => {
for (const check of checks) {
if (check.shouldBail()) {
return check.reason;
}
}
return null;
};
================================================
FILE: packages/react-native-boost/src/plugin/utils/logger.ts
================================================
import {
HubFile,
OptimizationLogPayload,
PluginLogger,
SkippedOptimizationLogPayload,
WarningLogPayload,
} from '../types';
const LOG_PREFIX = '[react-native-boost]';
const ANSI_RESET = '\u001B[0m';
const ANSI_GREEN = '\u001B[32m';
const ANSI_YELLOW = '\u001B[33m';
const ANSI_MAGENTA = '\u001B[35m';
const ANSI_RED = '\u001B[31m';
export const noopLogger: PluginLogger = {
optimized() {},
skipped() {},
forced() {},
warning() {},
};
export const createLogger = ({ verbose, silent }: { verbose: boolean; silent: boolean }): PluginLogger => {
if (silent) return noopLogger;
return {
optimized(payload) {
writeLog('optimized', `Optimized ${payload.component} in ${formatPathLocation(payload.path)}`);
},
skipped(payload) {
if (!verbose) return;
writeLog('skipped', `Skipped ${payload.component} in ${formatPathLocation(payload.path)} (${payload.reason})`);
},
forced(payload) {
writeLog(
'forced',
`Force-optimized ${payload.component} in ${formatPathLocation(payload.path)} (skipped bailout: ${payload.reason})`
);
},
warning(payload) {
const context = formatWarningContext(payload);
const message = context.length > 0 ? `${context}: ${payload.message}` : payload.message;
writeLog('warning', message);
},
};
};
function formatWarningContext(payload: WarningLogPayload): string {
const location = formatPathLocation(payload.path);
if (payload.component && location.length > 0) {
return `${payload.component} in ${location}`;
}
if (payload.component) {
return payload.component;
}
return location;
}
type LogLevel = 'optimized' | 'skipped' | 'forced' | 'warning';
function writeLog(level: LogLevel, message: string): void {
const levelTag = formatLevel(level);
console.log(`${LOG_PREFIX} ${levelTag} ${message}`);
}
function formatLevel(level: LogLevel): string {
if (level === 'optimized') {
return colorize('[optimized]', ANSI_GREEN);
}
if (level === 'skipped') {
return colorize('[skipped]', ANSI_YELLOW);
}
if (level === 'forced') {
return colorize('[forced]', ANSI_RED);
}
return colorize('[warning]', ANSI_MAGENTA);
}
function colorize(value: string, colorCode: string): string {
if (!shouldUseColor()) return value;
return `${colorCode}${value}${ANSI_RESET}`;
}
function shouldUseColor(): boolean {
if (process.env.NO_COLOR != null) return false;
if (process.env.FORCE_COLOR === '0') return false;
if (process.env.FORCE_COLOR != null) return true;
if (process.env.CLICOLOR === '0') return false;
if (process.env.CLICOLOR_FORCE != null && process.env.CLICOLOR_FORCE !== '0') return true;
if (process.stdout?.isTTY === true || process.stderr?.isTTY === true) {
return true;
}
const colorTerm = process.env.COLORTERM;
if (colorTerm != null && colorTerm !== '') {
return true;
}
const term = process.env.TERM;
return term != null && term !== '' && term.toLowerCase() !== 'dumb';
}
function formatPathLocation(
payloadPath: OptimizationLogPayload['path'] | SkippedOptimizationLogPayload['path'] | undefined
): string {
if (!payloadPath) return 'unknown file:unknown line';
const hub = payloadPath.hub as unknown;
const file = typeof hub === 'object' && hub !== null && 'file' in hub ? (hub.file as HubFile) : undefined;
const filename = file?.opts?.filename ?? 'unknown file';
const lineNumber = payloadPath.node.loc?.start.line ?? 'unknown line';
return `${filename}:${lineNumber}`;
}
================================================
FILE: packages/react-native-boost/src/plugin/utils/plugin-error.ts
================================================
export default class PluginError extends Error {
constructor(message: string) {
super(`[react-native-boost] Babel plugin exception: ${message}`);
this.name = 'PluginError';
}
}
================================================
FILE: packages/react-native-boost/src/runtime/__tests__/index.test.ts
================================================
import { vi, describe, it, expect } from 'vitest';
import {
processTextStyle,
processAccessibilityProps,
userSelectToSelectableMap,
verticalAlignToTextAlignVerticalMap,
} from '..';
import { TextStyle } from 'react-native';
vi.mock('../components/native-text', () => ({
NativeText: () => 'MockedNativeText',
}));
vi.mock('../components/native-view', () => ({
NativeView: () => 'MockedNativeView',
}));
vi.mock('react-native', () => ({
View: () => 'View',
Text: () => 'Text',
Platform: {
OS: 'ios',
},
StyleSheet: {
flatten: (style: any) => style,
},
}));
describe('processTextStyle', () => {
it('returns empty object for falsy style', () => {
expect(processTextStyle(null)).toEqual({});
expect(processTextStyle()).toEqual({});
});
it('caches computed props', () => {
const style = { color: 'red' };
const result1 = processTextStyle(style);
const result2 = processTextStyle(style);
expect(result1).toBe(result2);
});
it('converts numeric fontWeight to string', () => {
const style = { fontWeight: 400 } as const;
const result = processTextStyle(style);
expect(result.style).toBeDefined();
expect(result.style).toBeInstanceOf(Object);
expect((result.style as TextStyle).fontWeight).toBe('400');
});
it('maps userSelect to selectable and removes userSelect from style', () => {
const style = { userSelect: 'none', color: 'blue' } as const;
const result = processTextStyle(style);
expect(result.selectable).toBe(userSelectToSelectableMap['none']);
expect(result.style).toBeDefined();
expect(result.style).toBeInstanceOf(Object);
expect((result.style as TextStyle).userSelect).toBeUndefined();
expect((result.style as TextStyle).color).toBe('blue');
});
it('maps verticalAlign to textAlignVertical and removes verticalAlign from style', () => {
const style = { verticalAlign: 'top', fontSize: 16 } as const;
const result = processTextStyle(style);
expect(result.style).toBeDefined();
expect(result.style).toBeInstanceOf(Object);
expect((result.style as TextStyle).textAlignVertical).toBe(verticalAlignToTextAlignVerticalMap['top']);
expect((result.style as TextStyle).verticalAlign).toBeUndefined();
});
it('handles combination of properties', () => {
const style = {
fontWeight: 700,
userSelect: 'auto',
verticalAlign: 'middle',
margin: 10,
} as const;
const result = processTextStyle(style);
expect(result.style).toBeDefined();
expect(result.style).toBeInstanceOf(Object);
expect((result.style as TextStyle).fontWeight).toBe('700');
expect(result.selectable).toBe(userSelectToSelectableMap['auto']);
expect((result.style as TextStyle).textAlignVertical).toBe(verticalAlignToTextAlignVerticalMap['middle']);
expect((result.style as TextStyle).margin).toBe(10);
expect((result.style as TextStyle).userSelect).toBeUndefined();
expect((result.style as TextStyle).verticalAlign).toBeUndefined();
});
});
describe('processAccessibilityProps', () => {
it('sets default accessible to true and has no accessibilityLabel if not provided', () => {
const props = {};
const normalized = processAccessibilityProps(props);
expect(normalized.accessible).toBe(true);
expect(normalized.accessibilityLabel).toBeUndefined();
expect(normalized.accessibilityState).toBeUndefined();
});
it('merges accessibility labels using aria-label over accessibilityLabel', () => {
const props = {
'accessibilityLabel': 'Label one',
'aria-label': 'Label two',
};
const normalized = processAccessibilityProps(props);
expect(normalized.accessibilityLabel).toBe('Label two');
});
it('keeps accessibilityLabel if aria-label is not provided', () => {
const props = {
accessibilityLabel: 'Only label',
};
const normalized = processAccessibilityProps(props);
expect(normalized.accessibilityLabel).toBe('Only label');
});
it('creates accessibilityState from ARIA properties when accessibilityState is not provided', () => {
const props = {
'aria-busy': true,
'aria-disabled': false,
'aria-selected': true,
};
const normalized = processAccessibilityProps(props);
expect(normalized.accessibilityState).toEqual({
busy: true,
checked: undefined,
disabled: false,
expanded: undefined,
selected: true,
});
});
it('merges ARIA properties with existing accessibilityState', () => {
const props = {
'accessibilityState': { busy: false, checked: false },
'aria-busy': true, // should override busy
'aria-disabled': true, // new property
};
const normalized = processAccessibilityProps(props);
expect(normalized.accessibilityState).toEqual({
busy: true,
checked: false,
disabled: true,
expanded: undefined,
selected: undefined,
});
});
it('retains additional properties', () => {
const props = {
'foo': 'bar',
'aria-expanded': false,
};
const normalized = processAccessibilityProps(props);
expect(normalized.foo).toBe('bar');
expect(normalized.accessibilityState).toEqual({
busy: undefined,
checked: undefined,
disabled: undefined,
expanded: false,
selected: undefined,
});
});
it('uses provided accessible if it exists', () => {
const props = {
accessible: false,
};
const normalized = processAccessibilityProps(props);
expect(normalized.accessible).toBe(false);
});
});
================================================
FILE: packages/react-native-boost/src/runtime/__tests__/mocks/react-native.ts
================================================
export const View = () => 'View';
export const Text = () => 'Text';
export const Platform = {
OS: 'ios',
};
export const StyleSheet = {
flatten: (style: T) => style,
};
================================================
FILE: packages/react-native-boost/src/runtime/components/native-text.tsx
================================================
/* eslint-disable @typescript-eslint/no-require-imports,unicorn/prefer-module */
import type { ComponentType } from 'react';
import type { TextProps } from 'react-native';
const reactNative = require('react-native');
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;
================================================
FILE: packages/react-native-boost/src/runtime/components/native-view.tsx
================================================
/* eslint-disable @typescript-eslint/no-require-imports,unicorn/prefer-module */
import type { ComponentType } from 'react';
import type { ViewProps } from 'react-native';
const reactNative = require('react-native');
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;
================================================
FILE: packages/react-native-boost/src/runtime/index.ts
================================================
import { TextProps, TextStyle, StyleSheet } from 'react-native';
import { GenericStyleProp } from './types';
import { userSelectToSelectableMap, verticalAlignToTextAlignVerticalMap } from './utils/constants';
const propsCache = new WeakMap();
/**
* 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.
* @remarks
* - Flattens style arrays via `StyleSheet.flatten`
* - Converts numeric `fontWeight` values to string values
* - Maps `userSelect` and `verticalAlign` to native-compatible props
*/
export function processTextStyle(style: GenericStyleProp): Partial {
if (!style) return {};
// Cache the computed props
let props = propsCache.get(style);
if (props) return props;
props = {};
propsCache.set(style, props);
style = StyleSheet.flatten(style) as TextStyle;
if (!style) return {};
if (typeof style?.fontWeight === 'number') {
style.fontWeight = style.fontWeight.toString() as TextStyle['fontWeight'];
}
if (style?.userSelect != null) {
props.selectable = userSelectToSelectableMap[style.userSelect];
delete style.userSelect;
}
if (style?.verticalAlign != null) {
style.textAlignVertical = verticalAlignToTextAlignVerticalMap[
style.verticalAlign
] as TextStyle['textAlignVertical'];
delete style.verticalAlign;
}
props.style = style;
return props;
}
/**
* Normalizes accessibility and ARIA props for runtime native components.
*
* @param props - Accessibility and ARIA props.
* @returns Props with normalized accessibility fields.
* @remarks
* - Merges `aria-label` with `accessibilityLabel`
* - Merges ARIA state fields into `accessibilityState`
* - Defaults `accessible` to `true` when omitted
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function processAccessibilityProps(props: Record): Record {
const {
accessibilityLabel,
['aria-label']: ariaLabel,
accessibilityState,
['aria-busy']: ariaBusy,
['aria-checked']: ariaChecked,
['aria-disabled']: ariaDisabled,
['aria-expanded']: ariaExpanded,
['aria-selected']: ariaSelected,
accessible,
...restProperties
} = props;
// Merge label props: prefer the aria-label if defined.
const normalizedLabel = ariaLabel ?? accessibilityLabel;
// Merge the accessibilityState with any provided ARIA properties.
let normalizedState = accessibilityState;
if (ariaBusy != null || ariaChecked != null || ariaDisabled != null || ariaExpanded != null || ariaSelected != null) {
normalizedState =
normalizedState == null
? {
busy: ariaBusy,
checked: ariaChecked,
disabled: ariaDisabled,
expanded: ariaExpanded,
selected: ariaSelected,
}
: {
busy: ariaBusy ?? normalizedState.busy,
checked: ariaChecked ?? normalizedState.checked,
disabled: ariaDisabled ?? normalizedState.disabled,
expanded: ariaExpanded ?? normalizedState.expanded,
selected: ariaSelected ?? normalizedState.selected,
};
}
// For the accessible prop, if not provided, default to `true`
const normalizedAccessible = accessible == null ? true : accessible;
return {
...restProperties,
accessibilityLabel: normalizedLabel,
accessibilityState: normalizedState,
accessible: normalizedAccessible,
};
}
export * from './types';
export * from './utils/constants';
export * from './components/native-text';
export * from './components/native-view';
================================================
FILE: packages/react-native-boost/src/runtime/index.web.ts
================================================
// 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 { GenericStyleProp } from './types';
export const processTextStyle = (style: GenericStyleProp) => ({ style }) as Partial;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function processAccessibilityProps(props: Record): Record {
return props;
}
export * from './types';
export * from './utils/constants';
// On Web, the native components are not available, so we use the standard components that'll be replaced by their DOM
// equivalents by react-native-web.
/* eslint-disable @typescript-eslint/no-require-imports,unicorn/prefer-module */
export const NativeText = require('react-native').Text;
export const NativeView = require('react-native').View;
/* eslint-enable @typescript-eslint/no-require-imports,unicorn/prefer-module */
================================================
FILE: packages/react-native-boost/src/runtime/types/index.ts
================================================
/**
* Recursive style prop shape accepted by runtime style helpers.
*
* @template T - Style object type.
*/
export type GenericStyleProp = null | void | T | false | '' | ReadonlyArray>;
================================================
FILE: packages/react-native-boost/src/runtime/types/react-native.d.ts
================================================
declare module 'react-native/Libraries/Text/TextNativeComponent' {
export const NativeText: React.ComponentType;
}
declare module 'react-native/Libraries/Components/View/ViewNativeComponent' {
export default React.ComponentType;
}
================================================
FILE: packages/react-native-boost/src/runtime/utils/constants.ts
================================================
/**
* Maps CSS-like `userSelect` values to React Native's `selectable` prop.
*/
export const userSelectToSelectableMap = {
auto: true,
text: true,
none: false,
contain: true,
all: true,
};
/**
* Maps CSS-like `verticalAlign` values to React Native's `textAlignVertical`.
*/
export const verticalAlignToTextAlignVerticalMap = {
auto: 'auto',
top: 'top',
bottom: 'bottom',
middle: 'center',
};
================================================
FILE: packages/react-native-boost/tsconfig.build.json
================================================
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"noEmit": false,
"outDir": "./dist",
"emitDeclarationOnly": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "plugin", "fixtures", "scripts"]
}
================================================
FILE: packages/react-native-boost/tsconfig.json
================================================
{
"extends": "../../tsconfig.json",
"compilerOptions": {},
"include": ["src/**/*"],
"exclude": ["node_modules", "plugin", "fixtures", "scripts"]
}
================================================
FILE: packages/react-native-boost/vitest.config.ts
================================================
import { fileURLToPath } from 'node:url';
import { resolve } from 'node:path';
import { defineConfig } from 'vitest/config';
const runtimeMockPath = fileURLToPath(new URL('src/runtime/__tests__/mocks/react-native.ts', import.meta.url));
export default defineConfig({
test: {
// babel-plugin-tester requires it and describe to be set globally
globals: true,
},
resolve: {
alias: {
'react-native': resolve(runtimeMockPath),
},
},
});
================================================
FILE: packages/react-native-time-to-render/.gitignore
================================================
/android/build
================================================
FILE: packages/react-native-time-to-render/LICENSE
================================================
MIT License
Copyright (c) Kuatsu App Agency
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: packages/react-native-time-to-render/README.md
================================================
# Time to Render Benchmarking Module
This module is used by the React Native Boost example app to create render benchmarks.
## Attribution
The module was taken from the `RTNTimeToRender` module by [@sammy-SC](https://github.com/sammy-SC) from [react-native-community/RNNewArchitectureApp](https://github.com/react-native-community/RNNewArchitectureApp/tree/new-architecture-benchmarks).
================================================
FILE: packages/react-native-time-to-render/TimeToRender.podspec
================================================
require "json"
package = JSON.parse(File.read(File.join(__dir__, "package.json")))
folly_compiler_flags = '-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1 -Wno-comma -Wno-shorten-64-to-32'
Pod::Spec.new do |s|
s.name = "TimeToRender"
s.version = package["version"]
s.summary = package["description"]
s.homepage = package["homepage"]
s.license = package["license"]
s.authors = package["author"]
s.platforms = { :ios => min_ios_version_supported }
s.source = { :git => ".git", :tag => "#{s.version}" }
s.source_files = "ios/**/*.{h,m,mm,cpp}"
s.private_header_files = "ios/generated/**/*.h"
# Use install_modules_dependencies helper to install the dependencies if React Native version >=0.71.0.
# See https://github.com/facebook/react-native/blob/febf6b7f33fdb4904669f99d795eba4c0f95d7bf/scripts/cocoapods/new_architecture.rb#L79.
if respond_to?(:install_modules_dependencies, true)
install_modules_dependencies(s)
else
s.dependency "React-Core"
# Don't install the dependencies when we run `pod install` in the old architecture.
if ENV['RCT_NEW_ARCH_ENABLED'] == '1' then
s.compiler_flags = folly_compiler_flags + " -DRCT_NEW_ARCH_ENABLED=1"
s.pod_target_xcconfig = {
"HEADER_SEARCH_PATHS" => "\"$(PODS_ROOT)/boost\"",
"OTHER_CPLUSPLUSFLAGS" => "-DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1",
"CLANG_CXX_LANGUAGE_STANDARD" => "c++17"
}
s.dependency "React-RCTFabric"
s.dependency "React-Codegen"
s.dependency "RCT-Folly"
s.dependency "RCTRequired"
s.dependency "RCTTypeSafety"
s.dependency "ReactCommon/turbomodule/core"
end
end
end
================================================
FILE: packages/react-native-time-to-render/android/build.gradle
================================================
buildscript {
ext.getExtOrDefault = {name ->
return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties['TimeToRender_' + name]
}
repositories {
google()
mavenCentral()
}
dependencies {
classpath "com.android.tools.build:gradle:8.7.2"
// noinspection DifferentKotlinGradleVersion
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${getExtOrDefault('kotlinVersion')}"
}
}
def isNewArchitectureEnabled() {
return rootProject.hasProperty("newArchEnabled") && rootProject.getProperty("newArchEnabled") == "true"
}
apply plugin: "com.android.library"
apply plugin: "kotlin-android"
if (isNewArchitectureEnabled()) {
apply plugin: "com.facebook.react"
}
def getExtOrIntegerDefault(name) {
return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["TimeToRender_" + name]).toInteger()
}
def supportsNamespace() {
def parsed = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION.tokenize('.')
def major = parsed[0].toInteger()
def minor = parsed[1].toInteger()
// Namespace support was added in 7.3.0
return (major == 7 && minor >= 3) || major >= 8
}
android {
if (supportsNamespace()) {
namespace "com.timetorender"
sourceSets {
main {
manifest.srcFile "src/main/AndroidManifestNew.xml"
}
}
}
compileSdkVersion getExtOrIntegerDefault("compileSdkVersion")
defaultConfig {
minSdkVersion getExtOrIntegerDefault("minSdkVersion")
targetSdkVersion getExtOrIntegerDefault("targetSdkVersion")
buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString()
}
buildFeatures {
buildConfig true
}
buildTypes {
release {
minifyEnabled false
}
}
lintOptions {
disable "GradleCompatible"
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
sourceSets {
main {
if (isNewArchitectureEnabled()) {
java.srcDirs += [
"generated/java",
"generated/jni"
]
}
}
}
}
repositories {
mavenCentral()
google()
}
def kotlin_version = getExtOrDefault("kotlinVersion")
dependencies {
implementation "com.facebook.react:react-android"
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
}
if (isNewArchitectureEnabled()) {
react {
jsRootDir = file("../src/")
libraryName = "TimeToRender"
codegenJavaPackageName = "com.timetorender"
}
}
================================================
FILE: packages/react-native-time-to-render/android/gradle.properties
================================================
TimeToRender_kotlinVersion=2.0.21
TimeToRender_minSdkVersion=24
TimeToRender_targetSdkVersion=34
TimeToRender_compileSdkVersion=35
TimeToRender_ndkVersion=27.1.12297006
================================================
FILE: packages/react-native-time-to-render/android/src/main/AndroidManifest.xml
================================================
================================================
FILE: packages/react-native-time-to-render/android/src/main/AndroidManifestNew.xml
================================================
================================================
FILE: packages/react-native-time-to-render/android/src/main/java/com/timetorender/MarkerStore.kt
================================================
package com.timetorender
import java.util.Date
import kotlin.collections.mutableMapOf
import android.os.SystemClock
class MarkerStore(private val markers: MutableMap = mutableMapOf()) {
companion object {
val mainStore = MarkerStore()
fun JSTimeIntervalSinceStartup() = SystemClock.uptimeMillis()
}
fun startMarker(marker: String, timeSinceStartup: Long) {
markers.put(marker, timeSinceStartup)
}
fun endMarker(marker: String) : Long {
val markerStart = markers[marker]
val markerEnd = JSTimeIntervalSinceStartup()
markers.remove(marker)
return markerEnd - markerStart!!
}
}
================================================
FILE: packages/react-native-time-to-render/android/src/main/java/com/timetorender/OnMarkerPaintedEvent.kt
================================================
package com.timetorender
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.WritableMap
import com.facebook.react.uimanager.events.Event
internal class OnMarkerPaintedEvent(surfaceId: Int, viewId: Int, val paintTime: Long) : Event(surfaceId, viewId) {
@Deprecated("")
constructor(viewId: Int, paintTime: Long) : this(-1, viewId, paintTime) {
}
override fun getEventName() = EVENT_NAME
override protected fun getEventData(): WritableMap {
val eventData: WritableMap = Arguments.createMap()
eventData.putDouble("paintTime", paintTime.toDouble())
eventData.putInt("target", getViewTag())
return eventData
}
companion object {
const val EVENT_NAME = "topMarkerPainted"
}
}
================================================
FILE: packages/react-native-time-to-render/android/src/main/java/com/timetorender/TimeToRenderModule.kt
================================================
package com.timetorender
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
import com.timetorender.NativeTimeToRenderSpec
class TimeToRenderModule(reactContext: ReactApplicationContext) : NativeTimeToRenderSpec(reactContext) {
override fun getName() = NAME
override fun startMarker(name: String, time: Double) {
MarkerStore.mainStore.startMarker(name, time.toLong())
}
companion object {
const val NAME = "TimeToRender"
}
}
================================================
FILE: packages/react-native-time-to-render/android/src/main/java/com/timetorender/TimeToRenderPackage.kt
================================================
package com.timetorender;
import com.facebook.react.BaseReactPackage
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.module.model.ReactModuleInfo;
import com.facebook.react.module.model.ReactModuleInfoProvider
import kotlin.collections.listOf
import com.facebook.react.uimanager.ViewManager
class TimeToRenderPackage : BaseReactPackage() {
override fun createViewManagers(reactContext: ReactApplicationContext): List> =
listOf(TimeToRenderViewManager(reactContext))
override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? =
if (name == TimeToRenderModule.NAME) {
TimeToRenderModule(reactContext)
} else {
null
}
override fun getReactModuleInfoProvider() = ReactModuleInfoProvider {
mapOf(
TimeToRenderModule.NAME to ReactModuleInfo(
TimeToRenderModule.NAME,
TimeToRenderModule.NAME,
false, // canOverrideExistingModule
false, // needsEagerInit
true, // hasConstants
false, // isCxxModule
true // isTurboModule
)
)
}
}
================================================
FILE: packages/react-native-time-to-render/android/src/main/java/com/timetorender/TimeToRenderView.kt
================================================
package com.timetorender
import android.content.Context
import android.graphics.Canvas
import android.util.AttributeSet
import android.view.View
import com.facebook.react.bridge.ReactContext
import com.facebook.react.uimanager.UIManagerHelper
class TimeToRenderView : View {
var _alreadyLogged = false
var _markerName: String? = ""
constructor(context: Context?) : super(context) {
configureComponent()
}
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) {
configureComponent()
}
constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
configureComponent()
}
fun setMarkerName(markerName: String?) {
_markerName = markerName
}
private fun configureComponent() {
// do nothing
}
override protected fun onDraw(canvas: Canvas) {
if (getParent() != null && !_alreadyLogged) {
_alreadyLogged = true
val paintTime = MarkerStore.mainStore.endMarker(_markerName!!)
android.util.Log.e("DAVID", "Logging end of marker: $paintTime")
val reactContext: ReactContext = getContext() as ReactContext
val reactTag: Int = getId()
UIManagerHelper.getEventDispatcherForReactTag(reactContext, reactTag)
?.dispatchEvent(
OnMarkerPaintedEvent(
UIManagerHelper.getSurfaceId(reactContext), reactTag, paintTime))
}
}
}
================================================
FILE: packages/react-native-time-to-render/android/src/main/java/com/timetorender/TimeToRenderViewManager.kt
================================================
package com.timetorender
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.module.annotations.ReactModule
import com.facebook.react.uimanager.SimpleViewManager
import com.facebook.react.uimanager.ThemedReactContext
import com.facebook.react.uimanager.ViewManagerDelegate
import com.facebook.react.uimanager.annotations.ReactProp
import com.facebook.react.viewmanagers.TimeToRenderManagerDelegate
import com.facebook.react.viewmanagers.TimeToRenderManagerInterface
import kotlin.collections.mapOf
import kotlin.collections.mutableMapOf
@ReactModule(name = TimeToRenderViewManager.NAME)
class TimeToRenderViewManager(context: ReactApplicationContext) : SimpleViewManager(), TimeToRenderManagerInterface {
// Documentation is incorrect in the next line:
private val delegate: ViewManagerDelegate = TimeToRenderManagerDelegate(this)
override fun getDelegate(): ViewManagerDelegate = delegate
override fun getName(): String = NAME
override fun createViewInstance(context: ThemedReactContext): TimeToRenderView = TimeToRenderView(context)
@ReactProp(name = "markerName")
override fun setMarkerName(view: TimeToRenderView, markerName: String?) {
view.setMarkerName(markerName)
}
companion object {
const val NAME = "TimeToRender"
}
override fun getExportedCustomDirectEventTypeConstants(): Map? {
val baseEventTypeConstants: Map? = super.getExportedCustomDirectEventTypeConstants()
val eventTypeConstants: MutableMap = baseEventTypeConstants?.toMutableMap()
?: mutableMapOf()
eventTypeConstants.put(
OnMarkerPaintedEvent.EVENT_NAME,
mutableMapOf("registrationName" to "onMarkerPainted"))
return eventTypeConstants
}
}
================================================
FILE: packages/react-native-time-to-render/ios/MarkerPaintComponentView.h
================================================
#import
#import
NS_ASSUME_NONNULL_BEGIN
@interface MarkerPaintComponentView : RCTViewComponentView
@end
NS_ASSUME_NONNULL_END
================================================
FILE: packages/react-native-time-to-render/ios/MarkerPaintComponentView.mm
================================================
#import "MarkerPaintComponentView.h"
#import "MarkerStore.h"
#import
#import
#import
#import
#import
#import "RCTFabricComponentsPlugins.h"
using namespace facebook::react;
@implementation MarkerPaintComponentView {
BOOL _alreadyLogged;
}
- (instancetype)initWithFrame:(CGRect)frame
{
if (self = [super initWithFrame:frame]) {
static const auto defaultProps = std::make_shared();
_props = defaultProps;
}
return self;
}
- (void)prepareForRecycle
{
[super prepareForRecycle];
_alreadyLogged = NO;
}
- (void)didMoveToWindow {
[super didMoveToWindow];
if (_alreadyLogged) {
return;
}
if (!self.window) {
return;
}
_alreadyLogged = YES;
NSString *markerName = RCTNSStringFromString(std::static_pointer_cast(_props)->markerName);
// However, we cannot do it right now: the views were just mounted but pixels
// were not drawn on the screen yet.
// They will be drawn for sure before the next tick of the main run loop.
// Let's wait for that and then report.
dispatch_async(dispatch_get_main_queue(), ^{
NSTimeInterval paintTime = [[MarkerStore mainStore] endMarker:markerName];
std::dynamic_pointer_cast(self->_eventEmitter)->onMarkerPainted({.paintTime = paintTime});
});
}
+ (ComponentDescriptorProvider)componentDescriptorProvider
{
return concreteComponentDescriptorProvider();
}
@end
Class TimeToRenderCls(void)
{
return MarkerPaintComponentView.class;
}
================================================
FILE: packages/react-native-time-to-render/ios/MarkerStore.h
================================================
#import
NS_ASSUME_NONNULL_BEGIN
@interface MarkerStore : NSObject
+ (id)mainStore;
+ (NSTimeInterval)JSTimeIntervalSinceStartup;
- (void)startMarker:(NSString *)marker timeSinceStartup:(NSTimeInterval)time;
- (NSTimeInterval)endMarker:(NSString *)marker;
@end
NS_ASSUME_NONNULL_END
================================================
FILE: packages/react-native-time-to-render/ios/MarkerStore.m
================================================
#import "MarkerStore.h"
@implementation MarkerStore {
NSMutableDictionary *_markers;
}
+ (id)mainStore {
static MarkerStore *mainMarkerStore = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
mainMarkerStore = [[self alloc] init];
});
return mainMarkerStore;
}
+ (NSTimeInterval)JSTimeIntervalSinceStartup {
return [[NSProcessInfo processInfo] systemUptime] * 1000;
}
- (id)init {
if (self = [super init]) {
_markers = [NSMutableDictionary new];
}
return self;
}
- (void)startMarker:(NSString *)marker timeSinceStartup:(NSTimeInterval)time {
[_markers setObject:@(time) forKey:marker];
}
- (NSTimeInterval)endMarker:(NSString *)marker {
NSTimeInterval markerStart = [_markers objectForKey:marker].doubleValue;
NSTimeInterval markerEnd = [MarkerStore JSTimeIntervalSinceStartup];
[_markers removeObjectForKey:marker];
return markerEnd - markerStart;
}
@end
================================================
FILE: packages/react-native-time-to-render/ios/PaintMarkerView.h
================================================
#import
#import
NS_ASSUME_NONNULL_BEGIN
@interface PaintMarkerView : UIView
@property (nonatomic, retain) NSString *markerName;
@property (nonatomic, copy) RCTDirectEventBlock onMarkerPainted;
@end
NS_ASSUME_NONNULL_END
================================================
FILE: packages/react-native-time-to-render/ios/PaintMarkerView.m
================================================
#import "PaintMarkerView.h"
#import "MarkerStore.h"
@implementation PaintMarkerView {
BOOL _alreadyLogged;
}
- (void)didMoveToWindow {
[super didMoveToWindow];
if (_alreadyLogged) {
return;
}
if (!self.window) {
return;
}
_alreadyLogged = YES;
// However, we cannot do it right now: the views were just mounted but pixels
// were not drawn on the screen yet.
// They will be drawn for sure before the next tick of the main run loop.
// Let's wait for that and then report.
dispatch_async(dispatch_get_main_queue(), ^{
NSTimeInterval paintTime = [[MarkerStore mainStore] endMarker:self.markerName];
self.onMarkerPainted(@{@"paintTime": @(paintTime)});
});
}
/*
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
- (void)drawRect:(CGRect)rect {
// Drawing code
}
*/
@end
================================================
FILE: packages/react-native-time-to-render/ios/TimeToRender.h
================================================
#import "RNTimeToRenderSpec/RNTimeToRenderSpec.h"
@interface TimeToRender : NSObject
@end
================================================
FILE: packages/react-native-time-to-render/ios/TimeToRender.mm
================================================
#import "TimeToRender.h"
#import "MarkerStore.h"
@implementation TimeToRender
RCT_EXPORT_MODULE()
- (void)startMarker:(NSString *)name time:(double)time {
[[MarkerStore mainStore] startMarker:name timeSinceStartup:time];
}
- (std::shared_ptr)getTurboModule:
(const facebook::react::ObjCTurboModule::InitParams &)params
{
return std::make_shared(params);
}
@end
================================================
FILE: packages/react-native-time-to-render/ios/TimeToRenderManager.h
================================================
#import
@interface TimeToRenderManager : RCTViewManager
@end
================================================
FILE: packages/react-native-time-to-render/ios/TimeToRenderManager.m
================================================
#import
#import "TimeToRenderManager.h"
#import "PaintMarkerView.h"
@implementation TimeToRenderManager
RCT_EXPORT_MODULE()
RCT_EXPORT_VIEW_PROPERTY(markerName, NSString)
RCT_EXPORT_VIEW_PROPERTY(onMarkerPainted, RCTDirectEventBlock)
- (UIView *)view
{
return [[PaintMarkerView alloc] init];
}
@end
================================================
FILE: packages/react-native-time-to-render/package.json
================================================
{
"name": "react-native-time-to-render",
"version": "0.0.1",
"description": "Benchmarks the time to render",
"private": true,
"main": "src/index",
"codegenConfig": {
"name": "RNTimeToRenderSpec",
"type": "all",
"jsSrcsDir": "src"
},
"author": "Kuatsu App Agency ",
"license": "MIT",
"homepage": "#readme",
"create-react-native-library": {
"type": "turbo-module",
"languages": "kotlin-objc",
"version": "0.48.2"
}
}
================================================
FILE: packages/react-native-time-to-render/react-native.config.js
================================================
/**
* @type {import('@react-native-community/cli-types').UserDependencyConfig}
*/
module.exports = {
dependency: {
platforms: {
android: {
cmakeListsPath: 'build/generated/source/codegen/jni/CMakeLists.txt',
},
},
},
};
================================================
FILE: packages/react-native-time-to-render/src/NativeTimeToRender.ts
================================================
import type { TurboModule } from 'react-native';
import { TurboModuleRegistry } from 'react-native';
export interface Spec extends TurboModule {
startMarker(name: string, time: number): void;
}
export default TurboModuleRegistry.getEnforcing('TimeToRender');
================================================
FILE: packages/react-native-time-to-render/src/TimeToRenderNativeComponent.ts
================================================
import { codegenNativeComponent, type ViewProps } from 'react-native';
import { DirectEventHandler, Double } from 'react-native/Libraries/Types/CodegenTypes';
export interface NativeProps extends ViewProps {
markerName: string;
onMarkerPainted: DirectEventHandler<{ paintTime: Double }>;
}
export default codegenNativeComponent('TimeToRender');
================================================
FILE: packages/react-native-time-to-render/src/index.tsx
================================================
import TimeToRender from './NativeTimeToRender';
export { default as TimeToRenderView } from './TimeToRenderNativeComponent';
export function startMarker(name: string, time: number): void {
return TimeToRender.startMarker(name, time);
}
================================================
FILE: pnpm-workspace.yaml
================================================
packages:
- apps/*
- packages/*
onlyBuiltDependencies:
- core-js
- core-js-pure
- deasync
- esbuild
- sharp
================================================
FILE: tsconfig.json
================================================
{
"compilerOptions": {
"lib": ["ESNext"],
"module": "esnext",
"moduleResolution": "bundler",
"jsx": "react-native",
"target": "es2019",
"declaration": true,
"outDir": "./dist",
"types": ["node"],
"skipLibCheck": true,
"strict": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"noEmit": true
}
}