├── .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 |
13 |
14 |
15 |
16 |
17 |
18 | Open the tool!
19 |
20 |
21 |
22 | 
23 |
24 | ## ⚙️ How to use
25 |
26 | [](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 | You need to enable JavaScript to run this app.
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 | Binaries
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 | }
67 | onBlur={() => {
68 | setPopoverContent(popoverContentOpts.default)
69 | }}
70 | onClick={() => {
71 | copyContent().then(() =>
72 | setPopoverContent(popoverContentOpts.copied)
73 | )
74 | }}
75 | />
76 |
77 | )
78 | }
79 | )`
80 | font-size: 13px;
81 | margin-left: 5px;
82 | `
83 |
84 | export default CopyFileButton
85 |
--------------------------------------------------------------------------------
/src/components/common/DarkModeButton.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Button as AntdButton, Tooltip } from 'antd'
3 | import { ButtonProps as AntdButtonProps } from 'antd'
4 | import styled from '@emotion/styled'
5 |
6 | const Button = styled(AntdButton)`
7 | width: 32px;
8 | padding: 0;
9 | `
10 |
11 | interface DarkModeButtonProps extends AntdButtonProps {
12 | isDarkMode: boolean
13 | }
14 |
15 | const DarkModeButton = ({ isDarkMode, ...props }: DarkModeButtonProps) => {
16 | return (
17 |
18 | {isDarkMode ? '🌙' : '☀️'}
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 | } onClick={onClick} />
141 | ) : (
142 | } onClick={onClick} />
143 | )}
144 |
145 | )
146 | )`
147 | ${defaultIconButtonStyle}
148 | &,
149 | &:hover,
150 | &:focus {
151 | color: ${({ isDiffCompleted, theme }) =>
152 | isDiffCompleted ? '#52c41a' : theme.text};
153 | }
154 | `
155 |
156 | interface CopyPathToClipboardButtonProps extends Omit {
157 | oldPath: string
158 | newPath: string
159 | onCopy: () => void
160 | copyPathPopoverContent: string
161 | resetCopyPathPopoverContent: () => void
162 | type: string
163 | }
164 |
165 | const CopyPathToClipboardButton = styled(
166 | ({
167 | oldPath,
168 | newPath,
169 | type,
170 | onCopy,
171 | copyPathPopoverContent,
172 | resetCopyPathPopoverContent,
173 | ...props
174 | }: CopyPathToClipboardButtonProps) => (
175 |
176 |
184 | }
187 | onMouseOver={resetCopyPathPopoverContent}
188 | />
189 |
190 |
191 | )
192 | )`
193 | ${defaultIconButtonStyle}
194 | `
195 |
196 | const copyAnchorLinks = {
197 | default: 'Copy anchor link',
198 | copied: 'Anchor link copied!',
199 | }
200 |
201 | interface CopyAnchorLinksToClipboardButtonProps
202 | extends Omit {
203 | id: string
204 | fromVersion: string
205 | toVersion: string
206 | type: string
207 | }
208 |
209 | const CopyAnchorLinksToClipboardButton = styled(
210 | ({
211 | id,
212 | type,
213 | onCopy,
214 | fromVersion,
215 | toVersion,
216 | ...props
217 | }: CopyAnchorLinksToClipboardButtonProps) => {
218 | const [content, setContent] = React.useState(copyAnchorLinks.default)
219 | const resetContent = () => setContent(copyAnchorLinks.default)
220 | const onCopyContent = () => setContent(copyAnchorLinks.copied)
221 |
222 | const url = React.useMemo(() => {
223 | const url = new URL(window.location.toString())
224 | url.hash = id
225 | url.searchParams.set('from', fromVersion)
226 | url.searchParams.set('to', toVersion)
227 | return url.toString()
228 | }, [id])
229 |
230 | return (
231 |
232 |
240 | }
243 | onMouseOver={resetContent}
244 | />
245 |
246 |
247 | )
248 | }
249 | )`
250 | ${defaultIconButtonStyle}
251 | `
252 |
253 | const CollapseClickableArea = styled.div`
254 | display: inline-flex;
255 | align-items: center;
256 |
257 | &:hover {
258 | cursor: pointer;
259 | }
260 |
261 | & > *:last-child {
262 | margin-left: 8px;
263 | }
264 | `
265 | interface CollapseDiffButtonProps extends ButtonProps {
266 | open: boolean
267 | isDiffCollapsed: boolean
268 | theme?: Theme
269 | }
270 |
271 | const CollapseDiffButton = styled(
272 | ({ open, isDiffCollapsed, ...props }: CollapseDiffButtonProps) =>
273 | open ? (
274 | }
278 | />
279 | ) : null
280 | )`
281 | color: ${({ theme }) => theme.text};
282 | font-size: 10px;
283 | transform: ${({ isDiffCollapsed }) =>
284 | isDiffCollapsed ? 'rotate(-90deg)' : 'initial'};
285 | transition: transform 0.2s ease-in-out;
286 | transform-origin: center;
287 | line-height: 0;
288 | height: auto;
289 | &:hover,
290 | &:focus {
291 | color: ${({ theme }) => theme.text};
292 | }
293 | `
294 | interface DiffHeaderProps extends WrapperProps {
295 | oldPath: string
296 | newPath: string
297 | fromVersion: string
298 | toVersion: string
299 | type: DiffType
300 | diffKey: string
301 | hasDiff: boolean
302 | isDiffCollapsed: boolean
303 | setIsDiffCollapsed: (isDiffCollapsed: boolean, altKey?: boolean) => void
304 | isDiffCompleted: boolean
305 | onCompleteDiff: (diffKey: string) => void
306 | onCopyPathToClipboard: () => void
307 | copyPathPopoverContent: string
308 | resetCopyPathPopoverContent: () => void
309 | appName: string
310 | appPackage: string
311 | diffComments: ReleaseCommentT[]
312 | packageName: string
313 | }
314 |
315 | const DiffHeader = ({
316 | oldPath,
317 | newPath,
318 | fromVersion,
319 | toVersion,
320 | type,
321 | diffKey,
322 | hasDiff,
323 | isDiffCollapsed,
324 | setIsDiffCollapsed,
325 | isDiffCompleted,
326 | onCompleteDiff,
327 | onCopyPathToClipboard,
328 | copyPathPopoverContent,
329 | resetCopyPathPopoverContent,
330 | appName,
331 | appPackage,
332 | diffComments,
333 | packageName,
334 | ...props
335 | }: DiffHeaderProps) => {
336 | const sanitizedFilePaths = getFilePathsToShow({
337 | oldPath,
338 | newPath,
339 | appName,
340 | appPackage,
341 | })
342 |
343 | const id = React.useMemo(
344 | () => generatePathId(oldPath, newPath),
345 | [oldPath, newPath]
346 | )
347 |
348 | return (
349 |
350 |
351 | setIsDiffCollapsed(!isDiffCollapsed, altKey)}
354 | >
355 |
359 |
364 |
365 |
366 |
367 | {' '}
375 |
381 | setIsDiffCollapsed(false)}
385 | />
386 |
387 |
388 |
394 |
402 | {' '}
408 | onCompleteDiff(diffKey)}
411 | />
412 |
413 |
414 | )
415 | }
416 |
417 | export default DiffHeader
418 |
--------------------------------------------------------------------------------
/src/components/common/Diff/DiffLoading.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled from '@emotion/styled'
3 | import { HTMLMotionProps, motion } from 'framer-motion'
4 | import ContentLoader from 'react-content-loader'
5 | import { getTransitionDuration } from '../../../utils'
6 | import { useTheme } from '@emotion/react'
7 | import type { Theme } from '../../../theme'
8 |
9 | const TitleLoader = () => {
10 | const theme = useTheme() as Theme
11 |
12 | return (
13 |
19 |
20 |
21 | )
22 | }
23 |
24 | const DiffLoader = () => {
25 | const theme = useTheme() as Theme
26 |
27 | return (
28 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | )
43 | }
44 |
45 | interface ContainerProps extends HTMLMotionProps<'div'> {
46 | theme?: Theme
47 | }
48 |
49 | const Container = styled(motion.div)`
50 | margin-top: 16px;
51 | border: 1px solid ${({ theme }) => theme.border};
52 | border-radius: 3px;
53 | `
54 |
55 | interface HeaderProps extends React.HTMLAttributes {
56 | theme?: Theme
57 | }
58 | const Header = styled.div`
59 | background-color: ${({ theme }) => theme.background};
60 | padding: 10px;
61 | height: 40px;
62 | `
63 |
64 | const DiffLoading = () => (
65 |
70 |
73 |
78 |
79 |
80 |
81 | )
82 |
83 | export default DiffLoading
84 |
--------------------------------------------------------------------------------
/src/components/common/Diff/DiffSection.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useCallback } from 'react'
2 | import { Typography } from 'antd'
3 | import styled from '@emotion/styled'
4 | import semver from 'semver'
5 | import Diff from './Diff'
6 | import type { File } from 'gitdiff-parser'
7 | import type { ChangeEventArgs, ViewType } from 'react-diff-view'
8 |
9 | export const testIDs = {
10 | diffSection: 'diffSection',
11 | }
12 |
13 | const Title = styled(Typography.Title)`
14 | margin-top: 0.5em;
15 | `
16 |
17 | interface DiffSectionProps {
18 | packageName: string
19 | diff: any
20 | getDiffKey: (file: File) => string
21 | title?: string
22 | completedDiffs: string[]
23 | isDoneSection: boolean
24 | fromVersion: string
25 | toVersion: string
26 | handleCompleteDiff: (diffKey: string) => void
27 | selectedChanges: string[]
28 | onToggleChangeSelection: (args: ChangeEventArgs) => void
29 | diffViewStyle: ViewType
30 | appName: string
31 | appPackage: string
32 | doneTitleRef?: React.RefObject
33 | }
34 |
35 | const DiffSection = ({
36 | packageName,
37 | diff,
38 | getDiffKey,
39 | title,
40 | completedDiffs,
41 | isDoneSection,
42 | fromVersion,
43 | toVersion,
44 | handleCompleteDiff,
45 | selectedChanges,
46 | onToggleChangeSelection,
47 | diffViewStyle,
48 | appName,
49 | appPackage,
50 | doneTitleRef,
51 | }: DiffSectionProps) => {
52 | const [areAllCollapsed, setAllCollapsed] = useState(
53 | undefined
54 | )
55 |
56 | const getIsUpgradingFrom61To62 = useCallback(() => {
57 | const isUpgradingFrom61 = semver.satisfies(
58 | fromVersion,
59 | '>= 0.61.0 <= 0.62.0'
60 | )
61 |
62 | const isUpgradingTo62 = semver.satisfies(toVersion, '>= 0.62.0 <= 0.63.0')
63 |
64 | return isUpgradingFrom61 && isUpgradingTo62
65 | }, [fromVersion, toVersion])
66 |
67 | const isUpgradingFrom61To62 = getIsUpgradingFrom61To62()
68 |
69 | return (
70 |
71 | {title && completedDiffs.length > 0 && (
72 |
73 | {title}
74 |
75 | )}
76 |
77 | {diff.map((diffFile: File) => {
78 | const diffKey = getDiffKey(diffFile)
79 | const isDiffCompleted = completedDiffs.includes(diffKey)
80 |
81 | // If it's the "done" section, it shouldn't show if it's not completed
82 | if (isDoneSection !== isDiffCompleted) {
83 | return null
84 | }
85 |
86 | // This is here because there was a change in the line-endings of the
87 | // `gradlew.bat` from version 0.61 to 0.62 which showed the entire file
88 | // as a big change
89 | if (
90 | isUpgradingFrom61To62 &&
91 | diffFile.oldPath.match(/gradlew.bat/) &&
92 | diffFile.newPath.match(/gradlew.bat/)
93 | ) {
94 | return null
95 | }
96 |
97 | return (
98 |
118 | )
119 | })}
120 |
121 | )
122 | }
123 |
124 | export default DiffSection
125 |
--------------------------------------------------------------------------------
/src/components/common/Diff/DiffViewStyleOptions.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled from '@emotion/styled'
3 | import { Radio } from 'antd'
4 | import type { Theme } from '../../../theme'
5 | import type { ViewType } from 'react-diff-view'
6 |
7 | interface DiffViewStyleOptionsProps {
8 | handleViewStyleChange: (style: ViewType) => void
9 | diffViewStyle: ViewType
10 | theme?: Theme
11 | }
12 | const DiffViewStyleOptions = styled(
13 | ({ handleViewStyleChange, diffViewStyle }: DiffViewStyleOptionsProps) => (
14 |
15 | handleViewStyleChange('split')}
18 | >
19 | Split
20 |
21 | handleViewStyleChange('unified')}
24 | >
25 | Unified
26 |
27 |
28 | )
29 | )`
30 | float: right;
31 | position: absolute;
32 | top: 10px;
33 | right: 10px;
34 | font-size: 12px;
35 | border-width: 0px;
36 | width: 20px;
37 | height: 20px;
38 | margin-right: 8px;
39 | &,
40 | &:hover,
41 | &:focus {
42 | color: ${({ theme }) => theme.text};
43 | }
44 | `
45 | export default DiffViewStyleOptions
46 |
--------------------------------------------------------------------------------
/src/components/common/DiffViewer.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useRef, useReducer } from 'react'
2 | import styled from '@emotion/styled'
3 | import { Alert } from 'antd'
4 | import { motion, AnimatePresence, LayoutGroup } from 'framer-motion'
5 | import {
6 | type ViewType,
7 | withChangeSelect,
8 | ChangeEventArgs,
9 | } from 'react-diff-view'
10 | import 'react-diff-view/style/index.css'
11 | import { getTransitionDuration, getChangelogURL } from '../../utils'
12 | import DiffSection from './Diff/DiffSection'
13 | import DiffLoading from './Diff/DiffLoading'
14 | import UsefulContentSection from './UsefulContentSection'
15 | import BinaryDownload from './BinaryDownload'
16 | import ViewStyleOptions from './Diff/DiffViewStyleOptions'
17 | import CompletedFilesCounter from './CompletedFilesCounter'
18 | import { useFetchDiff } from '../../hooks/fetch-diff'
19 | import type { Theme } from '../../theme'
20 | import type { File } from 'gitdiff-parser'
21 |
22 | const Container = styled.div`
23 | width: 90%;
24 | `
25 |
26 | const TopContainer = styled.div`
27 | display: flex;
28 | position: relative;
29 | border-width: 1px;
30 | margin-top: 16px;
31 | flex-direction: row;
32 | justify-content: flex-end;
33 | `
34 |
35 | const Link = styled.a<{ theme?: Theme }>`
36 | padding: 4px 15px;
37 | color: ${({ theme }) => theme.link};
38 | `
39 |
40 | const getDiffKey = ({
41 | oldRevision,
42 | newRevision,
43 | }: {
44 | oldRevision: string
45 | newRevision: string
46 | }) => `${oldRevision}${newRevision}`
47 |
48 | const scrollToRef = (ref?: React.RefObject) =>
49 | ref?.current?.scrollIntoView({ behavior: 'smooth' })
50 |
51 | // Lazy loaded content won't respect anchor links in the URL, so we have to help
52 | // the viewport once we know our diff has loaded.
53 | const jumpToAnchor = (stopScrolling: boolean) => {
54 | if (!window.location.hash || stopScrolling) {
55 | return true
56 | }
57 | const ref = document.getElementById(window.location.hash.slice(1))
58 | if (!ref) {
59 | return true
60 | }
61 | ref.scrollIntoView()
62 | return true
63 | }
64 |
65 | interface DiffViewerProps {
66 | packageName: string
67 | language: string
68 | fromVersion: string
69 | toVersion: string
70 | shouldShowDiff: boolean
71 | selectedChanges: string[]
72 | onToggleChangeSelection: (args: ChangeEventArgs) => void
73 | appName: string
74 | appPackage: string
75 | }
76 | const DiffViewer = ({
77 | packageName,
78 | language,
79 | fromVersion,
80 | toVersion,
81 | shouldShowDiff,
82 | selectedChanges,
83 | onToggleChangeSelection,
84 | appName,
85 | appPackage,
86 | }: DiffViewerProps) => {
87 | const { isLoading, isDone, diff } = useFetchDiff({
88 | shouldShowDiff,
89 | packageName,
90 | language,
91 | fromVersion,
92 | toVersion,
93 | })
94 | const [completedDiffs, setCompletedDiffs] = useState([])
95 | const [isGoToDoneClicked, setIsGoToDoneClicked] = useState(false)
96 | const donePopoverPossibleOpts = {
97 | done: {
98 | content: 'Scroll to Done section',
99 | cursorType: 's-resize',
100 | },
101 | top: {
102 | content: 'Scroll to Top',
103 | cursorType: 'n-resize',
104 | },
105 | }
106 | const [donePopoverOpts, setDonePopoverOpts] = useState(
107 | donePopoverPossibleOpts.done
108 | )
109 | const doneTitleRef = useRef(null)
110 |
111 | const scrollToDone = () => scrollToRef(doneTitleRef)
112 | const scrollToTop = () => window.scrollTo({ top: 0, behavior: 'smooth' })
113 |
114 | const handleCompletedFilesCounterClick = () => {
115 | setIsGoToDoneClicked(!isGoToDoneClicked)
116 | if (isGoToDoneClicked) {
117 | setDonePopoverOpts(donePopoverPossibleOpts.done)
118 | scrollToTop()
119 | } else {
120 | setDonePopoverOpts(donePopoverPossibleOpts.top)
121 | scrollToDone()
122 | }
123 | }
124 |
125 | const handleCompleteDiff = (diffKey: string) => {
126 | if (completedDiffs.includes(diffKey)) {
127 | return setCompletedDiffs((prevCompletedDiffs) =>
128 | prevCompletedDiffs.filter((completedDiff) => completedDiff !== diffKey)
129 | )
130 | }
131 |
132 | setCompletedDiffs((prevCompletedDiffs) => [...prevCompletedDiffs, diffKey])
133 | }
134 |
135 | const renderUpgradeDoneMessage = ({
136 | diff,
137 | completedDiffs,
138 | }: {
139 | diff: File[]
140 | completedDiffs: string[]
141 | }) =>
142 | diff.length === completedDiffs.length && (
143 |
150 | )
151 |
152 | const resetCompletedDiffs = () => setCompletedDiffs([])
153 |
154 | const [diffViewStyle, setViewStyle] = useState(
155 | (localStorage.getItem('viewStyle') || 'split') as ViewType
156 | )
157 |
158 | const handleViewStyleChange = (newViewStyle: ViewType) => {
159 | setViewStyle(newViewStyle)
160 | localStorage.setItem('viewStyle', newViewStyle)
161 | }
162 |
163 | const [, jumpToAnchorOnce] = useReducer(jumpToAnchor, false)
164 |
165 | let changelog
166 | if (!toVersion.includes('-rc')) {
167 | const href = getChangelogURL({ packageName, version: toVersion })
168 | changelog = (
169 |
170 | changelog
171 |
172 | )
173 | }
174 |
175 | useEffect(() => {
176 | if (!isDone) {
177 | resetCompletedDiffs()
178 | }
179 | }, [isDone])
180 |
181 | if (!shouldShowDiff) {
182 | return null
183 | }
184 |
185 | if (isLoading) {
186 | return (
187 |
188 |
189 |
190 |
191 |
192 | )
193 | }
194 |
195 | const diffSectionProps = {
196 | diff: diff,
197 | getDiffKey: getDiffKey,
198 | completedDiffs: completedDiffs,
199 | fromVersion: fromVersion,
200 | toVersion: toVersion,
201 | handleCompleteDiff: handleCompleteDiff,
202 | selectedChanges: selectedChanges,
203 | onToggleChangeSelection: onToggleChangeSelection,
204 | }
205 |
206 | return (
207 |
208 |
209 |
215 |
221 |
222 |
223 | {changelog}
224 |
225 |
232 |
233 |
237 |
238 |
239 |
247 |
248 | {renderUpgradeDoneMessage({ diff, completedDiffs })}
249 |
250 |
260 |
261 |
262 |
263 |
270 |
271 | )
272 | }
273 |
274 | // @ts-ignore-next-line
275 | export default withChangeSelect({ multiple: true })(DiffViewer)
276 |
--------------------------------------------------------------------------------
/src/components/common/DownloadFileButton.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Button, ButtonProps } from 'antd'
3 | import { DownloadOutlined } from '@ant-design/icons'
4 | import { getBinaryFileURL } from '../../utils'
5 |
6 | interface DownloadFileButtonProps extends ButtonProps {
7 | open: boolean
8 | version: string
9 | path: string
10 | packageName: string
11 | }
12 | const DownloadFileButton = ({
13 | open,
14 | version,
15 | path,
16 | packageName,
17 | ...props
18 | }: DownloadFileButtonProps) => {
19 | return open ? (
20 | }
24 | target="_blank"
25 | href={getBinaryFileURL({ packageName, version, path })}
26 | />
27 | ) : null
28 | }
29 |
30 | export default DownloadFileButton
31 |
--------------------------------------------------------------------------------
/src/components/common/Markdown.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Markdown, { MarkdownToJSX } from 'markdown-to-jsx'
3 | import styled from '@emotion/styled'
4 | import type { Theme } from '../../theme'
5 |
6 | interface LinkProps extends React.AnchorHTMLAttributes {
7 | theme?: Theme
8 | }
9 |
10 | export const Link = styled((props: LinkProps) => (
11 | // eslint-disable-next-line jsx-a11y/anchor-has-content
12 | e.stopPropagation()}
17 | />
18 | ))`
19 | text-decoration: none;
20 | color: ${({ theme }) => theme.link};
21 | `
22 |
23 | const InlineCode = styled.em`
24 | font-style: normal;
25 | background-color: rgba(27, 31, 35, 0.07);
26 | border-radius: 3px;
27 | font-size: 85%;
28 | margin: 0;
29 | padding: 0.2em 0.4em;
30 | `
31 | interface MarkdownComponentProps {
32 | [key: string]: any
33 | children: string
34 | options?: MarkdownToJSX.Options
35 | }
36 |
37 | const MarkdownComponent = ({
38 | forceBlock = false,
39 | options = {},
40 | ...props
41 | }: MarkdownComponentProps) => (
42 |
58 | )
59 |
60 | export default MarkdownComponent
61 |
--------------------------------------------------------------------------------
/src/components/common/Select.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { render } from '@testing-library/react'
3 | import Select from './Select'
4 |
5 | it('renders without crashing', () => {
6 | const { container } = render(
7 |
12 | )
13 |
14 | expect(container).toMatchInlineSnapshot(`
15 |
16 |
19 |
20 | The title
21 |
22 |
25 |
28 |
31 |
45 |
46 |
49 | one option
50 |
51 |
52 |
58 |
63 |
73 |
76 |
77 |
78 |
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 |
27 | {option}
28 |
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 |
54 | ⚠️
55 |
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 |
38 | Show me how to upgrade!
39 |
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 |
128 | ) : (
129 |
130 | )
131 | }
132 | onClick={toggleContentVisibility}
133 | />
134 | )
135 | )`
136 | float: right;
137 | position: absolute;
138 | top: 11px;
139 | right: 12px;
140 | font-size: 12px;
141 | border-width: 0px;
142 | width: 20px;
143 | height: 20px;
144 | color: ${({ theme }) => theme.text + '73'}; // 45% opacity
145 | `
146 |
147 | const Separator = styled.hr<{ theme?: Theme }>`
148 | margin: 15px 0;
149 | background-color: ${({ theme }) => theme.border};
150 | height: 0.25em;
151 | border: 0;
152 | `
153 |
154 | interface UsefulContentSectionProps {
155 | packageName: string
156 | fromVersion: string
157 | toVersion: string
158 | isLoading: boolean
159 | }
160 |
161 | interface UsefulContentSectionState {
162 | isContentOpen: boolean
163 | }
164 |
165 | class UsefulContentSection extends Component<
166 | UsefulContentSectionProps,
167 | UsefulContentSectionState
168 | > {
169 | state = {
170 | isContentOpen: true,
171 | }
172 |
173 | shouldComponentUpdate(
174 | nextProps: Partial,
175 | nextState: Partial
176 | ) {
177 | // Only re-render component if it has reloaded the diff on the parent
178 | const hasLoaded = this.props.isLoading && !nextProps.isLoading
179 | // or if the content has been hidden
180 | const hasContentBeenHidden =
181 | this.state.isContentOpen !== nextState.isContentOpen
182 |
183 | return hasLoaded || hasContentBeenHidden
184 | }
185 |
186 | handleToggleContentVisibility = () =>
187 | this.setState(({ isContentOpen }) => ({
188 | isContentOpen: !isContentOpen,
189 | }))
190 |
191 | getChangelog = ({ version }: { version: string }) => {
192 | const { packageName, toVersion } = this.props
193 |
194 | if (
195 | packageName === PACKAGE_NAMES.RNW ||
196 | packageName === PACKAGE_NAMES.RNM
197 | ) {
198 | return {
199 | title: `React Native ${
200 | packageName === PACKAGE_NAMES.RNW ? 'Windows' : 'macOS'
201 | } ${toVersion} changelog`,
202 | url: getChangelogURL({
203 | packageName,
204 | version: toVersion,
205 | }),
206 | version: toVersion,
207 | }
208 | }
209 |
210 | const versionWithoutEndingZero = version.slice(0, 4)
211 |
212 | return {
213 | title: `React Native ${versionWithoutEndingZero} changelog`,
214 | url: getChangelogURL({
215 | packageName,
216 | version: versionWithoutEndingZero,
217 | }),
218 | version: versionWithoutEndingZero,
219 | }
220 | }
221 |
222 | render() {
223 | const { packageName, fromVersion, toVersion } = this.props
224 | const { isContentOpen } = this.state
225 |
226 | const versions = getVersionsContentInDiff({
227 | packageName,
228 | fromVersion,
229 | toVersion,
230 | })
231 |
232 | const doesAnyVersionHaveUsefulLinks = versions.some(
233 | ({ usefulContent }) => !!usefulContent
234 | )
235 |
236 | return (
237 |
238 |
239 |
243 | Useful content for upgrading
244 |
245 |
246 |
250 |
251 |
252 | {doesAnyVersionHaveUsefulLinks ? (
253 |
258 | ) : null}
259 |
260 |
261 |
262 |
263 |
264 |
265 |
266 |
267 |
268 | )
269 | }
270 | }
271 |
272 | export default UsefulContentSection
273 |
--------------------------------------------------------------------------------
/src/components/common/UsefulLinks.tsx:
--------------------------------------------------------------------------------
1 | import React, { Fragment } from 'react'
2 | import styled from '@emotion/styled'
3 | import { getChangelogURL } from '../../utils'
4 | import { Link } from './Markdown'
5 | import { PACKAGE_NAMES } from '../../constants'
6 | import type { Theme } from '../../theme'
7 |
8 | const Separator = styled.hr<{ theme?: Theme }>`
9 | margin: 15px 0;
10 | background-color: ${({ theme }) => theme.border};
11 | height: 0.25em;
12 | border: 0;
13 | `
14 |
15 | const List = styled.ol`
16 | padding-inline-start: 18px;
17 | margin: 10px 0 0;
18 | `
19 |
20 | interface UsefulLinksProps {
21 | versions: any[]
22 | packageName: string
23 | toVersion: string
24 | }
25 |
26 | const UsefulLinks = ({
27 | packageName,
28 | toVersion,
29 | versions,
30 | }: UsefulLinksProps) => {
31 | const getChangelog = ({ version }: { version: string }) => {
32 | if (
33 | packageName === PACKAGE_NAMES.RNW ||
34 | packageName === PACKAGE_NAMES.RNM
35 | ) {
36 | return {
37 | title: `React Native ${
38 | packageName === PACKAGE_NAMES.RNW ? 'Windows' : 'macOS'
39 | } ${toVersion} changelog`,
40 | url: getChangelogURL({
41 | packageName,
42 | version: toVersion,
43 | }),
44 | version: toVersion,
45 | }
46 | }
47 |
48 | const versionWithoutEndingZero = version.slice(0, 4)
49 |
50 | return {
51 | title: `React Native ${versionWithoutEndingZero} changelog`,
52 | url: getChangelogURL({
53 | packageName,
54 | version: versionWithoutEndingZero,
55 | }),
56 | version: versionWithoutEndingZero,
57 | }
58 | }
59 |
60 | const doesAnyVersionHaveUsefulContent = React.useMemo(
61 | () => versions.some(({ usefulContent }) => !!usefulContent),
62 | [versions]
63 | )
64 |
65 | const hasMoreThanOneRelease = versions.length > 1
66 |
67 | if (!doesAnyVersionHaveUsefulContent) {
68 | return null
69 | }
70 | return (
71 | <>
72 | {versions.map(({ usefulContent, version }, key) => {
73 | if (!usefulContent) {
74 | return null
75 | }
76 |
77 | const changelog = getChangelog({ version })
78 |
79 | const links = [...usefulContent.links, changelog]
80 |
81 | return (
82 |
83 | {key > 0 && }
84 |
85 | {hasMoreThanOneRelease && Release {changelog.version} }
86 |
87 | {usefulContent.description}
88 |
89 |
90 | {links.map(({ url, title }, key) => (
91 |
92 | {title}
93 |
94 | ))}
95 |
96 |
97 | )
98 | })}
99 |
100 | >
101 | )
102 | }
103 |
104 | export default UsefulLinks
105 |
--------------------------------------------------------------------------------
/src/components/common/VersionSelector.tsx:
--------------------------------------------------------------------------------
1 | import React, { Fragment, useState, useEffect, useRef } from 'react'
2 | import styled from '@emotion/styled'
3 | import { Popover } from 'antd'
4 | import semver from 'semver/preload'
5 | import queryString from 'query-string'
6 | import { Select } from '.'
7 | import UpgradeButton from './UpgradeButton'
8 | import { useFetchReleaseVersions } from '../../hooks/fetch-release-versions'
9 | import { updateURL } from '../../utils/update-url'
10 | import { deviceSizes } from '../../utils/device-sizes'
11 | import type { SelectProps } from './Select'
12 |
13 | export const testIDs = {
14 | fromVersionSelector: 'fromVersionSelector',
15 | toVersionSelector: 'toVersionSelector',
16 | }
17 |
18 | const Selectors = styled.div`
19 | display: flex;
20 | flex-direction: column;
21 | gap: 16px;
22 |
23 | @media ${deviceSizes.tablet} {
24 | flex-direction: row;
25 | }
26 | `
27 |
28 | const FromVersionSelector = styled(Select)``
29 |
30 | interface ToVersionSelectorProps extends SelectProps {
31 | popover?: React.ReactNode
32 | }
33 |
34 | const ToVersionSelector = styled(
35 | ({ popover, ...props }: ToVersionSelectorProps) =>
36 | popover ? (
37 | // @ts-ignore-next-line
38 | React.cloneElement(popover, {
39 | children: ,
40 | })
41 | ) : (
42 |
43 | )
44 | )``
45 |
46 | const getVersionsInURL = (): {
47 | fromVersion: string
48 | toVersion: string
49 | } => {
50 | // Parses `/?from=VERSION&to=VERSION` from URL
51 | const { from: fromVersion, to: toVersion } = queryString.parse(
52 | window.location.search
53 | )
54 |
55 | return {
56 | fromVersion: fromVersion as string,
57 | toVersion: toVersion as string,
58 | }
59 | }
60 |
61 | // Users making changes to version should not retain anchor links
62 | // to files that may or may not change.
63 | const stripAnchorInUrl = () => {
64 | if (window.location.hash) {
65 | const url = new URL(window.location.toString())
66 | url.hash = ''
67 | window.history.pushState({}, '', url)
68 | }
69 | return true
70 | }
71 |
72 | const compareReleaseCandidateVersions = ({
73 | version,
74 | versionToCompare,
75 | }: {
76 | version: string | semver.SemVer
77 | versionToCompare: string | semver.SemVer
78 | }) =>
79 | ['prerelease', 'prepatch', null].includes(
80 | semver.diff(version, versionToCompare)
81 | )
82 |
83 | const getLatestMajorReleaseVersion = (releasedVersions: string[]) =>
84 | semver.valid(
85 | semver.coerce(
86 | releasedVersions.find(
87 | (releasedVersion) =>
88 | !semver.prerelease(releasedVersion) &&
89 | semver.patch(releasedVersion) === 0
90 | )
91 | )
92 | )
93 |
94 | // Check if `from` rc version is one of the latest major release (ie. 0.60.0)
95 | const checkIfVersionIsALatestRC = ({
96 | version,
97 | latestVersion,
98 | }: {
99 | version: string
100 | latestVersion: string
101 | }) =>
102 | semver.prerelease(version) &&
103 | compareReleaseCandidateVersions({
104 | version: latestVersion,
105 | versionToCompare: version,
106 | })
107 |
108 | // Filters out release candidates from `releasedVersion` with the
109 | // exception of the release candidates from the latest version
110 | const getReleasedVersionsWithCandidates = ({
111 | releasedVersions,
112 | toVersion,
113 | latestVersion,
114 | showReleaseCandidates,
115 | }: {
116 | releasedVersions: string[]
117 | toVersion: string
118 | latestVersion: string
119 | showReleaseCandidates: boolean
120 | }) => {
121 | const isToVersionAReleaseCandidate = semver.prerelease(toVersion) !== null
122 | const isLatestAReleaseCandidate = semver.prerelease(latestVersion) !== null
123 |
124 | return releasedVersions.filter((releasedVersion) => {
125 | // Show the release candidates of the latest version
126 | const isLatestReleaseCandidate =
127 | showReleaseCandidates &&
128 | checkIfVersionIsALatestRC({
129 | version: releasedVersion,
130 | latestVersion,
131 | })
132 |
133 | return (
134 | isLatestReleaseCandidate ||
135 | semver.prerelease(releasedVersion) === null ||
136 | (isToVersionAReleaseCandidate &&
137 | compareReleaseCandidateVersions({
138 | version: toVersion,
139 | versionToCompare: releasedVersion,
140 | })) ||
141 | (isLatestAReleaseCandidate &&
142 | compareReleaseCandidateVersions({
143 | version: latestVersion,
144 | versionToCompare: releasedVersion,
145 | }))
146 | )
147 | })
148 | }
149 |
150 | const getReleasedVersions = ({
151 | releasedVersions,
152 | minVersion,
153 | maxVersion,
154 | }: {
155 | releasedVersions: string[]
156 | minVersion?: string
157 | maxVersion?: string
158 | }) => {
159 | const latestMajorReleaseVersion =
160 | getLatestMajorReleaseVersion(releasedVersions)
161 |
162 | const isVersionAReleaseAndOfLatest = (version: string) =>
163 | version.includes('rc') &&
164 | semver.valid(semver.coerce(version)) === latestMajorReleaseVersion
165 |
166 | return releasedVersions.filter(
167 | (releasedVersion) =>
168 | releasedVersion.length > 0 &&
169 | ((maxVersion && semver.lt(releasedVersion, maxVersion)) ||
170 | (minVersion &&
171 | semver.gt(releasedVersion, minVersion) &&
172 | !isVersionAReleaseAndOfLatest(releasedVersion)))
173 | )
174 | }
175 |
176 | // Finds the first minor release (which in react-native is the major) when compared to another version
177 | const getFirstMajorRelease = ({
178 | releasedVersions,
179 | versionToCompare,
180 | }: {
181 | releasedVersions: string[]
182 | versionToCompare: string
183 | }) =>
184 | releasedVersions.find(
185 | (releasedVersion) =>
186 | semver.lt(releasedVersion, versionToCompare) &&
187 | semver.diff(
188 | semver.valid(semver.coerce(releasedVersion)),
189 | semver.valid(semver.coerce(versionToCompare))
190 | ) === 'minor'
191 | )
192 |
193 | // Return if version exists in the ones returned from GitHub
194 | const doesVersionExist = ({
195 | version,
196 | allVersions,
197 | minVersion,
198 | }: {
199 | version: string
200 | allVersions: string[]
201 | minVersion?: string
202 | }) => {
203 | try {
204 | return (
205 | version &&
206 | allVersions.includes(version) &&
207 | // Also compare the version against a `minVersion`, this is used
208 | // to not allow the user to have a `fromVersion` newer than `toVersion`
209 | (!minVersion || (minVersion && semver.gt(version, minVersion)))
210 | )
211 | } catch (_error) {
212 | return false
213 | }
214 | }
215 |
216 | const VersionSelector = ({
217 | packageName,
218 | language,
219 | isPackageNameDefinedInURL,
220 | showDiff,
221 | showReleaseCandidates,
222 | appPackage,
223 | appName,
224 | }: {
225 | packageName: string
226 | language: string
227 | isPackageNameDefinedInURL: boolean
228 | showDiff: (args: { fromVersion: string; toVersion: string }) => void
229 | showReleaseCandidates: boolean
230 | appPackage: string
231 | appName?: string
232 | }) => {
233 | const { isLoading, isDone, releaseVersions } = useFetchReleaseVersions({
234 | packageName,
235 | })
236 | const [allVersions, setAllVersions] = useState([])
237 | const [fromVersionList, setFromVersionList] = useState([])
238 | const [toVersionList, setToVersionList] = useState([])
239 | const [hasVersionsFromURL, setHasVersionsFromURL] = useState(false)
240 |
241 | const [localFromVersion, setLocalFromVersion] = useState('')
242 | const [localToVersion, setLocalToVersion] = useState('')
243 |
244 | const upgradeButtonEl = useRef(null)
245 |
246 | useEffect(() => {
247 | const versionsInURL = getVersionsInURL()
248 |
249 | const fetchVersions = async () => {
250 | // Check if the versions provided in the URL are valid
251 | const hasFromVersionInURL = doesVersionExist({
252 | version: versionsInURL.fromVersion,
253 | allVersions: releaseVersions,
254 | })
255 |
256 | const hasToVersionInURL = doesVersionExist({
257 | version: versionsInURL.toVersion,
258 | allVersions: releaseVersions,
259 | minVersion: versionsInURL.fromVersion,
260 | })
261 |
262 | const latestVersion = releaseVersions[0]
263 | // If the version from URL is not valid then fallback to the latest
264 | const toVersionToBeSet = hasToVersionInURL
265 | ? versionsInURL.toVersion
266 | : latestVersion
267 |
268 | // Remove `rc` versions from the array of versions
269 | const sanitizedVersions = getReleasedVersionsWithCandidates({
270 | releasedVersions: releaseVersions,
271 | toVersion: toVersionToBeSet,
272 | latestVersion,
273 | showReleaseCandidates,
274 | })
275 |
276 | setAllVersions(sanitizedVersions)
277 |
278 | const fromVersionToBeSet = hasFromVersionInURL
279 | ? versionsInURL.fromVersion
280 | : // Get first major release before latest
281 | getFirstMajorRelease({
282 | releasedVersions: sanitizedVersions,
283 | versionToCompare: toVersionToBeSet,
284 | })
285 |
286 | setFromVersionList(
287 | getReleasedVersions({
288 | releasedVersions: sanitizedVersions,
289 | maxVersion: toVersionToBeSet,
290 | })
291 | )
292 | setToVersionList(
293 | getReleasedVersions({
294 | releasedVersions: sanitizedVersions,
295 | minVersion: fromVersionToBeSet,
296 | })
297 | )
298 |
299 | setLocalFromVersion(fromVersionToBeSet)
300 | setLocalToVersion(toVersionToBeSet)
301 |
302 | const doesHaveVersionsInURL = hasFromVersionInURL && hasToVersionInURL
303 |
304 | setHasVersionsFromURL(!!doesHaveVersionsInURL)
305 | }
306 |
307 | if (isDone) {
308 | fetchVersions()
309 | }
310 | }, [
311 | isDone,
312 | releaseVersions,
313 | setLocalFromVersion,
314 | setLocalToVersion,
315 | showReleaseCandidates,
316 | ])
317 |
318 | useEffect(() => {
319 | if (isLoading) {
320 | return
321 | }
322 |
323 | setFromVersionList(
324 | getReleasedVersions({
325 | releasedVersions: allVersions,
326 | maxVersion: localToVersion,
327 | })
328 | )
329 | setToVersionList(
330 | getReleasedVersions({
331 | releasedVersions: allVersions,
332 | minVersion: localFromVersion,
333 | })
334 | )
335 |
336 | if (hasVersionsFromURL) {
337 | upgradeButtonEl?.current?.click()
338 | }
339 | }, [
340 | isLoading,
341 | allVersions,
342 | localFromVersion,
343 | localToVersion,
344 | hasVersionsFromURL,
345 | showReleaseCandidates,
346 | ])
347 |
348 | const onShowDiff = () => {
349 | showDiff({
350 | fromVersion: localFromVersion,
351 | toVersion: localToVersion,
352 | })
353 |
354 | updateURL({
355 | packageName,
356 | language,
357 | isPackageNameDefinedInURL,
358 | fromVersion: localFromVersion,
359 | toVersion: localToVersion,
360 | appPackage,
361 | appName,
362 | })
363 | }
364 |
365 | return (
366 |
367 |
368 |
376 | stripAnchorInUrl() && setLocalFromVersion(chosenVersion)
377 | }
378 | />
379 |
380 |
394 | )
395 | }
396 | onChange={(chosenVersion) =>
397 | stripAnchorInUrl() && setLocalToVersion(chosenVersion)
398 | }
399 | />
400 |
401 |
402 |
403 |
404 | )
405 | }
406 |
407 | export default VersionSelector
408 |
--------------------------------------------------------------------------------
/src/components/common/ViewFileButton.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled from '@emotion/styled'
3 | import { Button, ButtonProps } from 'antd'
4 | import { getBinaryFileURL } from '../../utils'
5 |
6 | interface ViewFileButtonProps extends ButtonProps {
7 | open: boolean
8 | version: string
9 | path: string
10 | packageName: string
11 | }
12 | const ViewFileButton = styled(
13 | ({ open, version, path, packageName, ...props }: ViewFileButtonProps) => {
14 | if (!open) {
15 | return null
16 | }
17 |
18 | return (
19 |
25 | Raw
26 |
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 |
--------------------------------------------------------------------------------