├── .dockerignore ├── .eslintignore ├── .eslintrc.js ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── question.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── deploy.yml │ └── push.yml ├── .gitignore ├── .node-version ├── .prettierrc ├── @types ├── emotion.d.ts └── global.d.ts ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── README.md ├── __mocks__ └── react-github-btn.js ├── config-overrides.js ├── docker-compose.yml ├── jest.config.js ├── jest.setup.js ├── mise.toml ├── package.json ├── public ├── favicon.ico ├── index.html └── manifest.json ├── renovate.json ├── src ├── App.tsx ├── __fixtures__ │ └── releases │ │ └── 0.59.js ├── __tests__ │ ├── Home.e2e.spec.ts │ ├── __image_snapshots__ │ │ ├── home-e-2-e-spec-js-home-1-should-load-the-top-component-with-logo-and-versions-1-snap.png │ │ ├── home-e-2-e-spec-js-home-1-should-load-the-top-component-with-logo-versions-and-troubleshooting-guides-alert-1-snap.png │ │ ├── home-e-2-e-spec-js-home-10-should-collapse-first-file-in-diff-1-snap.png │ │ ├── home-e-2-e-spec-js-home-2-should-hide-the-troubleshooting-guides-alert-and-show-the-troubleshooting-guides-1-snap.png │ │ ├── home-e-2-e-spec-js-home-2-should-load-the-current-versions-1-snap.png │ │ ├── home-e-2-e-spec-js-home-3-should-hide-the-troubleshooting-guides-and-show-the-normal-header-1-snap.png │ │ ├── home-e-2-e-spec-js-home-3-should-select-a-current-version-1-snap.png │ │ ├── home-e-2-e-spec-js-home-4-should-load-the-current-versions-1-snap.png │ │ ├── home-e-2-e-spec-js-home-4-should-load-the-upgrading-versions-1-snap.png │ │ ├── home-e-2-e-spec-js-home-5-should-select-a-current-version-1-snap.png │ │ ├── home-e-2-e-spec-js-home-5-should-select-an-upgrading-version-1-snap.png │ │ ├── home-e-2-e-spec-js-home-6-should-load-the-upgrading-content-1-snap.png │ │ ├── home-e-2-e-spec-js-home-6-should-load-the-upgrading-versions-1-snap.png │ │ ├── home-e-2-e-spec-js-home-7-should-scroll-to-the-first-file-in-diff-1-snap.png │ │ ├── home-e-2-e-spec-js-home-7-should-select-an-upgrading-version-1-snap.png │ │ ├── home-e-2-e-spec-js-home-8-should-collapse-first-file-in-diff-1-snap.png │ │ ├── home-e-2-e-spec-js-home-8-should-load-the-upgrading-content-1-snap.png │ │ └── home-e-2-e-spec-js-home-9-should-scroll-to-the-first-file-in-diff-1-snap.png │ ├── components │ │ └── common │ │ │ ├── CompletedFilesCounter.spec.tsx │ │ │ └── Markdown.spec.tsx │ └── utils.spec.ts ├── assets │ ├── loading.svg │ └── logo.svg ├── components │ ├── common │ │ ├── AlignDepsAlert.tsx │ │ ├── BinaryDownload.tsx │ │ ├── CompletedFilesCounter.tsx │ │ ├── CopyFileButton.tsx │ │ ├── DarkModeButton.tsx │ │ ├── Diff │ │ │ ├── Diff.tsx │ │ │ ├── DiffComment.tsx │ │ │ ├── DiffCommentReminder.tsx │ │ │ ├── DiffHeader.tsx │ │ │ ├── DiffLoading.tsx │ │ │ ├── DiffSection.tsx │ │ │ └── DiffViewStyleOptions.tsx │ │ ├── DiffViewer.tsx │ │ ├── DownloadFileButton.tsx │ │ ├── Markdown.tsx │ │ ├── Select.test.tsx │ │ ├── Select.tsx │ │ ├── Settings.tsx │ │ ├── TroubleshootingGuides.tsx │ │ ├── TroubleshootingGuidesButton.tsx │ │ ├── UpgradeButton.tsx │ │ ├── UpgradeSupportAlert.tsx │ │ ├── UsefulContentSection.tsx │ │ ├── UsefulLinks.tsx │ │ ├── VersionSelector.tsx │ │ ├── ViewFileButton.tsx │ │ └── index.ts │ └── pages │ │ └── Home.tsx ├── constants.ts ├── hooks │ ├── fetch-diff.ts │ ├── fetch-release-versions.ts │ ├── get-language-from-url.ts │ └── get-package-name-from-url.ts ├── index.css ├── index.jsx ├── mocks │ ├── 0.63.2..0.64.2.diff │ └── repositoryInfo.json ├── releases │ ├── __mocks__ │ │ └── index.ts │ ├── index.js │ ├── react-native │ │ ├── 0.57.tsx │ │ ├── 0.58.tsx │ │ ├── 0.59.tsx │ │ ├── 0.60.tsx │ │ ├── 0.61.tsx │ │ ├── 0.62.tsx │ │ ├── 0.64.tsx │ │ ├── 0.68.tsx │ │ ├── 0.69.tsx │ │ ├── 0.71.tsx │ │ ├── 0.72.tsx │ │ ├── 0.73.tsx │ │ ├── 0.74.tsx │ │ └── 0.77.tsx │ └── types.d.ts ├── serviceWorker.ts ├── theme │ └── index.ts ├── utils.ts └── utils │ ├── device-sizes.ts │ ├── test-utils.ts │ └── update-url.ts ├── tsconfig.json └── yarn.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | !.eslintrc.js 2 | build/ 3 | node_modules 4 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: '@typescript-eslint/parser', 4 | extends: ['react-app'], 5 | plugins: ['prettier', '@emotion'], 6 | rules: { 7 | 'prettier/prettier': 'error', 8 | 'jsx-a11y/accessible-emoji': 'off', 9 | 'import/no-anonymous-default-export': 'off', 10 | 'react-hooks/exhaustive-deps': 'off', 11 | }, 12 | overrides: [ 13 | { 14 | files: ['src/__tests__/**/*'], 15 | env: { 16 | jest: true, 17 | }, 18 | }, 19 | ], 20 | } 21 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @lucasbento @pvinis @kelset 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: '🐛 Bug Report' 3 | about: Report a reproducible bug or regression in in this project. 4 | --- 5 | 6 | # Bug 7 | 8 | 12 | 13 | ## React Native versions 14 | 15 | 18 | 19 | ## Steps to reproduce 20 | 21 | 24 | 25 | Describe what you expected to happen: 26 | 27 | 1. 28 | 29 | 2. 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: '💡 Feature Request' 3 | about: Submit your idea for a change in the project. 4 | --- 5 | 6 | # Feature Request 7 | 8 | 11 | 12 | ## Why it is needed 13 | 14 | 17 | 18 | ## Possible implementation 19 | 20 | 23 | 24 | ### Code sample 25 | 26 | 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: '🤔 Questions and Help' 3 | about: Use this if there is something not clear about the code or its documentation. 4 | --- 5 | 6 | # Question 7 | 8 | 14 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Summary 4 | 5 | 13 | 14 | ## Test Plan 15 | 16 | 17 | 18 | ## What are the steps to reproduce? 19 | 20 | ## Checklist 21 | 22 | 23 | 24 | - [ ] I tested this thoroughly 25 | - [ ] I added the documentation in `README.md` (if needed) 26 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Build and Deploy 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | build-and-deploy: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: ⬇️ Checkout repository code 14 | uses: actions/checkout@v3 15 | 16 | - uses: actions/setup-node@v3 17 | with: 18 | node-version: '16' 19 | 20 | - name: 🛠 Install dependencies 21 | uses: bahmutov/npm-install@v1 22 | 23 | - name: 🔥 Build 24 | run: yarn build 25 | 26 | - name: 🚀 Deploy 27 | uses: JamesIves/github-pages-deploy-action@v4 28 | 29 | with: 30 | branch: gh-pages 31 | folder: build 32 | -------------------------------------------------------------------------------- /.github/workflows/push.yml: -------------------------------------------------------------------------------- 1 | name: Push 2 | 3 | on: [push] 4 | 5 | jobs: 6 | setup: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: ⬇️ Checkout repository code 10 | uses: actions/checkout@v3 11 | 12 | - uses: actions/setup-node@v3 13 | with: 14 | node-version: '16' 15 | 16 | - name: ⬆️ Upload output 17 | uses: actions/upload-artifact@v3 18 | with: 19 | name: ${{ github.sha }} 20 | path: ./** 21 | 22 | install: 23 | needs: setup 24 | runs-on: ubuntu-latest 25 | steps: 26 | - name: ⬇️ Restore output 27 | uses: actions/download-artifact@v3 28 | with: 29 | name: ${{ github.sha }} 30 | 31 | - name: 🛠 Install dependencies 32 | uses: bahmutov/npm-install@v1 33 | 34 | lint: 35 | needs: [setup, install] 36 | runs-on: ubuntu-latest 37 | steps: 38 | - name: ⬇️ Restore output 39 | uses: actions/download-artifact@v3 40 | with: 41 | name: ${{ github.sha }} 42 | 43 | - name: 🛠 Install dependencies 44 | uses: bahmutov/npm-install@v1 45 | 46 | - name: Run lint 47 | run: yarn lint 48 | 49 | - name: Run typecheck 50 | run: yarn typecheck 51 | 52 | test: 53 | needs: [setup, install] 54 | runs-on: ubuntu-latest 55 | steps: 56 | - name: ⬇️ Restore output 57 | uses: actions/download-artifact@v3 58 | with: 59 | name: ${{ github.sha }} 60 | 61 | - name: 🛠 Install dependencies 62 | uses: bahmutov/npm-install@v1 63 | 64 | - name: Run tests 65 | run: yarn test --runInBand 66 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # eslint 15 | .eslintcache 16 | 17 | # misc 18 | .DS_Store 19 | .env.local 20 | .env.development.local 21 | .env.test.local 22 | .env.production.local 23 | 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # IDE 29 | .idea 30 | .vscode 31 | 32 | # testing 33 | /.jest -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | 16.19.1 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "semi": false 4 | } 5 | -------------------------------------------------------------------------------- /@types/emotion.d.ts: -------------------------------------------------------------------------------- 1 | import '@emotion/react' 2 | 3 | import { Theme as EmotionTheme } from '../src/theme' 4 | 5 | export {} 6 | 7 | declare module '@emotion/react' { 8 | export interface Theme extends EmotionTheme {} 9 | } 10 | -------------------------------------------------------------------------------- /@types/global.d.ts: -------------------------------------------------------------------------------- 1 | interface Window {} 2 | 3 | interface Process { 4 | env: { 5 | PUBLIC_URL: string 6 | NODE_ENV: 'development' | 'production' 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | . 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## 💻 Contributing 2 | 3 | If you want to help us making this better, you can start by forking the project and follow these steps to testing it out locally: 4 | 5 | 1. Clone the project 6 | 1. Run `yarn install` 7 | 1. Run `yarn start` 8 | 1. Open `http://localhost:3000` 9 | 1. Select starting & target versions 10 | 1. Click the `Show me how to upgrade` button 11 | 12 | After which, you can create a branch to make your changes and then open a PR against this repository following the provided template 🤗 13 | 14 | ## Adding comments, notes, links to releases 15 | 16 | Inside `src/releases`, there are files for each release of React Native. `0.60.js` is a great example of how to add a description, some links, and some inline comments on the diff. 17 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM buildkite/puppeteer:latest 2 | 3 | # Fix emojis not loading 4 | RUN apt-get update -y 5 | RUN apt-get install -y fonts-noto-color-emoji 6 | 7 | RUN mkdir /app 8 | WORKDIR /app 9 | 10 | COPY package.json yarn.lock ./ 11 | 12 | RUN yarn --no-cache --frozen-lockfile 13 | 14 | COPY . . -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 React Native Community 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 |

Upgrade Helper

6 | 7 |

8 | A web tool to help you upgrade your React Native app with ease! 🚀 9 |

10 | 11 |

12 | CircleCI 13 | PRs Welcome 14 |

15 | 16 |

17 | 18 | Open the tool! 19 | 20 |

21 | 22 | ![Image showing a screenshot of Upgrade Helper with the phrase "You are gonna love this!"](https://user-images.githubusercontent.com/6207220/61382138-7a3a6780-a8ac-11e9-8c74-b4cb4830e131.png) 23 | 24 | ## ⚙️ How to use 25 | 26 | [![How to upgrade using upgrade-helper](https://img.youtube.com/vi/fmh_ZGHh_eg/0.jpg)](https://www.youtube.com/watch?v=fmh_ZGHh_eg) 27 | 28 | ## 🎩 How it works 29 | 30 | The **Upgrade Helper** tool aims to provide the full set of changes happening between any two versions, based on the previous work done in the [rn-diff-purge](https://github.com/react-native-community/rn-diff-purge) project: 31 | 32 | > This repository exposes an untouched React Native app generated with the CLI `react-native init RnDiffApp`. Each new React Native release causes a new project to be created, removing the old one, and getting a diff between them. This way, the diff is always clean, always in sync with the changes of the init template. 33 | 34 | This will help you see what changes you need to do in your code. 35 | 36 | Aside from this, the tool provides you a couple of cool extra features: 37 | 38 | - inline comments to help you with more insights about precise files 39 | - a set of links with further explanations on what the version you are upgrading to 40 | - a handy "done" button near each file to help you keep track of your process 41 | - a download button for new binary files 42 | - the ability to toggle all files by holding the alt key and clicking on expand/collapse 43 | - ...and we are planning many more features! Check the [enhancement tag](https://github.com/react-native-community/upgrade-helper/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3Aenhancement) in the issue section. 44 | 45 | ## 💻 Contributing 46 | 47 | If you want to help us making this better, you can start by forking the project and follow these steps to testing it out locally: 48 | 49 | 1. Clone the project 50 | 1. Run `yarn install` 51 | 1. Run `yarn start` 52 | 1. Open `http://localhost:3000` 53 | 1. Select starting & target versions 54 | 1. Click the `Show me how to upgrade` button 55 | 56 | After which, you can create a branch to to make your changes and then open a PR against this repository following the provided template 🤗 57 | 58 | ## 📣 Acknowledgments 59 | 60 | This project proudly uses [`rn-diff-purge`](https://github.com/react-native-community/rn-diff-purge), [`react-diff-view`](https://github.com/otakustay/react-diff-view) and [`create-react-app`](https://github.com/facebook/create-react-app). 61 | 62 | ## 📝 License & CoC 63 | 64 | This project is released under the [MIT license](./LICENSE). 65 | 66 | Morerover, this projects adopts the [Contributor Covenant Code of Conduct](./CODE_OF_CONDUCT.md) and all contributors are expected to follow it. 67 | -------------------------------------------------------------------------------- /__mocks__/react-github-btn.js: -------------------------------------------------------------------------------- 1 | // This mock is here because `react-github-btn` is not compiled in npm and CRA 2 | // doesn't support `transformIgnorePatterns` (to compile it) out-of-the-box for Jest 3 | import React from 'react' 4 | 5 | export default ({ children }) =>
ReactGitHubBtn - {children}
6 | -------------------------------------------------------------------------------- /config-overrides.js: -------------------------------------------------------------------------------- 1 | const { override, addBabelPreset } = require('customize-cra') 2 | 3 | const ignoreWarnings = (value) => (config) => { 4 | config.ignoreWarnings = value 5 | return config 6 | } 7 | 8 | module.exports = override( 9 | addBabelPreset('@emotion/babel-preset-css-prop'), 10 | 11 | // Ignore warnings about the react-diff-view sourcemap files. 12 | ignoreWarnings([/Failed to parse source map/]) 13 | ) 14 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | volumes: 4 | yarn: 5 | 6 | services: 7 | tests: 8 | image: tests 9 | build: 10 | context: ./ 11 | volumes: 12 | - yarn:/home/node/.cache/yarn 13 | - ./src:/app/src 14 | - ./test:/app/test 15 | - ./package.json:/usr/src/app/package.json 16 | - ./yarn.lock:/usr/src/app/yarn.lock 17 | command: sh -c "yarn docker-test-e2e" 18 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | transform: { 5 | '^.+\\.(js|jsx|ts|tsx)?$': 'ts-jest', 6 | }, 7 | transformIgnorePatterns: ['/node_modules/'], 8 | testMatch: ['>/__tests__/**/*.spec.(js|jsx|ts|tsx)'], 9 | setupFilesAfterEnv: ['/jest.setup.js'], 10 | } 11 | -------------------------------------------------------------------------------- /jest.setup.js: -------------------------------------------------------------------------------- 1 | import { toMatchImageSnapshot } from './src/utils/test-utils' 2 | 3 | expect.extend({ toMatchImageSnapshot }) 4 | -------------------------------------------------------------------------------- /mise.toml: -------------------------------------------------------------------------------- 1 | [tools] 2 | node = "22" 3 | yarn = "1.22.22" 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "upgrade-helper", 3 | "version": "0.1.0", 4 | "license": "MIT", 5 | "homepage": "https://react-native-community.github.io/upgrade-helper", 6 | "scripts": { 7 | "build": "EXTEND_ESLINT=true react-app-rewired build", 8 | "docker-test-e2e": "yarn start-and-wait && react-app-rewired test --watchAll=false --onlyChanged=false --testPathPattern='/__tests__/.*(\\.|).e2e.spec.(js|jsx|ts|tsx)?$'", 9 | "lint": "eslint . --cache --report-unused-disable-directives", 10 | "typecheck": "tsc --noEmit", 11 | "prepare": "husky install", 12 | "start": "EXTEND_ESLINT=true react-app-rewired start", 13 | "start-and-wait": "yarn start & wget --retry-connrefused --waitretry=1 --read-timeout=20 --timeout=15 -t 30 http://localhost:3000/", 14 | "test": "react-app-rewired test --watchAll=false --onlyChanged=false --testPathPattern='/__tests__/((?!e2e).)*.spec.(js|jsx|ts|tsx)?$'", 15 | "test-e2e": "docker-compose run tests" 16 | }, 17 | "dependencies": { 18 | "@ant-design/icons": "^5.3.0", 19 | "@emotion/react": "^11.11.3", 20 | "@emotion/styled": "^11.11.0", 21 | "antd": "^5.14.0", 22 | "date-fns": "^2.29.3", 23 | "framer-motion": "^11.0.3", 24 | "markdown-to-jsx": "7.1.9", 25 | "query-string": "8.1.0", 26 | "react": "18.2.0", 27 | "react-content-loader": "6.2.0", 28 | "react-copy-to-clipboard": "5.1.0", 29 | "react-diff-view": "^3.2.0", 30 | "react-dom": "18.2.0", 31 | "react-dom-confetti": "0.2.0", 32 | "react-ga": "3.3.1", 33 | "react-github-btn": "1.4.0", 34 | "react-scripts": "5.0.1", 35 | "semver": "7.3.8", 36 | "use-persisted-state": "^0.3.3" 37 | }, 38 | "devDependencies": { 39 | "@emotion/babel-preset-css-prop": "^11.11.0", 40 | "@emotion/eslint-plugin": "^11.11.0", 41 | "@jest/globals": "^29.7.0", 42 | "@testing-library/react": "^14.0.0", 43 | "@types/jest": "^29.5.12", 44 | "@types/jest-image-snapshot": "^6.4.0", 45 | "@types/markdown-to-jsx": "^7.0.1", 46 | "@types/node": "^20.11.16", 47 | "@types/react": "18.2.0", 48 | "@types/react-copy-to-clipboard": "^5.0.7", 49 | "@types/react-dom": "^18.2.18", 50 | "@types/semver": "^7.5.6", 51 | "@types/use-persisted-state": "^0.3.4", 52 | "@typescript-eslint/eslint-plugin": "^6.20.0", 53 | "@typescript-eslint/parser": "^6.20.0", 54 | "customize-cra": "^1.0.0", 55 | "eslint": "^8.35.0", 56 | "eslint-plugin-prettier": "^4.2.1", 57 | "gh-pages": "5.0.0", 58 | "husky": "8.0.3", 59 | "jest-image-snapshot": "6.4.0", 60 | "prettier": "2.8.4", 61 | "pretty-quick": "3.1.3", 62 | "puppeteer": "10.0.0", 63 | "react-app-rewired": "^2.2.1", 64 | "ts-jest": "^29.1.2", 65 | "typescript": "^5.3.3" 66 | }, 67 | "browserslist": { 68 | "production": [ 69 | ">0.2%", 70 | "not dead", 71 | "not op_mini all" 72 | ], 73 | "development": [ 74 | "last 1 chrome version", 75 | "last 1 firefox version", 76 | "last 1 safari version" 77 | ] 78 | }, 79 | "husky": { 80 | "hooks": { 81 | "pre-commit": "pretty-quick --staged --pattern \"src/**/*.*(js|jsx|ts|tsx)\"", 82 | "pre-push": "yarn run lint" 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-native-community/upgrade-helper/acf9a53c6944774d61aaccab9e2956010105eee1/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 13 | 14 | 23 | Upgrade React Native applications 24 | 25 | 26 | 27 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Upgrade RN apps", 3 | "name": "Upgrade your React Native Apps", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base", 4 | ":automergePatch" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Home from './components/pages/Home' 3 | 4 | const App = () => 5 | 6 | export default App 7 | -------------------------------------------------------------------------------- /src/__fixtures__/releases/0.59.js: -------------------------------------------------------------------------------- 1 | export default { 2 | usefulContent: { 3 | description: 'This is the very nice 0.59 release!', 4 | links: [ 5 | { 6 | title: 'This is a very cool link to a blog post about 0.59', 7 | url: 'https://facebook.github.io/react-native/blog/2019/03/12/releasing-react-native-059', 8 | }, 9 | ], 10 | }, 11 | } 12 | -------------------------------------------------------------------------------- /src/__tests__/Home.e2e.spec.ts: -------------------------------------------------------------------------------- 1 | import { launchBrowser, waitToRender, closeBrowser } from '../utils/test-utils' 2 | import { testIDs as versionSelectorTestIDs } from '../components/common/VersionSelector' 3 | import { testIDs as upgradeButtonTestIDs } from '../components/common/UpgradeButton' 4 | import { testIDs as diffSectionTestIDs } from '../components/common/Diff/DiffSection' 5 | import { testIDs as diffHeaderTestIDs } from '../components/common/Diff/DiffHeader' 6 | import { testIDs as troubleshootingGuidesButtonTestIDs } from '../components/common/TroubleshootingGuidesButton' 7 | 8 | describe('Home', () => { 9 | let page 10 | 11 | beforeAll(async () => { 12 | const browser = await launchBrowser() 13 | 14 | page = browser.page 15 | }) 16 | 17 | afterAll(closeBrowser) 18 | 19 | const selectVersion = async (targetVersion) => { 20 | await page.evaluate((pageTargetVersion) => { 21 | const element = [ 22 | ...document.querySelectorAll( 23 | '.ant-select-dropdown.ant-select-dropdown-placement-bottomLeft:not(.ant-select-dropdown-hidden) .ant-select-item-option-content' 24 | ), 25 | ].find(({ innerText: version }) => version === pageTargetVersion) 26 | 27 | element.click() 28 | }, targetVersion) 29 | } 30 | 31 | it('1. should load the top component with logo, versions and troubleshooting guides alert', async () => { 32 | await waitToRender() 33 | 34 | const image = await page.screenshot() 35 | 36 | expect(image).toMatchImageSnapshot() 37 | }) 38 | 39 | it('2. should hide the troubleshooting guides alert and show the troubleshooting guides', async () => { 40 | await page.click( 41 | `button[data-testid="${troubleshootingGuidesButtonTestIDs.troubleshootingGuidesButton}"]` 42 | ) 43 | 44 | await waitToRender() 45 | 46 | const image = await page.screenshot() 47 | 48 | expect(image).toMatchImageSnapshot() 49 | }) 50 | 51 | it('3. should hide the troubleshooting guides and show the normal header', async () => { 52 | await page.click( 53 | `button[data-testid="${troubleshootingGuidesButtonTestIDs.troubleshootingGuidesButton}"]` 54 | ) 55 | 56 | await waitToRender() 57 | 58 | const image = await page.screenshot() 59 | 60 | expect(image).toMatchImageSnapshot() 61 | }) 62 | 63 | it('4. should load the current versions', async () => { 64 | await page.click( 65 | `div[data-testid="${versionSelectorTestIDs.fromVersionSelector}"] input` 66 | ) 67 | 68 | await waitToRender() 69 | 70 | const image = await page.screenshot() 71 | 72 | expect(image).toMatchImageSnapshot() 73 | }) 74 | 75 | it('5. should select a current version', async () => { 76 | await selectVersion('0.63.2') 77 | 78 | await waitToRender() 79 | 80 | const image = await page.screenshot() 81 | 82 | expect(image).toMatchImageSnapshot() 83 | }) 84 | 85 | it('6. should load the upgrading versions', async () => { 86 | await page.click( 87 | `div[data-testid="${versionSelectorTestIDs.toVersionSelector}"] input` 88 | ) 89 | 90 | await waitToRender() 91 | 92 | const image = await page.screenshot() 93 | 94 | expect(image).toMatchImageSnapshot() 95 | }) 96 | 97 | it('7. should select an upgrading version', async () => { 98 | await selectVersion('0.64.2') 99 | await waitToRender() 100 | 101 | const image = await page.screenshot() 102 | 103 | expect(image).toMatchImageSnapshot() 104 | }) 105 | 106 | it('8. should load the upgrading content', async () => { 107 | await page.click(`[data-testid="${upgradeButtonTestIDs.upgradeButton}"]`) 108 | await waitToRender() 109 | 110 | const image = await page.screenshot() 111 | 112 | expect(image).toMatchImageSnapshot() 113 | }) 114 | 115 | it('9. should scroll to the first file in diff', async () => { 116 | await page.evaluate((testID) => { 117 | document 118 | .querySelector(`[data-testid="${testID}"]`) 119 | .querySelector('div') 120 | .scrollIntoView() 121 | }, diffSectionTestIDs.diffSection) 122 | 123 | const image = await page.screenshot() 124 | 125 | expect(image).toMatchImageSnapshot() 126 | }) 127 | 128 | it('10. should collapse first file in diff', async () => { 129 | await page.click( 130 | `[data-testid="${diffHeaderTestIDs.collapseClickableArea}"]` 131 | ) 132 | 133 | const image = await page.screenshot() 134 | 135 | expect(image).toMatchImageSnapshot() 136 | }) 137 | }) 138 | -------------------------------------------------------------------------------- /src/__tests__/__image_snapshots__/home-e-2-e-spec-js-home-1-should-load-the-top-component-with-logo-and-versions-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-native-community/upgrade-helper/acf9a53c6944774d61aaccab9e2956010105eee1/src/__tests__/__image_snapshots__/home-e-2-e-spec-js-home-1-should-load-the-top-component-with-logo-and-versions-1-snap.png -------------------------------------------------------------------------------- /src/__tests__/__image_snapshots__/home-e-2-e-spec-js-home-1-should-load-the-top-component-with-logo-versions-and-troubleshooting-guides-alert-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-native-community/upgrade-helper/acf9a53c6944774d61aaccab9e2956010105eee1/src/__tests__/__image_snapshots__/home-e-2-e-spec-js-home-1-should-load-the-top-component-with-logo-versions-and-troubleshooting-guides-alert-1-snap.png -------------------------------------------------------------------------------- /src/__tests__/__image_snapshots__/home-e-2-e-spec-js-home-10-should-collapse-first-file-in-diff-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-native-community/upgrade-helper/acf9a53c6944774d61aaccab9e2956010105eee1/src/__tests__/__image_snapshots__/home-e-2-e-spec-js-home-10-should-collapse-first-file-in-diff-1-snap.png -------------------------------------------------------------------------------- /src/__tests__/__image_snapshots__/home-e-2-e-spec-js-home-2-should-hide-the-troubleshooting-guides-alert-and-show-the-troubleshooting-guides-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-native-community/upgrade-helper/acf9a53c6944774d61aaccab9e2956010105eee1/src/__tests__/__image_snapshots__/home-e-2-e-spec-js-home-2-should-hide-the-troubleshooting-guides-alert-and-show-the-troubleshooting-guides-1-snap.png -------------------------------------------------------------------------------- /src/__tests__/__image_snapshots__/home-e-2-e-spec-js-home-2-should-load-the-current-versions-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-native-community/upgrade-helper/acf9a53c6944774d61aaccab9e2956010105eee1/src/__tests__/__image_snapshots__/home-e-2-e-spec-js-home-2-should-load-the-current-versions-1-snap.png -------------------------------------------------------------------------------- /src/__tests__/__image_snapshots__/home-e-2-e-spec-js-home-3-should-hide-the-troubleshooting-guides-and-show-the-normal-header-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-native-community/upgrade-helper/acf9a53c6944774d61aaccab9e2956010105eee1/src/__tests__/__image_snapshots__/home-e-2-e-spec-js-home-3-should-hide-the-troubleshooting-guides-and-show-the-normal-header-1-snap.png -------------------------------------------------------------------------------- /src/__tests__/__image_snapshots__/home-e-2-e-spec-js-home-3-should-select-a-current-version-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-native-community/upgrade-helper/acf9a53c6944774d61aaccab9e2956010105eee1/src/__tests__/__image_snapshots__/home-e-2-e-spec-js-home-3-should-select-a-current-version-1-snap.png -------------------------------------------------------------------------------- /src/__tests__/__image_snapshots__/home-e-2-e-spec-js-home-4-should-load-the-current-versions-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-native-community/upgrade-helper/acf9a53c6944774d61aaccab9e2956010105eee1/src/__tests__/__image_snapshots__/home-e-2-e-spec-js-home-4-should-load-the-current-versions-1-snap.png -------------------------------------------------------------------------------- /src/__tests__/__image_snapshots__/home-e-2-e-spec-js-home-4-should-load-the-upgrading-versions-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-native-community/upgrade-helper/acf9a53c6944774d61aaccab9e2956010105eee1/src/__tests__/__image_snapshots__/home-e-2-e-spec-js-home-4-should-load-the-upgrading-versions-1-snap.png -------------------------------------------------------------------------------- /src/__tests__/__image_snapshots__/home-e-2-e-spec-js-home-5-should-select-a-current-version-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-native-community/upgrade-helper/acf9a53c6944774d61aaccab9e2956010105eee1/src/__tests__/__image_snapshots__/home-e-2-e-spec-js-home-5-should-select-a-current-version-1-snap.png -------------------------------------------------------------------------------- /src/__tests__/__image_snapshots__/home-e-2-e-spec-js-home-5-should-select-an-upgrading-version-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-native-community/upgrade-helper/acf9a53c6944774d61aaccab9e2956010105eee1/src/__tests__/__image_snapshots__/home-e-2-e-spec-js-home-5-should-select-an-upgrading-version-1-snap.png -------------------------------------------------------------------------------- /src/__tests__/__image_snapshots__/home-e-2-e-spec-js-home-6-should-load-the-upgrading-content-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-native-community/upgrade-helper/acf9a53c6944774d61aaccab9e2956010105eee1/src/__tests__/__image_snapshots__/home-e-2-e-spec-js-home-6-should-load-the-upgrading-content-1-snap.png -------------------------------------------------------------------------------- /src/__tests__/__image_snapshots__/home-e-2-e-spec-js-home-6-should-load-the-upgrading-versions-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-native-community/upgrade-helper/acf9a53c6944774d61aaccab9e2956010105eee1/src/__tests__/__image_snapshots__/home-e-2-e-spec-js-home-6-should-load-the-upgrading-versions-1-snap.png -------------------------------------------------------------------------------- /src/__tests__/__image_snapshots__/home-e-2-e-spec-js-home-7-should-scroll-to-the-first-file-in-diff-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-native-community/upgrade-helper/acf9a53c6944774d61aaccab9e2956010105eee1/src/__tests__/__image_snapshots__/home-e-2-e-spec-js-home-7-should-scroll-to-the-first-file-in-diff-1-snap.png -------------------------------------------------------------------------------- /src/__tests__/__image_snapshots__/home-e-2-e-spec-js-home-7-should-select-an-upgrading-version-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-native-community/upgrade-helper/acf9a53c6944774d61aaccab9e2956010105eee1/src/__tests__/__image_snapshots__/home-e-2-e-spec-js-home-7-should-select-an-upgrading-version-1-snap.png -------------------------------------------------------------------------------- /src/__tests__/__image_snapshots__/home-e-2-e-spec-js-home-8-should-collapse-first-file-in-diff-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-native-community/upgrade-helper/acf9a53c6944774d61aaccab9e2956010105eee1/src/__tests__/__image_snapshots__/home-e-2-e-spec-js-home-8-should-collapse-first-file-in-diff-1-snap.png -------------------------------------------------------------------------------- /src/__tests__/__image_snapshots__/home-e-2-e-spec-js-home-8-should-load-the-upgrading-content-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-native-community/upgrade-helper/acf9a53c6944774d61aaccab9e2956010105eee1/src/__tests__/__image_snapshots__/home-e-2-e-spec-js-home-8-should-load-the-upgrading-content-1-snap.png -------------------------------------------------------------------------------- /src/__tests__/__image_snapshots__/home-e-2-e-spec-js-home-9-should-scroll-to-the-first-file-in-diff-1-snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/react-native-community/upgrade-helper/acf9a53c6944774d61aaccab9e2956010105eee1/src/__tests__/__image_snapshots__/home-e-2-e-spec-js-home-9-should-scroll-to-the-first-file-in-diff-1-snap.png -------------------------------------------------------------------------------- /src/__tests__/components/common/CompletedFilesCounter.spec.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render } from '@testing-library/react' 3 | import CompletedFilesCounter from '../../../components/common/CompletedFilesCounter' 4 | import { lightTheme } from '../../../theme' 5 | 6 | it('renders without crashing', () => { 7 | const { container } = render( 8 | 15 | ) 16 | 17 | expect(container).toMatchInlineSnapshot(` 18 |
19 |
22 | 23 | 26 | 10 27 | 28 | 29 | / 30 | 11 31 | 32 |
35 |
36 |
37 | `) 38 | }) 39 | -------------------------------------------------------------------------------- /src/__tests__/components/common/Markdown.spec.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render } from '@testing-library/react' 3 | import Markdown from '../../../components/common/Markdown' 4 | 5 | it('renders without crashing', () => { 6 | const { container } = render( 7 | # Hello world! 8 | ) 9 | 10 | expect(container).toMatchInlineSnapshot(` 11 |
12 |

15 | Hello world! 16 |

17 |
18 | `) 19 | }) 20 | -------------------------------------------------------------------------------- /src/__tests__/utils.spec.ts: -------------------------------------------------------------------------------- 1 | import { PACKAGE_NAMES } from '../constants' 2 | import '../releases/__mocks__/index' 3 | import { 4 | getVersionsContentInDiff, 5 | replaceAppDetails, 6 | getChangelogURL, 7 | } from '../utils' 8 | 9 | describe('getVersionsContentInDiff', () => { 10 | it('returns the versions in the provided range', () => { 11 | const versions = getVersionsContentInDiff({ 12 | packageName: PACKAGE_NAMES.RN, 13 | fromVersion: '0.57.0', 14 | toVersion: '0.59.0', 15 | }) 16 | 17 | expect(versions).toEqual([{ version: '0.59' }, { version: '0.58' }]) 18 | }) 19 | 20 | it('returns the versions in the provided range with release candidates', () => { 21 | const versions = getVersionsContentInDiff({ 22 | packageName: PACKAGE_NAMES.RN, 23 | fromVersion: '0.56.0', 24 | toVersion: '0.59.0-rc.1', 25 | }) 26 | 27 | expect(versions).toEqual([ 28 | { version: '0.59' }, 29 | { version: '0.58' }, 30 | { version: '0.57' }, 31 | ]) 32 | }) 33 | 34 | it('returns the versions in the provided range with patches specified', () => { 35 | const versions = getVersionsContentInDiff({ 36 | packageName: PACKAGE_NAMES.RN, 37 | fromVersion: '0.57.2', 38 | toVersion: '0.59.9', 39 | }) 40 | 41 | expect(versions).toEqual([{ version: '0.59' }, { version: '0.58' }]) 42 | }) 43 | }) 44 | 45 | describe('getChangelogURL', () => { 46 | const { RN, RNM, RNW } = PACKAGE_NAMES 47 | test.each([ 48 | [ 49 | RN, 50 | '0.71.7', 51 | 'https://github.com/facebook/react-native/blob/main/CHANGELOG.md#v0717', 52 | ], 53 | [ 54 | RN, 55 | '0.71.6', 56 | 'https://github.com/facebook/react-native/blob/main/CHANGELOG.md#v0716', 57 | ], 58 | [ 59 | RNM, 60 | '0.71.5', 61 | 'https://github.com/microsoft/react-native-macos/releases/tag/v0.71.5', 62 | ], 63 | [ 64 | RNW, 65 | '0.71.4', 66 | 'https://github.com/microsoft/react-native-windows/releases/tag/react-native-windows_v0.71.4', 67 | ], 68 | ])('getChangelogURL("%s", "%s") -> %s', (packageName, version, url) => { 69 | expect(getChangelogURL({ packageName, version })).toEqual(url) 70 | }) 71 | }) 72 | 73 | describe('replaceAppDetails ', () => { 74 | test.each([ 75 | // Don't change anything if no app name or package is passed. 76 | [ 77 | 'RnDiffApp/ios/RnDiffApp/main.m', 78 | '', 79 | '', 80 | 'RnDiffApp/ios/RnDiffApp/main.m', 81 | ], 82 | [ 83 | 'android/app/src/debug/java/com/rndiffapp/ReactNativeFlipper.java', 84 | '', 85 | '', 86 | 'android/app/src/debug/java/com/rndiffapp/ReactNativeFlipper.java', 87 | ], 88 | [ 89 | 'location = "group:RnDiffApp.xcodeproj">', 90 | '', 91 | '', 92 | 'location = "group:RnDiffApp.xcodeproj">', 93 | ], 94 | // Update Java file path with correct app name and package. 95 | [ 96 | 'android/app/src/debug/java/com/rndiffapp/ReactNativeFlipper.java', 97 | 'SuperApp', 98 | 'au.org.mycorp', 99 | 'android/app/src/debug/java/au/org/mycorp/ReactNativeFlipper.java', 100 | ], 101 | // Update the app details in file contents. 102 | [ 103 | 'location = "group:RnDiffApp.xcodeproj">', 104 | 'MyFancyApp', 105 | '', 106 | 'location = "group:MyFancyApp.xcodeproj">', 107 | ], 108 | [ 109 | 'applicationId "com.rndiffapp"', 110 | 'ACoolApp', 111 | 'net.foobar', 112 | 'applicationId "net.foobar"', 113 | ], 114 | // Don't accidentally pick up other instances of "com" such as in domain 115 | // names, or android or facebook packages. 116 | [ 117 | 'apply plugin: "com.android.application"', 118 | 'ACoolApp', 119 | 'net.foobar', 120 | 'apply plugin: "com.android.application"', 121 | ], 122 | [ 123 | '* https://github.com/facebook/react-native', 124 | 'ACoolApp', 125 | 'net.foobar', 126 | '* https://github.com/facebook/react-native', 127 | ], 128 | ])( 129 | 'replaceAppDetails("%s", "%s", "%s") -> %s', 130 | (path, appName, appPackage, newPath) => { 131 | expect(replaceAppDetails(path, appName, appPackage)).toEqual(newPath) 132 | } 133 | ) 134 | }) 135 | -------------------------------------------------------------------------------- /src/assets/loading.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | BlueprintLogo 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /src/components/common/AlignDepsAlert.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Markdown from './Markdown' 3 | 4 | const AlignDepsAlert = () => ( 5 | <> 6 | 7 | You can use the following command to kick off the upgrade: `npx 8 | @rnx-kit/align-deps --requirements react-native@[major.minor]`. 9 | 10 |
11 |
12 | 13 | [`align-deps`](https://microsoft.github.io/rnx-kit/docs/tools/align-deps) 14 | is an OSS tool from Microsoft that automates dependency management. It 15 | knows which packages\* versions are compatible with your specific version 16 | of RN, and it uses that knowledge to align dependencies, keeping your app 17 | healthy and up-to-date\*\*. [Find out more 18 | here](https://microsoft.github.io/rnx-kit/docs/guides/dependency-management). 19 | 20 |
21 | \* Not all packages are supported out-of-the-box. 22 |
23 | 24 | \*\* You still need to do the other changes below and verify the 25 | changelogs of the libraries that got upgraded. 26 | 27 | 28 | ) 29 | 30 | export default AlignDepsAlert 31 | -------------------------------------------------------------------------------- /src/components/common/BinaryDownload.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Button, Popover as AntdPopover, Tooltip } from 'antd' 3 | import type { PopoverProps as AntdPopoverProps } from 'antd' 4 | import styled from '@emotion/styled' 5 | import DownloadFileButton from './DownloadFileButton' 6 | import { removeAppPathPrefix } from '../../utils' 7 | import type { Theme } from '../../theme' 8 | import type { File } from 'gitdiff-parser' 9 | 10 | const Container = styled.div` 11 | padding-right: 10px; 12 | ` 13 | 14 | interface BinaryRowProps { 15 | index: number 16 | theme?: Theme 17 | } 18 | 19 | const BinaryRow = styled.div` 20 | display: flex; 21 | justify-content: space-between; 22 | align-items: center; 23 | background-color: ${({ index, theme }) => 24 | index % 2 === 0 ? theme.rowEven : theme.rowOdd}; 25 | font-family: SFMono-Regular, Consolas, Liberation Mono, Menlo, Courier, 26 | monospace; 27 | font-size: 12px; 28 | width: 500px; 29 | max-width: 500px; 30 | padding: 10px 15px; 31 | border-bottom: 1px solid ${({ theme }) => theme.border}; 32 | ` 33 | interface PopoverProps extends Omit { 34 | className?: string 35 | } 36 | const Popover = styled(({ className, ...props }: PopoverProps) => ( 37 | 38 | ))` 39 | .ant-popover-inner-content { 40 | padding: 0; 41 | } 42 | ` 43 | 44 | interface BinaryListProps { 45 | binaryFiles: File[] 46 | toVersion: string 47 | appName: string 48 | packageName: string 49 | } 50 | 51 | const BinaryList: React.FC = ({ 52 | binaryFiles, 53 | toVersion, 54 | appName, 55 | packageName, 56 | }) => { 57 | return ( 58 | <> 59 | {binaryFiles.map(({ newPath }, index) => { 60 | return ( 61 | 62 | {removeAppPathPrefix(newPath, appName)} 63 | 64 | 70 | 71 | ) 72 | })} 73 | 74 | ) 75 | } 76 | 77 | interface BinaryDownloadProps { 78 | diff: File[] 79 | fromVersion: string 80 | toVersion: string 81 | appName: string 82 | packageName: string 83 | } 84 | const BinaryDownload = ({ 85 | diff, 86 | fromVersion, 87 | toVersion, 88 | appName, 89 | packageName, 90 | }: BinaryDownloadProps) => { 91 | const binaryFiles = diff.filter( 92 | ({ hunks, type }) => hunks.length === 0 && type !== 'delete' 93 | ) 94 | 95 | if (binaryFiles.length === 0) { 96 | return null 97 | } 98 | 99 | return ( 100 | 101 | 110 | } 111 | trigger="click" 112 | > 113 | 117 | 118 | 119 | 120 | 121 | ) 122 | } 123 | 124 | export default BinaryDownload 125 | -------------------------------------------------------------------------------- /src/components/common/CompletedFilesCounter.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from '@emotion/styled' 3 | import { keyframes, css } from '@emotion/react' 4 | import Confetti from 'react-dom-confetti' 5 | import { Popover } from 'antd' 6 | import type { Theme } from '../../theme' 7 | 8 | const shake = keyframes` 9 | 0% { 10 | transform: translate(0, 0); 11 | } 12 | 13 | 10%, 90% { 14 | transform: translate(0, -2px); 15 | } 16 | 17 | 20%, 80% { 18 | transform: translate(0, 3px); 19 | } 20 | 21 | 30%, 50%, 70% { 22 | transform: translate(0, -5px); 23 | } 24 | 25 | 40%, 60% { 26 | transform: translate(0, 5px); 27 | } 28 | ` 29 | 30 | interface CompletedFilesCounterProps 31 | extends React.HTMLAttributes { 32 | completed: number 33 | total: number 34 | popoverContent: string 35 | popoverCursorType: React.CSSProperties['cursor'] 36 | theme?: Theme 37 | } 38 | 39 | const CompletedFilesCounter = styled( 40 | ({ 41 | completed, 42 | total, 43 | popoverContent, 44 | popoverCursorType, 45 | ...props 46 | }: CompletedFilesCounterProps) => ( 47 |
48 | 56 | 57 | {completed === 0 ? 1 : completed} 58 | {' '} 59 | /{total} 60 | 61 | 69 |
70 | ) 71 | )` 72 | position: fixed; 73 | bottom: 20px; 74 | right: 20px; 75 | background: ${({ theme }) => theme.popover.background}; 76 | padding: 10px; 77 | border: 1px solid ${({ theme }) => theme.popover.border}; 78 | border-radius: 20px; 79 | color: ${({ theme }) => theme.popover.text}; 80 | transform: ${({ completed }) => 81 | completed ? 'translateY(0px)' : 'translateY(70px)'}; 82 | display: flex; 83 | align-self: flex-end; 84 | transition: transform 0.3s; 85 | cursor: ${({ popoverCursorType }) => popoverCursorType}; 86 | ${({ completed, total }) => 87 | completed === total && 88 | css` 89 | transform: translateY(70px); 90 | animation: ${shake}; 91 | animation-duration: 1.5s; 92 | `} 93 | 94 | .completedAmount { 95 | color: ${({ theme }) => theme.popover.border}; 96 | } 97 | ` 98 | 99 | export default CompletedFilesCounter 100 | -------------------------------------------------------------------------------- /src/components/common/CopyFileButton.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import styled from '@emotion/styled' 3 | import { Button, Popover } from 'antd' 4 | import type { ButtonProps } from 'antd' 5 | import { getBinaryFileURL, replaceAppDetails } from '../../utils' 6 | import { CopyOutlined } from '@ant-design/icons' 7 | 8 | const popoverContentOpts = { 9 | default: 'Copy raw contents', 10 | copied: 'Copied!', 11 | } 12 | 13 | interface CopyFileButtonProps extends ButtonProps { 14 | open: boolean 15 | version: string 16 | path: string 17 | packageName: string 18 | appName: string 19 | appPackage: string 20 | } 21 | 22 | const CopyFileButton = styled( 23 | ({ 24 | open, 25 | version, 26 | path, 27 | packageName, 28 | appName, 29 | appPackage, 30 | ...props 31 | }: CopyFileButtonProps) => { 32 | const [popoverContent, setPopoverContent] = useState( 33 | popoverContentOpts.default 34 | ) 35 | 36 | const fetchContent = () => 37 | fetch(getBinaryFileURL({ packageName, version, path })) 38 | .then((response) => response.text()) 39 | .then((content) => replaceAppDetails(content, appName, appPackage)) 40 | 41 | const copyContent = () => { 42 | // From https://wolfgangrittner.dev/how-to-use-clipboard-api-in-firefox/ 43 | if (typeof ClipboardItem && navigator.clipboard.write) { 44 | const item = new ClipboardItem({ 45 | 'text/plain': fetchContent().then( 46 | (content) => new Blob([content], { type: 'text/plain' }) 47 | ), 48 | }) 49 | 50 | return navigator.clipboard.write([item]) 51 | } else { 52 | return fetchContent().then((content) => 53 | navigator.clipboard.writeText(content) 54 | ) 55 | } 56 | } 57 | 58 | if (!open) { 59 | return null 60 | } 61 | 62 | return ( 63 | 64 | 19 | 20 | ) 21 | } 22 | 23 | export { DarkModeButton } 24 | -------------------------------------------------------------------------------- /src/components/common/Diff/Diff.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useCallback, Fragment } from 'react' 2 | import styled from '@emotion/styled' 3 | import { 4 | Diff as RDiff, 5 | DiffProps as RDiffProps, 6 | Hunk, 7 | markEdits, 8 | tokenize, 9 | Decoration as DiffDecoration, 10 | HunkData, 11 | ViewType, 12 | DiffType, 13 | HunkTokens, 14 | TokenNode, 15 | } from 'react-diff-view' 16 | import DiffHeader from './DiffHeader' 17 | import { getComments } from './DiffComment' 18 | import { replaceAppDetails } from '../../../utils' 19 | import type { Theme } from '../../../theme' 20 | import type { ChangeEventArgs } from 'react-diff-view' 21 | import type { DefaultRenderToken } from 'react-diff-view/types/context' 22 | 23 | const copyPathPopoverContentOpts = { 24 | default: 'Copy file path', 25 | copied: 'File path copied!', 26 | } 27 | 28 | const Container = styled.div<{ theme?: Theme }>` 29 | border: 1px solid ${({ theme }) => theme.border}; 30 | border-radius: 3px; 31 | margin-bottom: 16px; 32 | margin-top: 16px; 33 | color: ${({ theme }) => theme.text}; 34 | ` 35 | 36 | const More = styled.div` 37 | margin-left: 30px; 38 | padding-left: 4px; 39 | ` 40 | 41 | const Decoration = styled(DiffDecoration)<{ theme?: Theme }>` 42 | background-color: ${({ theme }) => theme.diff.decorationContentBackground}; 43 | color: ${({ theme }) => theme.diff.decorationContent}; 44 | ` 45 | 46 | interface DiffViewProps extends RDiffProps { 47 | theme?: Theme 48 | } 49 | const DiffView = styled(RDiff)` 50 | .diff-gutter-col { 51 | width: 30px; 52 | } 53 | 54 | tr.diff-line { 55 | font-family: SFMono-Regular, Consolas, Liberation Mono, Menlo, Courier, 56 | monospace; 57 | } 58 | 59 | td.diff-gutter .diff-line-normal { 60 | background-color: ${({ theme }) => theme.diff.gutterInsertBackground}; 61 | border-color: ${({ theme }) => theme.greenBorder}; 62 | } 63 | 64 | td.diff-gutter:hover { 65 | cursor: pointer; 66 | color: ${({ theme }) => theme.textHover}; 67 | } 68 | 69 | td.diff-code { 70 | font-size: 12px; 71 | color: ${({ theme }) => theme.text}; 72 | } 73 | 74 | td.diff-gutter-omit:before { 75 | width: 0; 76 | background-color: transparent; 77 | } 78 | 79 | td.diff-widget-content { 80 | padding: 0; 81 | } 82 | 83 | // From diff global 84 | .diff { 85 | background-color: ${({ theme }) => theme.background}; 86 | color: ${({ theme }) => theme.text}; 87 | tab-size: 4; 88 | hyphens: none; 89 | } 90 | 91 | .diff::selection { 92 | background-color: ${({ theme }) => theme.diff.selectionBackground}; 93 | } 94 | 95 | .diff-decoration { 96 | line-height: 2; 97 | font-family: SFMono-Regular, Consolas, 'Liberation Mono', Menlo, Courier, 98 | monospace; 99 | } 100 | 101 | .diff-decoration-content { 102 | padding-left: 0.5em; 103 | background-color: ${({ theme }) => theme.diff.decorationContentBackground}; 104 | color: ${({ theme }) => theme.diff.decorationContent}; 105 | } 106 | 107 | .diff-gutter { 108 | padding: 0; 109 | text-align: center; 110 | font-size: 12px; 111 | cursor: auto; 112 | } 113 | 114 | .diff-gutter-insert { 115 | background-color: ${({ theme }) => theme.diff.gutterInsertBackground}; 116 | } 117 | 118 | .diff-gutter-delete { 119 | background-color: ${({ theme }) => theme.diff.gutterDeleteBackground}; 120 | } 121 | 122 | .diff-gutter-selected { 123 | background-color: ${({ theme }) => theme.diff.gutterSelectedBackground}; 124 | } 125 | 126 | .diff-code-insert { 127 | background-color: ${({ theme }) => theme.diff.codeInsertBackground}; 128 | } 129 | 130 | .diff-code-edit { 131 | color: inherit; 132 | } 133 | 134 | .diff-code-insert .diff-code-edit { 135 | background-color: ${({ theme }) => theme.diff.codeInsertEditBackground}; 136 | } 137 | 138 | .diff-code-delete { 139 | background-color: ${({ theme }) => theme.diff.codeDeleteBackground}; 140 | } 141 | 142 | .diff-code-delete .diff-code-edit { 143 | background-color: ${({ theme }) => theme.diff.codeDeleteEditBackground}; 144 | } 145 | 146 | .diff-code-selected { 147 | background-color: ${({ theme }) => theme.diff.codeSelectedBackground}; 148 | } 149 | 150 | .diff-decoration-gutter { 151 | background-color: ${({ theme }) => theme.diff.decorationGutterBackground}; 152 | color: ${({ theme }) => theme.diff.decorationGutter}; 153 | } 154 | ` 155 | 156 | // Diff will be collapsed by default if the file has been deleted or has more than 5 hunks 157 | const isDiffCollapsedByDefault = ({ 158 | type, 159 | hunks, 160 | }: { 161 | type: DiffType 162 | hunks: HunkData[] 163 | }) => (type === 'delete' || hunks.length > 5 ? true : undefined) 164 | 165 | const renderToken = ( 166 | token: TokenNode, 167 | renderDefault: DefaultRenderToken, 168 | index: number 169 | ) => { 170 | switch (token.type) { 171 | case 'space': 172 | console.log(token) 173 | return ( 174 | 175 | {token.children && 176 | token.children.map((token, index) => 177 | renderToken(token, renderDefault, index) 178 | )} 179 | 180 | ) 181 | default: 182 | return renderDefault(token, index) 183 | } 184 | } 185 | 186 | interface DiffProps { 187 | packageName: string 188 | oldPath: string 189 | newPath: string 190 | type: DiffType 191 | hunks: HunkData[] 192 | fromVersion: string 193 | toVersion: string 194 | diffKey: string 195 | isDiffCompleted: boolean 196 | onCompleteDiff: (diffKey: string) => void 197 | selectedChanges: string[] 198 | onToggleChangeSelection: (args: ChangeEventArgs) => void 199 | areAllCollapsed?: boolean 200 | setAllCollapsed: (collapse: boolean | undefined) => void 201 | diffViewStyle: ViewType 202 | appName: string 203 | appPackage: string 204 | } 205 | 206 | const Diff = ({ 207 | packageName, 208 | oldPath, 209 | newPath, 210 | type, 211 | hunks, 212 | fromVersion, 213 | toVersion, 214 | diffKey, 215 | isDiffCompleted, 216 | onCompleteDiff, 217 | selectedChanges, 218 | onToggleChangeSelection, 219 | areAllCollapsed, 220 | setAllCollapsed, 221 | diffViewStyle, 222 | appName, 223 | appPackage, 224 | }: DiffProps) => { 225 | const [isDiffCollapsed, setIsDiffCollapsed] = useState( 226 | isDiffCollapsedByDefault({ type, hunks }) || false 227 | ) 228 | 229 | const [copyPathPopoverContent, setCopyPathPopoverContent] = useState( 230 | copyPathPopoverContentOpts.default 231 | ) 232 | 233 | const handleCopyPathToClipboard = () => { 234 | setCopyPathPopoverContent(copyPathPopoverContentOpts.copied) 235 | } 236 | 237 | const handleResetCopyPathPopoverContent = () => { 238 | if (copyPathPopoverContent === copyPathPopoverContentOpts.copied) { 239 | setCopyPathPopoverContent(copyPathPopoverContentOpts.default) 240 | } 241 | } 242 | 243 | const getHunksWithAppName = useCallback( 244 | (originalHunks: HunkData[]) => { 245 | if (!appName && !appPackage) { 246 | // No patching of rn-diff-purge output required. 247 | return originalHunks 248 | } 249 | 250 | return originalHunks.map((hunk) => ({ 251 | ...hunk, 252 | changes: hunk.changes.map((change) => ({ 253 | ...change, 254 | content: replaceAppDetails(change.content, appName, appPackage), 255 | })), 256 | content: replaceAppDetails(hunk.content, appName, appPackage), 257 | })) 258 | }, 259 | [appName, appPackage] 260 | ) 261 | 262 | if (areAllCollapsed !== undefined && areAllCollapsed !== isDiffCollapsed) { 263 | setIsDiffCollapsed(areAllCollapsed) 264 | } else if (isDiffCompleted && isDiffCollapsed === undefined) { 265 | setIsDiffCollapsed(true) 266 | } 267 | 268 | const diffComments = getComments({ 269 | packageName, 270 | newPath, 271 | fromVersion, 272 | toVersion, 273 | }) 274 | 275 | const updatedHunks: HunkData[] = React.useMemo( 276 | () => getHunksWithAppName(hunks), 277 | [hunks] 278 | ) 279 | const tokens: HunkTokens = React.useMemo( 280 | () => 281 | tokenize(hunks, { 282 | enhancers: [markEdits(updatedHunks)], 283 | }), 284 | [hunks, updatedHunks] 285 | ) 286 | 287 | return ( 288 | 289 | 0} 297 | isDiffCollapsed={isDiffCollapsed} 298 | setIsDiffCollapsed={(collapsed: boolean, altKey?: boolean) => { 299 | if (altKey) { 300 | return setAllCollapsed(collapsed) 301 | } 302 | 303 | setAllCollapsed(undefined) 304 | setIsDiffCollapsed(collapsed) 305 | }} 306 | isDiffCompleted={isDiffCompleted} 307 | onCopyPathToClipboard={handleCopyPathToClipboard} 308 | copyPathPopoverContent={copyPathPopoverContent} 309 | resetCopyPathPopoverContent={handleResetCopyPathPopoverContent} 310 | onCompleteDiff={onCompleteDiff} 311 | appName={appName} 312 | appPackage={appPackage} 313 | diffComments={diffComments} 314 | packageName={packageName} 315 | /> 316 | 317 | {!isDiffCollapsed && ( 318 | 328 | {(hunks: HunkData[]) => 329 | hunks 330 | .map((_, i) => updatedHunks[i]) 331 | .map((hunk) => ( 332 | 333 | 334 | {hunk.content} 335 | 336 | 342 | 343 | )) 344 | } 345 | 346 | )} 347 | 348 | ) 349 | } 350 | 351 | /* 352 | Return true if passing `nextProps` to render would return 353 | the same result as passing prevProps to render, otherwise return false 354 | */ 355 | const arePropsEqual = (prevProps: DiffProps, nextProps: DiffProps) => 356 | prevProps.isDiffCompleted === nextProps.isDiffCompleted && 357 | prevProps.areAllCollapsed === nextProps.areAllCollapsed && 358 | prevProps.diffViewStyle === nextProps.diffViewStyle && 359 | prevProps.appName === nextProps.appName && 360 | prevProps.appPackage === nextProps.appPackage 361 | 362 | export default React.memo(Diff, arePropsEqual) 363 | -------------------------------------------------------------------------------- /src/components/common/Diff/DiffComment.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import styled from '@emotion/styled' 3 | import { HTMLMotionProps, motion } from 'framer-motion' 4 | import { removeAppPathPrefix, getVersionsContentInDiff } from '../../../utils' 5 | import Markdown from '../Markdown' 6 | import type { Theme } from '../../../theme' 7 | import type { 8 | LineChangeT, 9 | ReleaseCommentT, 10 | ReleaseT, 11 | } from '../../../releases/types' 12 | 13 | interface ContainerProps 14 | extends React.PropsWithChildren> { 15 | isCommentOpen: boolean 16 | lineChangeType: LineChangeT 17 | theme?: Theme 18 | } 19 | 20 | const Container = styled( 21 | ({ isCommentOpen, children, ...props }: ContainerProps) => { 22 | return ( 23 | 38 |
{children}
39 |
40 | ) 41 | } 42 | )` 43 | overflow: hidden; 44 | 45 | & > div { 46 | display: flex; 47 | flex-direction: row; 48 | background-color: ${({ lineChangeType, theme }) => { 49 | const colorMap = { 50 | add: theme.diff.codeInsertBackground, 51 | delete: theme.diff.codeDeleteBackground, 52 | neutral: undefined, 53 | } 54 | 55 | return colorMap[lineChangeType] || theme.background 56 | }}; 57 | cursor: pointer; 58 | } 59 | ` 60 | 61 | interface ContentContainerProps extends React.HTMLAttributes { 62 | theme?: Theme 63 | } 64 | const ContentContainer = styled.div` 65 | flex: 1; 66 | position: relative; 67 | padding: 16px; 68 | color: ${({ theme }) => theme.text}; 69 | background-color: ${({ theme }) => theme.yellowBackground};} 70 | user-select: none; 71 | ` 72 | 73 | interface ShowButtonProps extends HTMLMotionProps<'div'> { 74 | isCommentOpen: boolean 75 | theme?: Theme 76 | } 77 | 78 | const ShowButton = styled(({ isCommentOpen, ...props }: ShowButtonProps) => ( 79 | 96 | ))` 97 | background-color: ${({ theme }) => theme.yellowBorder}; 98 | margin-left: 20px; 99 | width: 10px; 100 | cursor: pointer; 101 | ` 102 | 103 | const Content = styled(Markdown)` 104 | opacity: 1; 105 | ${({ isCommentOpen }) => 106 | !isCommentOpen && 107 | ` 108 | opacity: 0; 109 | `} 110 | transition: opacity 0.25s ease-out; 111 | ` 112 | 113 | const LINE_CHANGE_TYPES = { 114 | ADD: 'I', 115 | DELETE: 'D', 116 | NEUTRAL: 'N', 117 | } 118 | 119 | const getLineNumberWithType = ({ 120 | lineChangeType, 121 | lineNumber, 122 | }: { 123 | lineChangeType: LineChangeT 124 | lineNumber: number 125 | }) => 126 | `${ 127 | LINE_CHANGE_TYPES[ 128 | lineChangeType.toUpperCase() as keyof typeof LINE_CHANGE_TYPES 129 | ] 130 | }${lineNumber}` 131 | 132 | const getComments = ({ 133 | packageName, 134 | newPath, 135 | fromVersion, 136 | toVersion, 137 | }: { 138 | packageName: string 139 | newPath: string 140 | fromVersion: string 141 | toVersion: string 142 | }) => { 143 | const newPathSanitized = removeAppPathPrefix(newPath) 144 | 145 | const versionsInDiff = getVersionsContentInDiff({ 146 | packageName, 147 | fromVersion, 148 | toVersion, 149 | }).filter( 150 | ({ comments }: ReleaseT) => 151 | comments && 152 | comments.length > 0 && 153 | comments.some(({ fileName }) => fileName === newPathSanitized) 154 | ) 155 | 156 | return versionsInDiff.reduce((allComments, version: ReleaseT) => { 157 | const comments = version.comments?.reduce( 158 | ( 159 | versionComments, 160 | { fileName, lineChangeType, lineNumber, content }: ReleaseCommentT 161 | ) => { 162 | if (fileName !== newPathSanitized) { 163 | return versionComments 164 | } 165 | 166 | return { 167 | ...versionComments, 168 | [getLineNumberWithType({ lineChangeType, lineNumber })]: ( 169 | 170 | ), 171 | } 172 | }, 173 | {} 174 | ) 175 | 176 | return { 177 | ...allComments, 178 | ...comments, 179 | } 180 | }, {}) 181 | } 182 | 183 | const DiffComment = ({ 184 | content, 185 | lineChangeType, 186 | }: { 187 | content: any 188 | lineChangeType: LineChangeT 189 | }) => { 190 | const [isCommentOpen, setIsCommentOpen] = useState(true) 191 | 192 | return ( 193 | setIsCommentOpen(!isCommentOpen)} 197 | > 198 | setIsCommentOpen(!isCommentOpen)} 201 | /> 202 | 203 | 204 | 205 | {content.props.children} 206 | 207 | 208 | 209 | ) 210 | } 211 | 212 | export { getComments } 213 | export default DiffComment 214 | -------------------------------------------------------------------------------- /src/components/common/Diff/DiffCommentReminder.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from '@emotion/styled' 3 | import { motion, HTMLMotionProps } from 'framer-motion' 4 | import { InfoCircleOutlined } from '@ant-design/icons' 5 | import { getTransitionDuration } from '../../../utils' 6 | import type { Theme } from '../../../theme' 7 | import type { ReleaseCommentT } from '../../../releases/types' 8 | 9 | interface DiffCommentReminderProps extends HTMLMotionProps<'div'> { 10 | comments: ReleaseCommentT[] 11 | isDiffCollapsed: boolean 12 | uncollapseDiff: () => void 13 | theme?: Theme 14 | } 15 | 16 | const DiffCommentReminder = styled( 17 | ({ 18 | comments, 19 | isDiffCollapsed, 20 | uncollapseDiff, 21 | ...props 22 | }: DiffCommentReminderProps) => { 23 | const numberOfComments = Object.keys(comments).length 24 | const isOpen = isDiffCollapsed && numberOfComments > 0 25 | 26 | return ( 27 | 39 | 40 | 41 | 42 | {numberOfComments} hidden comment{numberOfComments > 1 && 's'} 43 | 44 | 45 | ) 46 | } 47 | )` 48 | display: inline; 49 | background-color: ${({ theme }) => theme.yellowBackground}; 50 | padding: 5px; 51 | border-radius: 3px; 52 | margin-left: 10px; 53 | border: 1px solid ${({ theme }) => theme.yellowBorder}; 54 | 55 | & > .icon { 56 | margin-right: 6px; 57 | } 58 | 59 | & > .reminder { 60 | word-spacing: -2px; 61 | } 62 | ` 63 | 64 | export default DiffCommentReminder 65 | -------------------------------------------------------------------------------- /src/components/common/Diff/DiffHeader.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from '@emotion/styled' 3 | import { Tag, Button, Popover } from 'antd' 4 | import type { ButtonProps, TagProps } from 'antd' 5 | import { 6 | CheckOutlined, 7 | DownOutlined, 8 | RightOutlined, 9 | CopyOutlined, 10 | RollbackOutlined, 11 | LinkOutlined, 12 | } from '@ant-design/icons' 13 | import { getFilePathsToShow } from '../../../utils' 14 | import { CopyToClipboard } from 'react-copy-to-clipboard' 15 | import DiffCommentReminder from './DiffCommentReminder' 16 | import DownloadFileButton from '../DownloadFileButton' 17 | import ViewFileButton from '../ViewFileButton' 18 | import CopyFileButton from '../CopyFileButton' 19 | import type { Theme } from '../../../theme' 20 | import type { ReleaseCommentT } from '../../../releases/types' 21 | import type { DiffType } from 'react-diff-view' 22 | 23 | export const testIDs = { 24 | collapseClickableArea: 'collapseClickableArea', 25 | } 26 | interface WrapperProps extends React.HTMLAttributes { 27 | theme?: Theme 28 | } 29 | const Wrapper = styled.div` 30 | display: flex; 31 | justify-content: space-between; 32 | font-family: SFMono-Regular, Consolas, Liberation Mono, Menlo, Courier, 33 | monospace; 34 | font-size: 12px; 35 | color: ${({ theme }) => theme.text}; 36 | line-height: 32px; 37 | background-color: ${({ theme }) => theme.background}; 38 | border-bottom: 1px solid ${({ theme }) => theme.border}; 39 | border-top-left-radius: 2px; 40 | border-top-right-radius: 2px; 41 | padding: 5px 10px; 42 | position: sticky; 43 | top: 0; 44 | ` 45 | 46 | const FileRenameArrow = styled(RightOutlined)({ 47 | fontSize: '10px', 48 | margin: '0 5px', 49 | color: '#f78206', 50 | }) 51 | 52 | const FileName = ({ 53 | oldPath, 54 | newPath, 55 | type, 56 | }: { 57 | oldPath: string 58 | newPath: string 59 | type: DiffType 60 | }) => { 61 | if (type === 'delete') { 62 | return {oldPath} 63 | } 64 | 65 | if (oldPath !== newPath && type !== 'add') { 66 | return ( 67 | 68 | {oldPath} {newPath} 69 | 70 | ) 71 | } 72 | 73 | return {newPath} 74 | } 75 | 76 | function generatePathId(oldPath: string, newPath: string) { 77 | const isMoved = oldPath !== newPath 78 | if (newPath === '/dev/null') { 79 | newPath = 'deleted' 80 | } 81 | const path = isMoved ? oldPath + '-' + newPath : oldPath 82 | return encodeURIComponent(path.replace(/[/\\]/g, '-')) 83 | } 84 | 85 | const FileStatus = ({ 86 | type, 87 | ...props 88 | }: { 89 | type: DiffType 90 | } & TagProps) => { 91 | const colors = { 92 | add: 'blue', 93 | modify: 'green', 94 | delete: 'red', 95 | rename: 'orange', 96 | } 97 | 98 | const labels = { 99 | add: 'ADDED', 100 | modify: 'MODIFIED', 101 | delete: 'DELETED', 102 | rename: 'RENAMED', 103 | } 104 | 105 | return ( 106 | 107 | {labels[type as keyof typeof labels]} 108 | 109 | ) 110 | } 111 | 112 | interface BinaryBadgeProps extends TagProps { 113 | open: boolean 114 | } 115 | const BinaryBadge = ({ open, ...props }: BinaryBadgeProps) => 116 | open ? ( 117 | 118 | BINARY 119 | 120 | ) : null 121 | 122 | const defaultIconButtonStyle = ` 123 | font-size: 13px; 124 | ` 125 | 126 | interface CompleteDiffButtonProps extends ButtonProps { 127 | open: boolean 128 | onClick: () => void 129 | isDiffCompleted?: boolean 130 | theme?: Theme 131 | } 132 | 133 | const CompleteDiffButton = styled( 134 | ({ open, onClick, ...props }: CompleteDiffButtonProps) => ( 135 | 139 | {open ? ( 140 |
52 | 79 | 80 | 81 | 82 | `) 83 | }) 84 | -------------------------------------------------------------------------------- /src/components/common/Select.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from '@emotion/styled' 3 | import { Select as AntdSelect, Typography } from 'antd' 4 | import type { SelectProps as AntdSelectProps } from 'antd' 5 | 6 | const { Option } = AntdSelect 7 | 8 | const SelectBoxContainer = styled.div` 9 | width: 100%; 10 | ` 11 | const SelectBox = styled(AntdSelect)` 12 | width: 100%; 13 | ` 14 | 15 | export interface SelectProps extends AntdSelectProps { 16 | title: string 17 | options: string[] 18 | } 19 | 20 | const Select = ({ title, options, ...props }: SelectProps) => ( 21 | 22 | {title} 23 | 24 | 25 | {options.map((option) => ( 26 | 29 | ))} 30 | 31 | 32 | ) 33 | 34 | export default Select 35 | -------------------------------------------------------------------------------- /src/components/common/Settings.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import { Popover, Button, Checkbox, Radio, Typography } from 'antd' 3 | import { SHOW_LATEST_RCS } from '../../utils' 4 | import styled from '@emotion/styled' 5 | import { WindowsFilled } from '@ant-design/icons' 6 | import { PACKAGE_NAMES, LANGUAGE_NAMES } from '../../constants' 7 | import { CheckboxValueType } from 'antd/es/checkbox/Group' 8 | 9 | const SettingsButton = styled(Button)` 10 | color: initial; 11 | ` 12 | 13 | const SettingsIcon = styled((props: React.HTMLAttributes) => ( 14 | ⚙️ 15 | ))` 16 | font-family: initial; 17 | ` 18 | 19 | const PlatformsContainer = styled.div` 20 | display: flex; 21 | flex-direction: column; 22 | align-items: start; 23 | margin-top: 12px; 24 | ` 25 | 26 | const PackagesGroupContainer = styled.div` 27 | display: flex; 28 | flex-direction: column; 29 | align-items: start; 30 | ` 31 | 32 | const Settings = ({ 33 | handleSettingsChange, 34 | packageName, 35 | language, 36 | onChangePackageNameAndLanguage, 37 | }: { 38 | handleSettingsChange: (checkboxValues: CheckboxValueType[]) => void 39 | packageName: string 40 | language: string 41 | onChangePackageNameAndLanguage: (params: { 42 | newPackageName?: string 43 | newLanguage: string 44 | }) => void 45 | }) => { 46 | const [popoverVisibility, setVisibility] = useState(false) 47 | const [newPackageName, setNewPackageName] = useState(packageName) 48 | const [newLanguage, setNewLanguage] = useState(language) 49 | 50 | const handleClickChange = (visibility: boolean) => { 51 | setVisibility(visibility) 52 | 53 | const processedNewLanguage = 54 | newLanguage !== language && newPackageName === PACKAGE_NAMES.RNW 55 | ? newLanguage 56 | : LANGUAGE_NAMES.CPP 57 | 58 | if ( 59 | !visibility && 60 | (newPackageName !== packageName || processedNewLanguage !== language) 61 | ) { 62 | onChangePackageNameAndLanguage({ 63 | newPackageName: 64 | newPackageName !== packageName ? newPackageName : undefined, 65 | newLanguage: processedNewLanguage, 66 | }) 67 | } 68 | } 69 | 70 | const updateCheckboxValues = (checkboxValues: CheckboxValueType[]) => 71 | handleSettingsChange(checkboxValues) 72 | 73 | return ( 74 | 78 | 79 |
80 | {SHOW_LATEST_RCS} 81 |
82 |
83 | 84 |
Upgrading another platform?
85 | setNewPackageName(e.target.value)} 88 | > 89 | 90 | react-native 91 | 92 | { 100 | setNewPackageName(PACKAGE_NAMES.RNW) 101 | setNewLanguage(e.target.value) 102 | }} 103 | > 104 | 105 | react-native-windows 106 | 107 | 108 | C++ 109 | C# 110 | 111 | 112 | react-native-macos 113 | 114 | 115 |
116 | 117 | } 118 | trigger="click" 119 | open={popoverVisibility} 120 | onOpenChange={handleClickChange} 121 | > 122 | } /> 123 |
124 | ) 125 | } 126 | 127 | export default Settings 128 | -------------------------------------------------------------------------------- /src/components/common/TroubleshootingGuides.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef } from 'react' 2 | import styled from '@emotion/styled' 3 | import { Link } from './Markdown' 4 | import { HTMLMotionProps, motion, useAnimation } from 'framer-motion' 5 | import type { Theme } from '../../theme' 6 | 7 | const TROUBLESHOOTING_GUIDES = [ 8 | { 9 | title: 'Xcode 12.5', 10 | href: 'https://github.com/facebook/react-native/issues/31480', 11 | }, 12 | { 13 | title: 'Apple Silicon (M1)', 14 | href: 'https://github.com/facebook/react-native/issues/31941', 15 | }, 16 | ] 17 | 18 | const Container = styled(motion.div)>` 19 | width: 250px; 20 | ` 21 | 22 | interface ContentProps extends HTMLMotionProps<'div'> { 23 | theme?: Theme 24 | } 25 | const Content = styled(motion.div)` 26 | h4 { 27 | border-bottom: 1px solid ${({ theme }) => theme.border}; 28 | padding-bottom: 6px; 29 | } 30 | ` 31 | 32 | const ListContainer = styled.ul` 33 | padding-left: 13px; 34 | margin-bottom: 0; 35 | list-style: disc; 36 | ` 37 | 38 | const TroubleshootingGuides = ({ 39 | isTooltipOpen, 40 | }: { 41 | isTooltipOpen: boolean 42 | }) => { 43 | const willHaveAnimation = useRef(isTooltipOpen) 44 | 45 | const containerAnimation = useAnimation() 46 | 47 | return ( 48 | 53 | 54 |

Troubleshooting guides

55 | 56 | {TROUBLESHOOTING_GUIDES.map(({ title, href }) => ( 57 |
  • 58 | {title} 59 |
  • 60 | ))} 61 |
    62 |
    63 |
    64 | ) 65 | } 66 | 67 | export { TroubleshootingGuides } 68 | -------------------------------------------------------------------------------- /src/components/common/TroubleshootingGuidesButton.tsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useState } from 'react' 2 | import { Button as AntdButton, Popover } from 'antd' 3 | import styled from '@emotion/styled' 4 | import { TroubleshootingGuides } from './TroubleshootingGuides' 5 | 6 | export const testIDs = { 7 | troubleshootingGuidesButton: 'troubleshootingGuidesButton', 8 | } 9 | 10 | const Icon = styled.span` 11 | font-family: initial; 12 | ` 13 | 14 | const Button = styled(AntdButton)` 15 | width: 32px; 16 | padding: 0; 17 | color: initial; 18 | ` 19 | 20 | const TroubleshootingGuidesButton = () => { 21 | const [showContent, setShowContent] = useState(false) 22 | const hasHandledClick = useRef(false) 23 | 24 | const handlePopoverVisibilityChange = () => { 25 | if (hasHandledClick.current) { 26 | return 27 | } 28 | 29 | setShowContent(false) 30 | } 31 | 32 | const handleToggleShowContent = () => { 33 | hasHandledClick.current = true 34 | 35 | setShowContent((prevState) => !prevState) 36 | 37 | setTimeout(() => { 38 | hasHandledClick.current = false 39 | }, 0) 40 | } 41 | 42 | return ( 43 | } 46 | trigger="click" 47 | open={showContent} 48 | onOpenChange={handlePopoverVisibilityChange} 49 | > 50 | 56 | 57 | ) 58 | } 59 | 60 | export { TroubleshootingGuidesButton } 61 | -------------------------------------------------------------------------------- /src/components/common/UpgradeButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from '@emotion/styled' 3 | import { Button as AntdButton, ButtonProps } from 'antd' 4 | 5 | export const testIDs = { 6 | upgradeButton: 'upgradeButton', 7 | } 8 | 9 | const Container = styled.div` 10 | display: flex; 11 | justify-content: center; 12 | height: auto; 13 | overflow: hidden; 14 | margin-top: 28px; 15 | ` 16 | 17 | const Button = styled(AntdButton)` 18 | border-radius: 5px; 19 | ` 20 | 21 | interface UpgradeButtonProps extends React.PropsWithRef { 22 | onShowDiff: () => void 23 | } 24 | 25 | const UpgradeButton = React.forwardRef< 26 | HTMLElement, 27 | UpgradeButtonProps & React.RefAttributes 28 | >(({ onShowDiff, ...props }, ref) => ( 29 | 30 | 40 | 41 | )) 42 | 43 | export default UpgradeButton 44 | -------------------------------------------------------------------------------- /src/components/common/UpgradeSupportAlert.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from '@emotion/styled' 3 | import { Tooltip } from 'antd' 4 | import type { Theme } from '../../theme' 5 | 6 | interface UpgradeSupportAlertProps 7 | extends React.HTMLAttributes { 8 | theme?: Theme 9 | } 10 | const UpgradeSupportAlert = styled((props: UpgradeSupportAlertProps) => ( 11 |
    12 | 13 | Check out{' '} 14 | 18 | 23 | Upgrade Support 24 | 25 | {' '} 26 | if you are experiencing issues related to React Native during the 27 | upgrading process. 28 | 29 |
    30 | ))` 31 | span > a { 32 | color: ${({ theme }) => theme.link}}; 33 | } 34 | ` 35 | 36 | export default UpgradeSupportAlert 37 | -------------------------------------------------------------------------------- /src/components/common/UsefulContentSection.tsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import styled from '@emotion/styled' 3 | import { UpOutlined, DownOutlined } from '@ant-design/icons' 4 | import { Button } from 'antd' 5 | import type { ButtonProps } from 'antd' 6 | import { HTMLMotionProps, motion } from 'framer-motion' 7 | 8 | import { 9 | getVersionsContentInDiff, 10 | getChangelogURL, 11 | getTransitionDuration, 12 | } from '../../utils' 13 | import UpgradeSupportAlert from './UpgradeSupportAlert' 14 | import UsefulLinks from './UsefulLinks' 15 | import AlignDepsAlert from './AlignDepsAlert' 16 | 17 | import { PACKAGE_NAMES } from '../../constants' 18 | import type { Theme } from '../../theme' 19 | 20 | const Container = styled.div<{ isContentOpen: boolean }>` 21 | position: relative; 22 | margin-top: 16px; 23 | overflow: hidden; 24 | ` 25 | 26 | const InnerContainer = styled.div<{ theme?: Theme; isContentOpen: boolean }>` 27 | color: ${({ theme }) => 28 | theme.text + 'D9'}; // the D9 adds some transparency to the text color 29 | background-color: ${({ theme }) => theme.yellowBackground}; 30 | border-width: 1px; 31 | border-left-width: 7px; 32 | border-color: ${({ theme }) => theme.yellowBorder}; 33 | border-style: solid; 34 | border-radius: 3px; 35 | transition: padding 0.25s ease-out; 36 | ` 37 | 38 | interface TitleProps extends HTMLMotionProps<'h2'> { 39 | isContentOpen: boolean 40 | } 41 | 42 | const Title = styled(({ isContentOpen, ...props }: TitleProps) => ( 43 | 62 | ))` 63 | font-size: 17px; 64 | cursor: pointer; 65 | margin: 0px; 66 | padding: 18px 0px 0px 14px; 67 | ` 68 | 69 | interface ContentContainerProps 70 | extends React.PropsWithChildren> { 71 | isContentOpen: boolean 72 | } 73 | 74 | const ContentContainer = styled( 75 | ({ isContentOpen, children, ...props }: ContentContainerProps) => ( 76 | 93 |
    {children}
    94 |
    95 | ) 96 | )` 97 | & > div { 98 | padding: 15px 24px 19px; 99 | } 100 | ` 101 | 102 | const Icon = styled((props: React.HTMLAttributes) => ( 103 | 104 | 📣 105 | 106 | ))` 107 | margin: 0px 10px; 108 | ` 109 | 110 | interface HideContentButtonProps extends ButtonProps { 111 | isContentOpen: boolean 112 | toggleContentVisibility: () => void 113 | theme?: Theme 114 | } 115 | 116 | const HideContentButton = styled( 117 | ({ 118 | toggleContentVisibility, 119 | isContentOpen, 120 | ...props 121 | }: HideContentButtonProps) => ( 122 | 27 | ) 28 | } 29 | )` 30 | font-size: 13px; 31 | ` 32 | 33 | export default ViewFileButton 34 | -------------------------------------------------------------------------------- /src/components/common/index.ts: -------------------------------------------------------------------------------- 1 | export { default as Select } from './Select' 2 | -------------------------------------------------------------------------------- /src/components/pages/Home.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useDeferredValue } from 'react' 2 | import styled from '@emotion/styled' 3 | import { ThemeProvider } from '@emotion/react' 4 | import { Card, Input, Typography, ConfigProvider, theme } from 'antd' 5 | import GitHubButton, { ReactGitHubButtonProps } from 'react-github-btn' 6 | import ReactGA from 'react-ga' 7 | import createPersistedState from 'use-persisted-state' 8 | import queryString from 'query-string' 9 | import VersionSelector from '../common/VersionSelector' 10 | import DiffViewer from '../common/DiffViewer' 11 | import Settings from '../common/Settings' 12 | // @ts-ignore-next-line 13 | import logo from '../../assets/logo.svg' 14 | import { SHOW_LATEST_RCS } from '../../utils' 15 | import { useGetLanguageFromURL } from '../../hooks/get-language-from-url' 16 | import { useGetPackageNameFromURL } from '../../hooks/get-package-name-from-url' 17 | import { 18 | DEFAULT_APP_NAME, 19 | DEFAULT_APP_PACKAGE, 20 | PACKAGE_NAMES, 21 | } from '../../constants' 22 | import { TroubleshootingGuidesButton } from '../common/TroubleshootingGuidesButton' 23 | import { DarkModeButton } from '../common/DarkModeButton' 24 | import { updateURL } from '../../utils/update-url' 25 | import { deviceSizes } from '../../utils/device-sizes' 26 | import { lightTheme, darkTheme, type Theme } from '../../theme' 27 | 28 | const Page = styled.div<{ theme?: Theme }>` 29 | background-color: ${({ theme }) => theme.background}; 30 | display: flex; 31 | align-items: center; 32 | justify-content: center; 33 | flex-direction: column; 34 | padding-top: 30px; 35 | ` 36 | 37 | const Container = styled(Card)<{ theme?: Theme }>` 38 | background-color: ${({ theme }) => theme.background}; 39 | width: 90%; 40 | border-radius: 3px; 41 | border-color: ${({ theme }) => theme.border}; 42 | ` 43 | 44 | const HeaderContainer = styled.div` 45 | display: flex; 46 | flex-direction: column; 47 | align-items: center; 48 | 49 | @media ${deviceSizes.tablet} { 50 | flex-direction: row; 51 | } 52 | ` 53 | 54 | const LogoImg = styled.img` 55 | width: 50px; 56 | margin-bottom: 15px; 57 | 58 | @media ${deviceSizes.tablet} { 59 | width: 100px; 60 | } 61 | ` 62 | 63 | const TitleHeader = styled.h1` 64 | margin: 0; 65 | margin-left: 15px; 66 | ` 67 | 68 | const TitleContainer = styled.div` 69 | display: flex; 70 | align-items: center; 71 | margin-bottom: 8px; 72 | ` 73 | 74 | const AppNameField = styled.div` 75 | width: 100%; 76 | ` 77 | 78 | const AppPackageField = styled.div` 79 | width: 100%; 80 | ` 81 | 82 | const AppDetailsContainer = styled.div` 83 | display: flex; 84 | flex-direction: column; 85 | gap: 16px; 86 | 87 | @media ${deviceSizes.tablet} { 88 | flex-direction: row; 89 | } 90 | ` 91 | 92 | const SettingsContainer = styled.div` 93 | display: flex; 94 | align-items: center; 95 | justify-content: center; 96 | gap: 15px; 97 | margin-bottom: 8px; 98 | flex: 1; 99 | ` 100 | 101 | const getAppInfoInURL = () => { 102 | // Parses `/?name=RnDiffApp&package=com.rndiffapp` from URL 103 | const { name, package: pkg } = queryString.parse(window.location.search) 104 | 105 | return { 106 | appPackage: pkg as string, 107 | appName: name as string | null, 108 | } 109 | } 110 | 111 | interface StarButtonProps extends ReactGitHubButtonProps { 112 | className?: string 113 | } 114 | 115 | const StarButton = styled(({ className, ...props }: StarButtonProps) => ( 116 |
    117 | 118 |
    119 | ))` 120 | margin-top: 10px; 121 | margin-left: 15px; 122 | margin-right: auto; 123 | ` 124 | 125 | // Set up a persisted state hook for for dark mode so users coming back 126 | // will have dark mode automatically if they've selected it previously. 127 | const useDarkModeState = createPersistedState('darkMode') 128 | 129 | const Home = () => { 130 | const { packageName: defaultPackageName, isPackageNameDefinedInURL } = 131 | useGetPackageNameFromURL() 132 | const defaultLanguage = useGetLanguageFromURL() 133 | const [packageName, setPackageName] = useState(defaultPackageName) 134 | const [language, setLanguage] = useState(defaultLanguage) 135 | const [fromVersion, setFromVersion] = useState('') 136 | const [toVersion, setToVersion] = useState('') 137 | const [shouldShowDiff, setShouldShowDiff] = useState(false) 138 | const [settings, setSettings] = useState>({ 139 | [`${SHOW_LATEST_RCS}`]: false, 140 | }) 141 | 142 | const appInfoInURL = getAppInfoInURL() 143 | const [appName, setAppName] = useState(appInfoInURL.appName) 144 | const [appPackage, setAppPackage] = useState(appInfoInURL.appPackage) 145 | 146 | // Avoid UI lag when typing. 147 | const deferredAppName = useDeferredValue(appName || DEFAULT_APP_NAME) 148 | const deferredAppPackage = useDeferredValue(appPackage) 149 | 150 | const homepageUrl = process.env.PUBLIC_URL 151 | 152 | useEffect(() => { 153 | if (process.env.NODE_ENV === 'production') { 154 | ReactGA.initialize('UA-136307971-1') 155 | ReactGA.pageview('/') 156 | } 157 | }, []) 158 | 159 | const handleShowDiff = ({ 160 | fromVersion, 161 | toVersion, 162 | }: { 163 | fromVersion: string 164 | toVersion: string 165 | }) => { 166 | if (fromVersion === toVersion) { 167 | return 168 | } 169 | 170 | setFromVersion(fromVersion) 171 | setToVersion(toVersion) 172 | setShouldShowDiff(true) 173 | } 174 | 175 | const handlePackageNameAndLanguageChange = ({ 176 | newPackageName, 177 | newLanguage, 178 | }: { 179 | newPackageName?: string 180 | newLanguage: string 181 | }) => { 182 | let localPackageName = 183 | newPackageName === undefined ? packageName : newPackageName 184 | let localLanguage = newLanguage === undefined ? language : newLanguage 185 | 186 | updateURL({ 187 | packageName: localPackageName, 188 | language: localLanguage, 189 | isPackageNameDefinedInURL: 190 | isPackageNameDefinedInURL || newPackageName !== undefined, 191 | toVersion: '', 192 | fromVersion: '', 193 | }) 194 | setPackageName(localPackageName) 195 | setLanguage(localLanguage) 196 | setFromVersion('') 197 | setToVersion('') 198 | setShouldShowDiff(false) 199 | } 200 | 201 | const handleSettingsChange = (settingsValues: string[]) => { 202 | const normalizedIncomingSettings = settingsValues.reduce((acc, val) => { 203 | acc[val] = true 204 | return acc 205 | }, {}) 206 | 207 | setSettings(normalizedIncomingSettings) 208 | } 209 | 210 | // Dark Mode Setup: 211 | const { defaultAlgorithm, darkAlgorithm } = theme // Get default and dark mode states from antd. 212 | const [isDarkMode, setIsDarkMode] = useDarkModeState(false) // Remembers dark mode state between sessions. 213 | const toggleDarkMode = () => 214 | setIsDarkMode((previousValue: boolean) => !previousValue) 215 | const themeString = isDarkMode ? 'dark' : 'light' 216 | useEffect(() => { 217 | // Set the document's background color to the theme's body color. 218 | document.body.style.backgroundColor = isDarkMode 219 | ? darkTheme.background 220 | : lightTheme.background 221 | }, [isDarkMode]) 222 | 223 | return ( 224 | 229 | 230 | 231 | 232 | 233 | 234 | 239 | 240 | React Native Upgrade Helper 241 | 242 | 243 | 244 | 245 | 252 | Star 253 | 254 | {packageName === PACKAGE_NAMES.RN && ( 255 | 256 | )} 257 | 265 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | What's your app name? 276 | 277 | 278 | setAppName((value) => target.value)} 283 | /> 284 | 285 | 286 | 287 | 288 | What's your app package? 289 | 290 | 291 | 296 | setAppPackage((value) => target.value) 297 | } 298 | /> 299 | 300 | 301 | 311 | 312 | {/* 313 | Pass empty values for app name and package if they're the defaults to 314 | hint to diffing components they don't need to further patch the 315 | rn-diff-purge output. 316 | */} 317 | 331 | 332 | 333 | 334 | ) 335 | } 336 | 337 | export default Home 338 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export const DEFAULT_APP_NAME = 'RnDiffApp' 2 | export const DEFAULT_APP_PACKAGE = 'com.rndiffapp' 3 | 4 | export const PACKAGE_NAMES = { 5 | RN: 'react-native', 6 | RNM: 'react-native-macos', 7 | RNW: 'react-native-windows', 8 | } 9 | 10 | export const LANGUAGE_NAMES = { 11 | CPP: 'cpp', 12 | CS: 'cs', 13 | } 14 | 15 | export const RN_DIFF_REPOSITORIES = { 16 | [PACKAGE_NAMES.RN]: 'react-native-community/rn-diff-purge', 17 | [PACKAGE_NAMES.RNM]: 'acoates-ms/rnw-diff', 18 | [PACKAGE_NAMES.RNW]: 'acoates-ms/rnw-diff', 19 | } 20 | 21 | export const RN_CHANGELOG_URLS = { 22 | [PACKAGE_NAMES.RN]: 23 | 'https://github.com/facebook/react-native/blob/main/CHANGELOG.md', 24 | [PACKAGE_NAMES.RNM]: 25 | 'https://github.com/microsoft/react-native-macos/releases/tag/', 26 | [PACKAGE_NAMES.RNW]: 27 | 'https://github.com/microsoft/react-native-windows/releases/tag/react-native-windows_', 28 | } 29 | -------------------------------------------------------------------------------- /src/hooks/fetch-diff.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | import { parseDiff } from 'react-diff-view' 3 | import type { File } from 'gitdiff-parser' 4 | import { getDiffURL } from '../utils' 5 | 6 | const delay = (ms: number) => new Promise((res) => setTimeout(res, ms)) 7 | 8 | const movePackageJsonToTop = (parsedDiff: File[]) => 9 | parsedDiff.sort(({ newPath }) => (newPath.includes('package.json') ? -1 : 1)) 10 | 11 | interface UseFetchDiffProps { 12 | shouldShowDiff: boolean 13 | packageName: string 14 | language: string 15 | fromVersion: string 16 | toVersion: string 17 | } 18 | export const useFetchDiff = ({ 19 | shouldShowDiff, 20 | packageName, 21 | language, 22 | fromVersion, 23 | toVersion, 24 | }: UseFetchDiffProps) => { 25 | const [isLoading, setIsLoading] = useState(true) 26 | const [isDone, setIsDone] = useState(false) 27 | const [diff, setDiff] = useState([]) 28 | 29 | useEffect(() => { 30 | const fetchDiff = async () => { 31 | setIsLoading(true) 32 | setIsDone(false) 33 | 34 | const [response] = await Promise.all([ 35 | fetch(getDiffURL({ packageName, language, fromVersion, toVersion })), 36 | delay(300), 37 | ]) 38 | 39 | const diff = await response.text() 40 | 41 | setDiff(movePackageJsonToTop(parseDiff(diff))) 42 | 43 | setIsLoading(false) 44 | setIsDone(true) 45 | 46 | return 47 | } 48 | 49 | if (shouldShowDiff) { 50 | fetchDiff() 51 | } 52 | }, [shouldShowDiff, packageName, language, fromVersion, toVersion]) 53 | 54 | return { 55 | isLoading, 56 | isDone, 57 | diff, 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/hooks/fetch-release-versions.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | import { getReleasesFileURL } from '../utils' 3 | 4 | export const useFetchReleaseVersions = ({ 5 | packageName, 6 | }: { 7 | packageName: string 8 | }) => { 9 | const [isLoading, setIsLoading] = useState(true) 10 | const [isDone, setIsDone] = useState(false) 11 | const [releaseVersions, setReleaseVersions] = useState([]) 12 | 13 | useEffect(() => { 14 | const fetchReleaseVersions = async () => { 15 | setIsLoading(true) 16 | setIsDone(false) 17 | 18 | const response = await fetch(getReleasesFileURL({ packageName })) 19 | 20 | const releaseVersions = (await response.text()) 21 | .split('\n') 22 | .filter(Boolean) 23 | 24 | setReleaseVersions(releaseVersions) 25 | 26 | setIsLoading(false) 27 | setIsDone(true) 28 | 29 | return 30 | } 31 | 32 | fetchReleaseVersions() 33 | }, [packageName]) 34 | 35 | return { 36 | isLoading, 37 | isDone, 38 | releaseVersions, 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/hooks/get-language-from-url.ts: -------------------------------------------------------------------------------- 1 | import { LANGUAGE_NAMES } from '../constants' 2 | 3 | export const useGetLanguageFromURL = () => { 4 | const urlParams = new URLSearchParams(window.location.search) 5 | 6 | const languageFromURL = urlParams.get('language') 7 | const languageNames = Object.values(LANGUAGE_NAMES) 8 | 9 | if (!languageFromURL || !languageNames.includes(languageFromURL)) { 10 | return LANGUAGE_NAMES.CPP 11 | } 12 | 13 | return languageFromURL 14 | } 15 | -------------------------------------------------------------------------------- /src/hooks/get-package-name-from-url.ts: -------------------------------------------------------------------------------- 1 | import { PACKAGE_NAMES } from '../constants' 2 | 3 | export const useGetPackageNameFromURL = () => { 4 | const urlParams = new URLSearchParams(window.location.search) 5 | 6 | const packageNameFromURL = urlParams.get('package') 7 | const packageNames = Object.values(PACKAGE_NAMES) 8 | 9 | if (!packageNameFromURL || !packageNames.includes(packageNameFromURL)) { 10 | return { 11 | packageName: PACKAGE_NAMES.RN, 12 | isPackageNameDefinedInURL: false, 13 | } 14 | } 15 | 16 | return { 17 | packageName: packageNameFromURL, 18 | isPackageNameDefinedInURL: true, 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | /* @import '~antd/dist/antd.css'; */ 2 | 3 | body { 4 | margin: 0; 5 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 6 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 7 | sans-serif; 8 | -webkit-font-smoothing: antialiased; 9 | -moz-osx-font-smoothing: grayscale; 10 | } 11 | 12 | code { 13 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 14 | monospace; 15 | } 16 | 17 | .ant-checkbox-wrapper+.ant-checkbox-wrapper { 18 | margin-left: 0px; 19 | } -------------------------------------------------------------------------------- /src/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { createRoot } from 'react-dom/client' 3 | import './index.css' 4 | import App from './App' 5 | import * as serviceWorker from './serviceWorker' 6 | 7 | const container = document.getElementById('root') 8 | const root = createRoot(container) 9 | root.render() 10 | 11 | // If you want your app to work offline and load faster, you can change 12 | // unregister() to register() below. Note this comes with some pitfalls. 13 | // Learn more about service workers: https://bit.ly/CRA-PWA 14 | serviceWorker.unregister() 15 | -------------------------------------------------------------------------------- /src/mocks/repositoryInfo.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 1, 3 | "node_id": "XX=", 4 | "name": "upgrade-helper", 5 | "full_name": "react-native-community/upgrade-helper", 6 | "private": false, 7 | "owner": { 8 | "login": "react-native-community", 9 | "id": 1, 10 | "node_id": "XX", 11 | "avatar_url": "https://avatars.githubusercontent.com/u/20269980?v=4", 12 | "gravatar_id": "", 13 | "url": "https://api.github.com/users/react-native-community", 14 | "html_url": "https://github.com/react-native-community", 15 | "followers_url": "https://api.github.com/users/react-native-community/followers", 16 | "following_url": "https://api.github.com/users/react-native-community/following{/other_user}", 17 | "gists_url": "https://api.github.com/users/react-native-community/gists{/gist_id}", 18 | "starred_url": "https://api.github.com/users/react-native-community/starred{/owner}{/repo}", 19 | "subscriptions_url": "https://api.github.com/users/react-native-community/subscriptions", 20 | "organizations_url": "https://api.github.com/users/react-native-community/orgs", 21 | "repos_url": "https://api.github.com/users/react-native-community/repos", 22 | "events_url": "https://api.github.com/users/react-native-community/events{/privacy}", 23 | "received_events_url": "https://api.github.com/users/react-native-community/received_events", 24 | "type": "Organization", 25 | "site_admin": false 26 | }, 27 | "html_url": "https://github.com/react-native-community/upgrade-helper", 28 | "description": "⚛️ A web tool to support React Native developers in upgrading their apps.", 29 | "fork": false, 30 | "url": "https://api.github.com/repos/react-native-community/upgrade-helper", 31 | "forks_url": "https://api.github.com/repos/react-native-community/upgrade-helper/forks", 32 | "keys_url": "https://api.github.com/repos/react-native-community/upgrade-helper/keys{/key_id}", 33 | "collaborators_url": "https://api.github.com/repos/react-native-community/upgrade-helper/collaborators{/collaborator}", 34 | "teams_url": "https://api.github.com/repos/react-native-community/upgrade-helper/teams", 35 | "hooks_url": "https://api.github.com/repos/react-native-community/upgrade-helper/hooks", 36 | "issue_events_url": "https://api.github.com/repos/react-native-community/upgrade-helper/issues/events{/number}", 37 | "events_url": "https://api.github.com/repos/react-native-community/upgrade-helper/events", 38 | "assignees_url": "https://api.github.com/repos/react-native-community/upgrade-helper/assignees{/user}", 39 | "branches_url": "https://api.github.com/repos/react-native-community/upgrade-helper/branches{/branch}", 40 | "tags_url": "https://api.github.com/repos/react-native-community/upgrade-helper/tags", 41 | "blobs_url": "https://api.github.com/repos/react-native-community/upgrade-helper/git/blobs{/sha}", 42 | "git_tags_url": "https://api.github.com/repos/react-native-community/upgrade-helper/git/tags{/sha}", 43 | "git_refs_url": "https://api.github.com/repos/react-native-community/upgrade-helper/git/refs{/sha}", 44 | "trees_url": "https://api.github.com/repos/react-native-community/upgrade-helper/git/trees{/sha}", 45 | "statuses_url": "https://api.github.com/repos/react-native-community/upgrade-helper/statuses/{sha}", 46 | "languages_url": "https://api.github.com/repos/react-native-community/upgrade-helper/languages", 47 | "stargazers_url": "https://api.github.com/repos/react-native-community/upgrade-helper/stargazers", 48 | "contributors_url": "https://api.github.com/repos/react-native-community/upgrade-helper/contributors", 49 | "subscribers_url": "https://api.github.com/repos/react-native-community/upgrade-helper/subscribers", 50 | "subscription_url": "https://api.github.com/repos/react-native-community/upgrade-helper/subscription", 51 | "commits_url": "https://api.github.com/repos/react-native-community/upgrade-helper/commits{/sha}", 52 | "git_commits_url": "https://api.github.com/repos/react-native-community/upgrade-helper/git/commits{/sha}", 53 | "comments_url": "https://api.github.com/repos/react-native-community/upgrade-helper/comments{/number}", 54 | "issue_comment_url": "https://api.github.com/repos/react-native-community/upgrade-helper/issues/comments{/number}", 55 | "contents_url": "https://api.github.com/repos/react-native-community/upgrade-helper/contents/{+path}", 56 | "compare_url": "https://api.github.com/repos/react-native-community/upgrade-helper/compare/{base}...{head}", 57 | "merges_url": "https://api.github.com/repos/react-native-community/upgrade-helper/merges", 58 | "archive_url": "https://api.github.com/repos/react-native-community/upgrade-helper/{archive_format}{/ref}", 59 | "downloads_url": "https://api.github.com/repos/react-native-community/upgrade-helper/downloads", 60 | "issues_url": "https://api.github.com/repos/react-native-community/upgrade-helper/issues{/number}", 61 | "pulls_url": "https://api.github.com/repos/react-native-community/upgrade-helper/pulls{/number}", 62 | "milestones_url": "https://api.github.com/repos/react-native-community/upgrade-helper/milestones{/number}", 63 | "notifications_url": "https://api.github.com/repos/react-native-community/upgrade-helper/notifications{?since,all,participating}", 64 | "labels_url": "https://api.github.com/repos/react-native-community/upgrade-helper/labels{/name}", 65 | "releases_url": "https://api.github.com/repos/react-native-community/upgrade-helper/releases{/id}", 66 | "deployments_url": "https://api.github.com/repos/react-native-community/upgrade-helper/deployments", 67 | "created_at": "2019-03-13T19:52:36Z", 68 | "updated_at": "2021-06-28T17:36:11Z", 69 | "pushed_at": "2021-06-29T08:26:23Z", 70 | "git_url": "git://github.com/react-native-community/upgrade-helper.git", 71 | "ssh_url": "git@github.com:react-native-community/upgrade-helper.git", 72 | "clone_url": "https://github.com/react-native-community/upgrade-helper.git", 73 | "svn_url": "https://github.com/react-native-community/upgrade-helper", 74 | "homepage": "https://react-native-community.github.io/upgrade-helper", 75 | "size": 26796, 76 | "stargazers_count": 2050, 77 | "watchers_count": 2050, 78 | "language": "JavaScript", 79 | "has_issues": true, 80 | "has_projects": true, 81 | "has_downloads": true, 82 | "has_wiki": false, 83 | "has_pages": true, 84 | "forks_count": 54, 85 | "mirror_url": null, 86 | "archived": false, 87 | "disabled": false, 88 | "open_issues_count": 18, 89 | "license": { 90 | "key": "mit", 91 | "name": "MIT License", 92 | "spdx_id": "MIT", 93 | "url": "https://api.github.com/licenses/mit", 94 | "node_id": "MDc6TGljZW5zZTEz" 95 | }, 96 | "forks": 54, 97 | "open_issues": 18, 98 | "watchers": 2050, 99 | "default_branch": "master", 100 | "temp_clone_token": null, 101 | "organization": { 102 | "login": "react-native-community", 103 | "id": 20269980, 104 | "node_id": "MDEyOk9yZ2FuaXphdGlvbjIwMjY5OTgw", 105 | "avatar_url": "https://avatars.githubusercontent.com/u/20269980?v=4", 106 | "gravatar_id": "", 107 | "url": "https://api.github.com/users/react-native-community", 108 | "html_url": "https://github.com/react-native-community", 109 | "followers_url": "https://api.github.com/users/react-native-community/followers", 110 | "following_url": "https://api.github.com/users/react-native-community/following{/other_user}", 111 | "gists_url": "https://api.github.com/users/react-native-community/gists{/gist_id}", 112 | "starred_url": "https://api.github.com/users/react-native-community/starred{/owner}{/repo}", 113 | "subscriptions_url": "https://api.github.com/users/react-native-community/subscriptions", 114 | "organizations_url": "https://api.github.com/users/react-native-community/orgs", 115 | "repos_url": "https://api.github.com/users/react-native-community/repos", 116 | "events_url": "https://api.github.com/users/react-native-community/events{/privacy}", 117 | "received_events_url": "https://api.github.com/users/react-native-community/received_events", 118 | "type": "Organization", 119 | "site_admin": false 120 | }, 121 | "network_count": 54, 122 | "subscribers_count": 19 123 | } 124 | -------------------------------------------------------------------------------- /src/releases/__mocks__/index.ts: -------------------------------------------------------------------------------- 1 | import { PACKAGE_NAMES } from '../../constants' 2 | 3 | const fixtureVersions = ['0.59', '0.58', '0.57', '0.56'] 4 | 5 | jest.setMock('../index.js', { 6 | [PACKAGE_NAMES.RN]: fixtureVersions.map((version) => ({ 7 | version, 8 | })), 9 | }) 10 | -------------------------------------------------------------------------------- /src/releases/index.js: -------------------------------------------------------------------------------- 1 | import { PACKAGE_NAMES } from '../constants' 2 | 3 | const versionsWithContent = { 4 | [PACKAGE_NAMES.RN]: [ 5 | '0.77', 6 | '0.73', 7 | '0.74', 8 | '0.72', 9 | '0.71', 10 | '0.69', 11 | '0.68', 12 | '0.64', 13 | '0.62', 14 | '0.61', 15 | '0.60', 16 | '0.59', 17 | '0.58', 18 | '0.57', 19 | ], 20 | [PACKAGE_NAMES.RNM]: [], 21 | [PACKAGE_NAMES.RNW]: [], 22 | } 23 | 24 | const getReleaseVersionFiles = (packageName) => 25 | versionsWithContent[packageName].map((version) => ({ 26 | ...require(`./${packageName}/${version}`).default, 27 | version, 28 | })) 29 | 30 | export default { 31 | [PACKAGE_NAMES.RN]: getReleaseVersionFiles(PACKAGE_NAMES.RN), 32 | [PACKAGE_NAMES.RNM]: getReleaseVersionFiles(PACKAGE_NAMES.RNM), 33 | [PACKAGE_NAMES.RNW]: getReleaseVersionFiles(PACKAGE_NAMES.RNW), 34 | } 35 | -------------------------------------------------------------------------------- /src/releases/react-native/0.57.tsx: -------------------------------------------------------------------------------- 1 | import type { ReleaseT } from '../types' 2 | 3 | const release: ReleaseT = { 4 | usefulContent: { 5 | description: 6 | 'React Native 0.57 includes 599 commits by 73 different contributors, it has improvements to Accessibility APIs, Babel 7 stable support and more.', 7 | links: [ 8 | { 9 | title: '[External] Tutorial on upgrading to React Native 0.57', 10 | url: 'https://reactnative.thenativebits.com/courses/upgrading-react-native/upgrade-to-react-native-0.57/', 11 | }, 12 | ], 13 | }, 14 | } 15 | 16 | export default release 17 | -------------------------------------------------------------------------------- /src/releases/react-native/0.58.tsx: -------------------------------------------------------------------------------- 1 | import type { ReleaseT } from '../types' 2 | 3 | const release: ReleaseT = { 4 | usefulContent: { 5 | description: 6 | 'React Native 0.58 is the first release of 2019, it includes work for modernizing and strengthening flow types for core components and numerous crash fixes and resolutions for unexpected behaviors.', 7 | links: [ 8 | { 9 | title: '[External] Tutorial on upgrading to React Native 0.58', 10 | url: 'https://reactnative.thenativebits.com/courses/upgrading-react-native/upgrade-to-react-native-0.58/', 11 | }, 12 | ], 13 | }, 14 | } 15 | 16 | export default release 17 | -------------------------------------------------------------------------------- /src/releases/react-native/0.59.tsx: -------------------------------------------------------------------------------- 1 | import type { ReleaseT } from '../types' 2 | 3 | const release: ReleaseT = { 4 | usefulContent: { 5 | description: 6 | 'React Native 0.59 includes React Hooks, performance gains on Android and lots of cool stuff.', 7 | links: [ 8 | { 9 | title: 10 | 'Official blog post about the major changes on React Native 0.59', 11 | url: 'https://facebook.github.io/react-native/blog/2019/03/12/releasing-react-native-059', 12 | }, 13 | { 14 | title: '[External] Tutorial on upgrading to React Native 0.59', 15 | url: 'https://reactnative.thenativebits.com/courses/upgrading-react-native/upgrade-to-react-native-0.59/', 16 | }, 17 | ], 18 | }, 19 | } 20 | 21 | export default release 22 | -------------------------------------------------------------------------------- /src/releases/react-native/0.60.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react' 2 | import type { ReleaseT } from '../types' 3 | 4 | const release: ReleaseT = { 5 | usefulContent: { 6 | description: 7 | 'React Native 0.60 includes Cocoapods integration by default, AndroidX support, auto-linking libraries, a brand new Start screen and more.', 8 | links: [ 9 | { 10 | title: 11 | 'Official blog post about the major changes on React Native 0.60', 12 | url: 'https://facebook.github.io/react-native/blog/2019/07/03/version-60', 13 | }, 14 | { 15 | title: '[External] Tutorial on upgrading to React Native 0.60', 16 | url: 'https://reactnative.thenativebits.com/courses/upgrading-react-native/upgrade-to-react-native-0.60/', 17 | }, 18 | ], 19 | }, 20 | comments: [ 21 | { 22 | fileName: 'ios/Podfile', 23 | lineNumber: 4, 24 | lineChangeType: 'add', 25 | content: ( 26 | 27 | All these libraries below have been removed from the Xcode project 28 | file and now live in the Podfile. Cocoapods handles the linking now. 29 | Here you can add more libraries with native modules. 30 | 31 | ), 32 | }, 33 | { 34 | fileName: 'ios/RnDiffApp.xcodeproj/project.pbxproj', 35 | lineNumber: 9, 36 | lineChangeType: 'neutral', 37 | content: ( 38 | 39 | Click 40 | [here](https://github.com/react-native-community/upgrade-support/issues/14) 41 | for an explanation and some help with upgrading this file. 42 | 43 | ), 44 | }, 45 | ], 46 | } 47 | export default release 48 | -------------------------------------------------------------------------------- /src/releases/react-native/0.61.tsx: -------------------------------------------------------------------------------- 1 | import type { ReleaseT } from '../types' 2 | 3 | const release: ReleaseT = { 4 | usefulContent: { 5 | description: 'React Native 0.61 includes Fast Refresh and more.', 6 | links: [ 7 | { 8 | title: 9 | 'Official blog post about the major changes on React Native 0.61', 10 | url: 'https://facebook.github.io/react-native/blog/2019/09/18/version-0.61', 11 | }, 12 | { 13 | title: '[External] Tutorial on upgrading to React Native 0.61', 14 | url: 'https://reactnative.thenativebits.com/courses/upgrading-react-native/upgrade-to-react-native-0.61/', 15 | }, 16 | ], 17 | }, 18 | } 19 | 20 | export default release 21 | -------------------------------------------------------------------------------- /src/releases/react-native/0.62.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react' 2 | import type { ReleaseT } from '../types' 3 | 4 | const release: ReleaseT = { 5 | usefulContent: { 6 | description: 7 | 'React Native 0.62 includes built-in integration with Flipper.', 8 | links: [ 9 | { 10 | title: 11 | 'Official blog post about the major changes on React Native 0.62', 12 | url: 'https://reactnative.dev/blog/2020/03/26/version-0.62', 13 | }, 14 | { 15 | title: '[External] Tutorial on upgrading to React Native 0.62', 16 | url: 'https://reactnative.thenativebits.com/courses/upgrading-react-native/upgrade-to-react-native-0.62/', 17 | }, 18 | { 19 | title: 20 | '[iOS] Tutorial on upgrading Xcode-related files to React Native 0.62', 21 | url: 'https://github.com/react-native-community/upgrade-helper/issues/191', 22 | }, 23 | ], 24 | }, 25 | comments: [ 26 | { 27 | fileName: 'ios/RnDiffApp.xcodeproj/project.pbxproj', 28 | lineNumber: 19, 29 | lineChangeType: 'add', 30 | content: ( 31 | 32 | Click 33 | [here](https://github.com/react-native-community/upgrade-support/issues/13) 34 | for an explanation and some help with upgrading this file. 35 | 36 | ), 37 | }, 38 | { 39 | fileName: 'android/app/build.gradle', 40 | lineNumber: 81, 41 | lineChangeType: 'neutral', 42 | content: ( 43 | 44 | If you are using Hermes Engine and ProGuard, make sure to update the 45 | rules in `proguard-rules.pro` to what is specified in the 46 | [documentation](https://reactnative.dev/docs/hermes) for `0.62`. 47 | 48 | ), 49 | }, 50 | ], 51 | } 52 | 53 | export default release 54 | -------------------------------------------------------------------------------- /src/releases/react-native/0.64.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react' 2 | import type { ReleaseT } from '../types' 3 | 4 | const release: ReleaseT = { 5 | usefulContent: { 6 | description: 7 | 'React Native 0.64 includes Hermes opt-in on iOS and React 17.', 8 | links: [ 9 | { 10 | title: 11 | 'Official blog post about the major changes on React Native 0.64', 12 | url: 'https://reactnative.dev/blog/2021/03/12/version-0.64', 13 | }, 14 | ], 15 | }, 16 | comments: [ 17 | { 18 | fileName: 'package.json', 19 | lineNumber: 14, 20 | lineChangeType: 'add', 21 | content: ( 22 | 23 | If you have the `hermes-engine` dependency you need to upgrade to 24 | 0.7.2 [see release 25 | here](https://github.com/facebook/hermes/releases/tag/v0.7.2) if you 26 | are on a previous version you might get crashes at boot on Android. 27 | 28 | ), 29 | }, 30 | ], 31 | } 32 | 33 | export default release 34 | -------------------------------------------------------------------------------- /src/releases/react-native/0.68.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react' 2 | import { ReleaseCommentT, ReleaseT } from '../types' 3 | 4 | const newArchitectureFiles = [ 5 | 'android/app/src/main/java/com/rndiffapp/newarchitecture/MainApplicationReactNativeHost.java', 6 | 'android/app/src/main/java/com/rndiffapp/newarchitecture/components/MainComponentsRegistry.java', 7 | 'android/app/src/main/java/com/rndiffapp/newarchitecture/modules/MainApplicationTurboModuleManagerDelegate.java', 8 | 'android/app/src/main/jni/MainApplicationModuleProvider.h', 9 | 'android/app/src/main/jni/MainComponentsRegistry.cpp', 10 | 'android/app/src/main/jni/Android.mk', 11 | 'android/app/src/main/jni/MainApplicationModuleProvider.cpp', 12 | 'android/app/src/main/jni/MainApplicationTurboModuleManagerDelegate.cpp', 13 | 'android/app/src/main/jni/MainApplicationTurboModuleManagerDelegate.h', 14 | 'android/app/src/main/jni/MainComponentsRegistry.h', 15 | 'android/app/src/main/jni/OnLoad.cpp', 16 | ] 17 | 18 | const release: ReleaseT = { 19 | usefulContent: { 20 | description: 21 | 'React Native 0.68 includes preview of the New Architecture opt-in.', 22 | links: [ 23 | { 24 | title: 25 | 'See here to learn more about new architecture and how to enable it in your project', 26 | url: 'https://reactnative.dev/docs/next/new-architecture-intro', 27 | }, 28 | ], 29 | }, 30 | comments: [ 31 | { 32 | fileName: 'android/app/build.gradle', 33 | lineNumber: 142, 34 | lineChangeType: 'add', 35 | content: ( 36 | 37 | `isNewArchitectureEnabled()` and related logic is optional if you are 38 | not planning to upgrade to the new architecture. 39 | 40 | ), 41 | }, 42 | { 43 | fileName: 'android/app/build.gradle', 44 | lineNumber: 283, 45 | lineChangeType: 'add', 46 | content: ( 47 | 48 | `isNewArchitectureEnabled()` and related logic is optional if you are 49 | not planning to upgrade to the new architecture. 50 | 51 | ), 52 | }, 53 | { 54 | fileName: 'android/app/src/main/java/com/rndiffapp/MainActivity.java', 55 | lineNumber: 36, 56 | lineChangeType: 'add', 57 | content: ( 58 | 59 | New delegate and enabling Fabric in `ReactRootView` is only required 60 | for the new architecture builds. 61 | 62 | ), 63 | }, 64 | { 65 | fileName: 'ios/RnDiffApp/AppDelegate.mm', 66 | lineNumber: 9, 67 | lineChangeType: 'add', 68 | content: ( 69 | 70 | Parts under `RCT_NEW_ARCH_ENABLED` are only required for the new 71 | architecture builds. 72 | 73 | ), 74 | }, 75 | ...newArchitectureFiles.map( 76 | (file) => 77 | ({ 78 | fileName: file, 79 | 80 | lineNumber: 1, 81 | lineChangeType: 'add', 82 | content: ( 83 | 84 | This file is only required for the New Architecture setup. 85 | 86 | ), 87 | } as ReleaseCommentT) 88 | ), 89 | ], 90 | } 91 | 92 | export default release 93 | -------------------------------------------------------------------------------- /src/releases/react-native/0.69.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react' 2 | import type { ReleaseT } from '../types' 3 | 4 | const release: ReleaseT = { 5 | usefulContent: { 6 | description: 7 | 'React Native 0.69 includes a bundled version of the Hermes engine', 8 | links: [ 9 | { 10 | title: 'See here to learn more about bundled Hermes', 11 | url: 'https://reactnative.dev/architecture/bundled-hermes', 12 | }, 13 | ], 14 | }, 15 | comments: [ 16 | { 17 | fileName: 'android/app/build.gradle', 18 | lineNumber: 280, 19 | lineChangeType: 'add', 20 | content: ( 21 | 22 | These lines instruct Gradle to consume the bundled version of Hermes. 23 | For further information on Bundled Hermes and how this mechanism 24 | works, look 25 | [here](https://reactnative.dev/architecture/bundled-hermes). 26 | 27 | ), 28 | }, 29 | ], 30 | } 31 | 32 | export default release 33 | -------------------------------------------------------------------------------- /src/releases/react-native/0.71.tsx: -------------------------------------------------------------------------------- 1 | import type { ReleaseT } from '../types' 2 | 3 | const release: ReleaseT = { 4 | usefulContent: { 5 | description: 6 | 'React Native 0.71 includes an updated process for the iOS privacy manifest, now required by Apple', 7 | links: [ 8 | { 9 | title: "Learn how to update your app's Apple privacy settings", 10 | url: 'https://github.com/react-native-community/discussions-and-proposals/discussions/776', 11 | }, 12 | ], 13 | }, 14 | comments: [], 15 | } 16 | 17 | export default release 18 | -------------------------------------------------------------------------------- /src/releases/react-native/0.72.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react' 2 | import type { ReleaseT } from '../types' 3 | 4 | const release: ReleaseT = { 5 | usefulContent: { 6 | description: 7 | 'React Native 0.72 includes a new metro config setup and an updated process for the iOS privacy manifest, now required by Apple', 8 | links: [ 9 | { 10 | title: 'Show about the major changes on React Native 0.72.0-rc.1', 11 | url: 'https://github.com/facebook/react-native/releases/tag/v0.72.0-rc.1', 12 | }, 13 | { 14 | title: "Learn how to update your app's Apple privacy settings", 15 | url: 'https://github.com/react-native-community/discussions-and-proposals/discussions/776', 16 | }, 17 | ], 18 | }, 19 | comments: [ 20 | { 21 | fileName: 'metro.config.js', 22 | lineNumber: 1, 23 | lineChangeType: 'add', 24 | content: ( 25 | 26 | In React Native 0.72, we've changed the config loading setup for Metro 27 | in React Native CLI. The base React Native Metro config is now 28 | explicitly required and extended here in your project's Metro config 29 | file, giving you full control over the final config. In addition, this 30 | means that standalone Metro CLI commands, such as [`metro 31 | get-dependencies`](https://facebook.github.io/metro/docs/cli/#get-dependencies-entryfile) 32 | will work. We've also cleaned up the leftover defaults. 33 | 34 | ), 35 | }, 36 | ], 37 | } 38 | 39 | export default release 40 | -------------------------------------------------------------------------------- /src/releases/react-native/0.73.tsx: -------------------------------------------------------------------------------- 1 | import type { ReleaseT } from '../types' 2 | 3 | const release: ReleaseT = { 4 | usefulContent: { 5 | description: 6 | 'React Native 0.73 includes an updated process for the iOS privacy manifest, now required by Apple', 7 | links: [ 8 | { 9 | title: "Learn how to update your app's Apple privacy settings", 10 | url: 'https://github.com/react-native-community/discussions-and-proposals/discussions/776', 11 | }, 12 | ], 13 | }, 14 | comments: [], 15 | } 16 | 17 | export default release 18 | -------------------------------------------------------------------------------- /src/releases/react-native/0.74.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react' 2 | import type { ReleaseT } from '../types' 3 | 4 | const release: ReleaseT = { 5 | usefulContent: { 6 | description: 7 | 'React Native 0.74 includes Yoga 3.0, Bridgeless by default under the New Architecture, batched onLayout updates, Yarn 3, removal of previously deprecated PropTypes, and some breaking changes, including updates to PushNotificationIOS. The Android Minimum SDK is now 23 (Android 6.0).', 8 | links: [ 9 | { 10 | title: 11 | 'Official blog post about the major changes on React Native 0.74', 12 | url: 'https://reactnative.dev/blog/2024/04/22/release-0.74', 13 | }, 14 | ], 15 | }, 16 | comments: [ 17 | { 18 | fileName: 'package.json', 19 | lineNumber: 36, 20 | lineChangeType: 'add', 21 | content: ( 22 | 23 | In React Native 0.74, for projects bootstrapped with React Native 24 | Community CLI, we've added first-class support for modern Yarn 25 | versions. For new projects Yarn 3.6.4 is the default package manager, 26 | and for existing projects, you can upgrade to Yarn 3.6.4 by running 27 | `yarn set version berry` in the project root. Read more 28 | [here](https://reactnative.dev/blog/2024/04/22/release-0.74#yarn-3-for-new-projects). 29 | 30 | ), 31 | }, 32 | ], 33 | } 34 | 35 | export default release 36 | -------------------------------------------------------------------------------- /src/releases/react-native/0.77.tsx: -------------------------------------------------------------------------------- 1 | import Markdown from '../../components/common/Markdown' 2 | import type { ReleaseT } from '../types' 3 | const release: ReleaseT = { 4 | usefulContent: { 5 | description: ( 6 | 7 | React Native 0.77 changes the AppDelegate template from Obj-C++ to 8 | Swift, but it's not only a syntax change. If you stick with the 9 | `AppDelegate.mm` file, be sure to add the new line with 10 | `RCTAppDependencyProvider`, as explained in the blog post below. 11 | 12 | ), 13 | links: [ 14 | { 15 | title: 'React Native 0.77 blog post', 16 | url: 'https://reactnative.dev/blog/2025/01/21/version-0.77#rctappdependencyprovider', 17 | }, 18 | ], 19 | }, 20 | } 21 | 22 | export default release 23 | -------------------------------------------------------------------------------- /src/releases/types.d.ts: -------------------------------------------------------------------------------- 1 | interface ReleaseLinkT { 2 | title: string 3 | url: string 4 | } 5 | 6 | interface ReleaseUsefulContentT { 7 | description: string | React.ReactNode 8 | links: ReleaseLinkT[] 9 | } 10 | 11 | type LineChangeT = 'add' | 'delete' | 'neutral' 12 | 13 | export interface ReleaseCommentT { 14 | fileName: string 15 | lineNumber: number 16 | lineChangeType: LineChangeT 17 | content: React.ReactNode 18 | } 19 | 20 | export interface ReleaseT { 21 | usefulContent: ReleaseUsefulContentT 22 | comments?: ReleaseCommentT[] 23 | } 24 | -------------------------------------------------------------------------------- /src/serviceWorker.ts: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.1/8 is considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ) 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href) 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js` 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config) 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ) 48 | }) 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config) 52 | } 53 | }) 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then((registration) => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing 63 | if (installingWorker == null) { 64 | return 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 75 | ) 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration) 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.') 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration) 90 | } 91 | } 92 | } 93 | } 94 | } 95 | }) 96 | .catch((error) => { 97 | console.error('Error during service worker registration:', error) 98 | }) 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl) 104 | .then((response) => { 105 | // Ensure service worker exists, and that we really are getting a JS file. 106 | const contentType = response.headers.get('content-type') 107 | if ( 108 | response.status === 404 || 109 | (contentType != null && contentType.indexOf('javascript') === -1) 110 | ) { 111 | // No service worker found. Probably a different app. Reload the page. 112 | navigator.serviceWorker.ready.then((registration) => { 113 | registration.unregister().then(() => { 114 | window.location.reload() 115 | }) 116 | }) 117 | } else { 118 | // Service worker found. Proceed as normal. 119 | registerValidSW(swUrl, config) 120 | } 121 | }) 122 | .catch(() => { 123 | console.log( 124 | 'No internet connection found. App is running in offline mode.' 125 | ) 126 | }) 127 | } 128 | 129 | export function unregister() { 130 | if ('serviceWorker' in navigator) { 131 | navigator.serviceWorker.ready.then((registration) => { 132 | registration.unregister() 133 | }) 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/theme/index.ts: -------------------------------------------------------------------------------- 1 | export interface Theme { 2 | background: string 3 | text: string 4 | textHover: string 5 | link: string 6 | linkHover: string 7 | border: string 8 | greenBorder: string 9 | yellowBorder: string 10 | yellowBackground: string 11 | 12 | diff: { 13 | textColor: string 14 | selectionBackground: string 15 | gutterInsertBackground: string 16 | gutterDeleteBackground: string 17 | gutterSelectedBackground: string 18 | codeInsertBackground: string 19 | codeDeleteBackground: string 20 | codeInsertEditBackground: string 21 | codeDeleteEditBackground: string 22 | codeSelectedBackground: string 23 | omitBackground: string 24 | decorationGutterBackground: string 25 | decorationGutter: string 26 | decorationContentBackground: string 27 | decorationContent: string 28 | } 29 | 30 | rowEven: string 31 | rowOdd: string 32 | 33 | popover: { 34 | text: string 35 | background: string 36 | border: string 37 | } 38 | } 39 | export const lightTheme: Theme = { 40 | background: '#FFFFFF', 41 | 42 | text: '#363537', 43 | textHover: 'rgba(27, 31, 35, 0.6)', 44 | link: '#045dc1', 45 | linkHover: '#40a9ff', 46 | border: '#e8e8e8', 47 | greenBorder: '#bef5cb', 48 | yellowBorder: '#ffe58f', 49 | yellowBackground: '#fffbe6', 50 | 51 | diff: { 52 | textColor: '#000', 53 | selectionBackground: '#b3d7ff', 54 | gutterInsertBackground: '#cdffd8', 55 | gutterDeleteBackground: '#fadde0', 56 | gutterSelectedBackground: '#fffce0', 57 | codeInsertBackground: '#eaffee', 58 | codeDeleteBackground: '#fdeff0', 59 | codeInsertEditBackground: '#acf2bd', 60 | codeDeleteEditBackground: '#f39ea2', 61 | codeSelectedBackground: '#fffce0', 62 | omitBackground: '#fafbfc', 63 | decorationGutterBackground: '#dbedff', 64 | decorationGutter: '#999', 65 | decorationContentBackground: '#dbedff', 66 | decorationContent: '#999', 67 | }, 68 | 69 | // Alternating Row Colors for Binary Download component and Content Loader animation 70 | rowEven: '#EEEEEE', 71 | rowOdd: '#FFFFFF', 72 | 73 | // The completed files counter 74 | popover: { 75 | background: '#d5eafd', 76 | text: '#7dadda', 77 | border: '#1890ff', 78 | }, 79 | } 80 | export const darkTheme: Theme = { 81 | background: '#0d1117', 82 | 83 | text: '#FAFAFA', 84 | textHover: '#999999', 85 | link: '#045dc1', 86 | linkHover: '#40a9ff', 87 | 88 | border: '#30363d', 89 | greenBorder: '#bef5cb', 90 | yellowBorder: '#c69026', 91 | yellowBackground: '#37332a8a', 92 | 93 | diff: { 94 | // Color object from https://github.com/otakustay/react-diff-view/blob/master/site/components/DiffView/diff.global.less 95 | textColor: '#fafafa', 96 | selectionBackground: '#5a5f80', 97 | gutterInsertBackground: '#3fb9504d', 98 | gutterDeleteBackground: '#f8514c4d', 99 | gutterSelectedBackground: '#5a5f80', 100 | codeInsertBackground: '#2ea04326', 101 | codeDeleteBackground: '#f851491a', 102 | codeInsertEditBackground: '#2ea04366', 103 | codeDeleteEditBackground: '#f8514c66', 104 | codeSelectedBackground: '#5a5f80', 105 | omitBackground: '#101120', 106 | decorationGutterBackground: '#222', 107 | decorationGutter: '#388bfd66', 108 | decorationContentBackground: '#388bfd1a', 109 | decorationContent: '#7d8590', 110 | }, 111 | 112 | // Alternating Row Colors for Binary Download component and Content Loader animation 113 | rowEven: '#363537', 114 | rowOdd: '#222223', 115 | 116 | // The completed files counter 117 | popover: { 118 | text: '#7dadda', 119 | background: '#0E5699', 120 | border: '#aabbca', 121 | }, 122 | } 123 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import semver from 'semver/preload' 2 | import { 3 | RN_DIFF_REPOSITORIES, 4 | DEFAULT_APP_NAME, 5 | DEFAULT_APP_PACKAGE, 6 | PACKAGE_NAMES, 7 | RN_CHANGELOG_URLS, 8 | } from './constants' 9 | import versions from './releases' 10 | 11 | const getRNDiffRepository = ({ packageName }: { packageName: string }) => 12 | RN_DIFF_REPOSITORIES[packageName] 13 | 14 | export const getReleasesFileURL = ({ packageName }: { packageName: string }) => 15 | `https://raw.githubusercontent.com/${getRNDiffRepository({ 16 | packageName, 17 | })}/master/${packageName === PACKAGE_NAMES.RNM ? 'RELEASES_MAC' : 'RELEASES'}` 18 | 19 | export const getDiffURL = ({ 20 | packageName, 21 | language, 22 | fromVersion, 23 | toVersion, 24 | }: { 25 | packageName: string 26 | language: string 27 | fromVersion: string 28 | toVersion: string 29 | }) => { 30 | const languageDir = 31 | packageName === PACKAGE_NAMES.RNM 32 | ? 'mac/' 33 | : packageName === PACKAGE_NAMES.RNW 34 | ? `${language}/` 35 | : '' 36 | 37 | return `https://raw.githubusercontent.com/${getRNDiffRepository({ 38 | packageName, 39 | })}/diffs/diffs/${languageDir}${fromVersion}..${toVersion}.diff` 40 | } 41 | 42 | const getBranch = ({ 43 | packageName, 44 | language, 45 | version, 46 | }: { 47 | packageName: string 48 | language?: string 49 | version: string 50 | }) => 51 | packageName === PACKAGE_NAMES.RNM 52 | ? `mac/${version}` 53 | : packageName === PACKAGE_NAMES.RNW 54 | ? `${language}/${version}` 55 | : version 56 | 57 | interface GetBinaryFileURLProps { 58 | packageName: string 59 | language?: string 60 | version: string 61 | path: string 62 | } 63 | // `path` must contain `RnDiffApp` prefix 64 | export const getBinaryFileURL = ({ 65 | packageName, 66 | language, 67 | version, 68 | path, 69 | }: GetBinaryFileURLProps) => { 70 | const branch = getBranch({ packageName, language, version }) 71 | 72 | return `https://raw.githubusercontent.com/${getRNDiffRepository({ 73 | packageName, 74 | })}/release/${branch}/${path}` 75 | } 76 | 77 | export const removeAppPathPrefix = (path: string, appName = DEFAULT_APP_NAME) => 78 | path.replace(new RegExp(`${appName}/`), '') 79 | 80 | /** 81 | * Replaces DEFAULT_APP_PACKAGE and DEFAULT_APP_NAME in str with custom 82 | * values if provided. 83 | * str could be a path, or content from a text file. 84 | */ 85 | export const replaceAppDetails = ( 86 | str: string, 87 | appName?: string, 88 | appPackage?: string 89 | ) => { 90 | const appNameOrFallback = appName || DEFAULT_APP_NAME 91 | const appPackageOrFallback = 92 | appPackage || `com.${appNameOrFallback.toLowerCase()}` 93 | 94 | return str 95 | .replaceAll(DEFAULT_APP_PACKAGE, appPackageOrFallback) 96 | .replaceAll( 97 | DEFAULT_APP_PACKAGE.replaceAll('.', '/'), 98 | appPackageOrFallback.replaceAll('.', '/') 99 | ) 100 | .replaceAll(DEFAULT_APP_NAME, appNameOrFallback) 101 | .replaceAll(DEFAULT_APP_NAME.toLowerCase(), appNameOrFallback.toLowerCase()) 102 | } 103 | 104 | export const getVersionsContentInDiff = ({ 105 | packageName, 106 | fromVersion, 107 | toVersion, 108 | }: { 109 | packageName: string 110 | fromVersion: string 111 | toVersion: string 112 | }) => { 113 | if (!versions[packageName]) { 114 | return [] 115 | } 116 | 117 | const cleanedToVersion = semver.valid(semver.coerce(toVersion)) 118 | 119 | return versions[packageName].filter(({ version }) => { 120 | const cleanedVersion = semver.coerce(version) 121 | 122 | // `cleanedVersion` can't be newer than `cleanedToVersion` nor older (or equal) than `fromVersion` 123 | return ( 124 | semver.compare(cleanedToVersion, cleanedVersion) !== -1 && 125 | ![0, -1].includes(semver.compare(cleanedVersion, fromVersion)) 126 | ) 127 | }) 128 | } 129 | 130 | export const getChangelogURL = ({ 131 | version, 132 | packageName, 133 | }: { 134 | version: string 135 | packageName: string 136 | }) => { 137 | if (packageName === PACKAGE_NAMES.RNW || packageName === PACKAGE_NAMES.RNM) { 138 | return `${RN_CHANGELOG_URLS[packageName]}v${version}` 139 | } 140 | 141 | return `${RN_CHANGELOG_URLS[packageName]}#v${version.replaceAll('.', '')}` 142 | } 143 | 144 | // If the browser is headless (running puppeteer) then it doesn't have any duration 145 | export const getTransitionDuration = (duration: number) => 146 | navigator.webdriver ? 0 : duration 147 | 148 | // settings constants 149 | export const SHOW_LATEST_RCS = 'Show latest release candidates' 150 | 151 | /** 152 | * Returns the file paths to display for each side of the diff. Takes into account 153 | * custom app name and package, and truncates the leading app name to provide 154 | * paths relative to the project directory. 155 | */ 156 | export const getFilePathsToShow = ({ 157 | oldPath, 158 | newPath, 159 | appName, 160 | appPackage, 161 | }: { 162 | oldPath: string 163 | newPath: string 164 | appName?: string 165 | appPackage?: string 166 | }) => { 167 | const oldPathSanitized = replaceAppDetails(oldPath, appName, appPackage) 168 | const newPathSanitized = replaceAppDetails(newPath, appName, appPackage) 169 | 170 | return { 171 | oldPath: removeAppPathPrefix(oldPathSanitized, appName), 172 | newPath: removeAppPathPrefix(newPathSanitized, appName), 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /src/utils/device-sizes.ts: -------------------------------------------------------------------------------- 1 | export const deviceSizes = { 2 | tablet: '(min-width: 768px)', 3 | } 4 | -------------------------------------------------------------------------------- /src/utils/test-utils.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import puppeteer from 'puppeteer' 3 | import { configureToMatchImageSnapshot } from 'jest-image-snapshot' 4 | 5 | let browser: puppeteer.Browser 6 | let page: puppeteer.Page 7 | 8 | const URLs = { 9 | RELEASES: 10 | 'https://raw.githubusercontent.com/react-native-community/rn-diff-purge/master/RELEASES', 11 | DIFF: 'https://raw.githubusercontent.com/react-native-community/rn-diff-purge/diffs/diffs/0.62.2..0.63.3.diff', 12 | REPOSITORY_INFO: 13 | 'https://api.github.com/repos/react-native-community/upgrade-helper', 14 | } 15 | 16 | const MOCK_RESPONSES = { 17 | [URLs.RELEASES]: () => 18 | '0.64.2\n0.64.1\n0.64.0\n0.64.0-rc.3\n0.64.0-rc.2\n0.64.0-rc.1\n0.64.0-rc.0\n0.63.4\n0.63.3\n0.63.2\n0.63.1\n0.63.0\n0.62.2\n0.62.1', 19 | [URLs.DIFF]: () => 20 | fs.readFileSync('./src/mocks/0.63.2..0.64.2.diff', 'utf-8'), 21 | [URLs.REPOSITORY_INFO]: () => 22 | fs.readFileSync('./src/mocks/repositoryInfo.json', 'utf-8'), 23 | } 24 | 25 | const mockResponses = (request: puppeteer.HTTPRequest) => { 26 | const requestedURL = request.url() 27 | const mockedURLs = Object.keys(MOCK_RESPONSES) 28 | 29 | if (mockedURLs.includes(requestedURL)) { 30 | request.respond({ 31 | headers: { 32 | 'access-control-allow-origin': '*', 33 | }, 34 | body: MOCK_RESPONSES[requestedURL](), 35 | }) 36 | } else { 37 | request.continue() 38 | } 39 | } 40 | 41 | export const launchBrowser = async () => { 42 | browser = await puppeteer.launch({ 43 | args: [ 44 | // Required for Docker version of Puppeteer 45 | '--no-sandbox', 46 | '--disable-setuid-sandbox', 47 | // This will write shared memory files into /tmp instead of /dev/shm, 48 | // because Docker’s default for /dev/shm is 64MB 49 | '--disable-dev-shm-usage', 50 | ], 51 | }) 52 | 53 | page = await browser.newPage() 54 | 55 | await page.setRequestInterception(true) 56 | page.on('request', mockResponses) 57 | 58 | await page.goto('http://localhost:3000/') 59 | await page.setViewport({ 60 | width: 1280, 61 | height: 720, 62 | }) 63 | 64 | return { 65 | browser, 66 | page, 67 | } 68 | } 69 | 70 | export const closeBrowser = async () => { 71 | await browser.close() 72 | } 73 | 74 | export const waitToRender = ({ waitingTime = 500 } = {}) => 75 | page.waitForTimeout(waitingTime) 76 | 77 | export const toMatchImageSnapshot = configureToMatchImageSnapshot({ 78 | comparisonMethod: 'ssim', 79 | failureThreshold: 0.0005, 80 | failureThresholdType: 'percent', 81 | allowSizeMismatch: true, 82 | }) 83 | -------------------------------------------------------------------------------- /src/utils/update-url.ts: -------------------------------------------------------------------------------- 1 | import { PACKAGE_NAMES } from '../constants' 2 | 3 | export function updateURL({ 4 | packageName, 5 | language, 6 | isPackageNameDefinedInURL, 7 | fromVersion, 8 | toVersion, 9 | appPackage, 10 | appName, 11 | }: { 12 | packageName: string 13 | language: string 14 | isPackageNameDefinedInURL: boolean 15 | fromVersion: string 16 | toVersion: string 17 | appPackage: string 18 | appName?: string 19 | }) { 20 | const url = new URL(window.location.origin) 21 | url.pathname = window.location.pathname 22 | url.hash = window.location.hash 23 | 24 | if (fromVersion) { 25 | url.searchParams.set('from', fromVersion) 26 | } 27 | if (toVersion) { 28 | url.searchParams.set('to', toVersion) 29 | } 30 | if (isPackageNameDefinedInURL) { 31 | url.searchParams.set('package', packageName) 32 | } 33 | if (packageName === PACKAGE_NAMES.RNW) { 34 | url.searchParams.set('language', language) 35 | } 36 | if (appPackage) { 37 | url.searchParams.set('package', appPackage) 38 | } 39 | if (appName) { 40 | url.searchParams.set('name', appName) 41 | } 42 | 43 | window.history.replaceState(null, '', url.toString()) 44 | } 45 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx", 18 | "jsxImportSource": "@emotion/react", 19 | "typeRoots": ["./node_modules/@types", "./@types"], 20 | "paths": { 21 | "react": ["./node_modules/@types/react"] 22 | } 23 | }, 24 | "ts-node": { 25 | "compilerOptions": { 26 | "module": "commonjs" 27 | } 28 | }, 29 | "include": ["./src", "./assets"], 30 | "exclude": ["./src/__tests__/*"] 31 | } 32 | --------------------------------------------------------------------------------