From 7a206d463d8e77e3a593b3fa1a29908837ac8756 Mon Sep 17 00:00:00 2001 From: Tommy Nguyen <4123478+tido64@users.noreply.github.com> Date: Mon, 10 Oct 2022 10:39:19 +0200 Subject: [PATCH] feat(align-deps): copy from dep-check --- .changeset/lemon-boats-learn.md | 2 + packages/align-deps/CHANGELOG.md | 918 ++++++++++++++++++ packages/align-deps/README.md | 394 ++++++++ packages/align-deps/package.json | 63 ++ .../align-deps/scripts/update-profile.mjs | 399 ++++++++ packages/align-deps/scripts/update-readme.js | 78 ++ packages/align-deps/src/capabilities.ts | 147 +++ packages/align-deps/src/check.ts | 158 +++ packages/align-deps/src/cli.ts | 242 +++++ packages/align-deps/src/dependencies.ts | 167 ++++ packages/align-deps/src/findBadPackages.ts | 33 + packages/align-deps/src/helpers.ts | 42 + packages/align-deps/src/index.ts | 20 + packages/align-deps/src/initialize.ts | 31 + packages/align-deps/src/manifest.ts | 131 +++ packages/align-deps/src/presets/banned.ts | 42 + .../src/presets/microsoft/baseCapabilities.ts | 11 + .../align-deps/src/presets/microsoft/index.ts | 29 + .../src/presets/microsoft/profile-0.61.ts | 190 ++++ .../src/presets/microsoft/profile-0.62.ts | 92 ++ .../src/presets/microsoft/profile-0.63.ts | 118 +++ .../src/presets/microsoft/profile-0.64.ts | 185 ++++ .../src/presets/microsoft/profile-0.65.ts | 132 +++ .../src/presets/microsoft/profile-0.66.ts | 122 +++ .../src/presets/microsoft/profile-0.67.ts | 57 ++ .../src/presets/microsoft/profile-0.68.ts | 120 +++ .../src/presets/microsoft/profile-0.69.ts | 123 +++ .../src/presets/microsoft/profile-0.70.ts | 104 ++ packages/align-deps/src/profiles.ts | 194 ++++ packages/align-deps/src/setVersion.ts | 90 ++ packages/align-deps/src/types.ts | 93 ++ packages/align-deps/src/vigilant.ts | 198 ++++ .../node_modules/conan/package.json | 14 + .../node_modules/dutch/package.json | 16 + .../node_modules/john/package.json | 15 + .../node_modules/quaid/package.json | 16 + .../node_modules/react-native/package.json | 7 + .../node_modules/react/package.json | 4 + .../node_modules/t-800/package.json | 18 + .../awesome-repo-extended/package.json | 19 + .../node_modules/conan/package.json | 14 + .../node_modules/dutch/package.json | 16 + .../node_modules/john/package.json | 15 + .../node_modules/quaid/package.json | 16 + .../node_modules/react-native/package.json | 7 + .../node_modules/react/package.json | 4 + .../node_modules/t-800/package.json | 16 + .../__fixtures__/awesome-repo/package.json | 19 + .../packageSpecificProfiles.js | 22 + .../custom-profiles/local-profiles.js | 0 .../custom-profiles-package/index.js | 0 .../custom-profiles-package/package.json | 5 + .../custom-profiles/root-level-profiles.js | 26 + .../custom-profiles/valid-profiles.js | 23 + .../node_modules/conan/package.json | 17 + .../node_modules/react-native/package.json | 7 + .../node_modules/react/package.json | 4 + .../node_modules/t-800/package.json | 17 + .../no-profile-satisfying-deps/package.json | 18 + .../test/__mocks__/@rnx-kit/config.js | 13 + packages/align-deps/test/__mocks__/chalk.js | 16 + packages/align-deps/test/__mocks__/fs.js | 19 + .../test/__snapshots__/check.app.test.ts.snap | 31 + .../test/__snapshots__/check.test.ts.snap | 37 + .../test/__snapshots__/profiles.test.ts.snap | 112 +++ .../test/__snapshots__/vigilant.test.ts.snap | 249 +++++ packages/align-deps/test/capabilities.test.ts | 276 ++++++ packages/align-deps/test/check.app.test.ts | 40 + packages/align-deps/test/check.test.ts | 359 +++++++ packages/align-deps/test/dependencies.test.ts | 239 +++++ .../align-deps/test/findBadPackages.test.ts | 115 +++ packages/align-deps/test/helpers.test.ts | 17 + packages/align-deps/test/helpers.ts | 20 + packages/align-deps/test/initialize.test.ts | 169 ++++ packages/align-deps/test/manifest.test.ts | 312 ++++++ packages/align-deps/test/profiles.test.ts | 265 +++++ packages/align-deps/test/setVersion.test.ts | 204 ++++ packages/align-deps/test/vigilant.test.ts | 477 +++++++++ packages/align-deps/tsconfig.json | 7 + 79 files changed, 8057 insertions(+) create mode 100644 .changeset/lemon-boats-learn.md create mode 100644 packages/align-deps/CHANGELOG.md create mode 100644 packages/align-deps/README.md create mode 100644 packages/align-deps/package.json create mode 100755 packages/align-deps/scripts/update-profile.mjs create mode 100755 packages/align-deps/scripts/update-readme.js create mode 100644 packages/align-deps/src/capabilities.ts create mode 100644 packages/align-deps/src/check.ts create mode 100644 packages/align-deps/src/cli.ts create mode 100644 packages/align-deps/src/dependencies.ts create mode 100644 packages/align-deps/src/findBadPackages.ts create mode 100644 packages/align-deps/src/helpers.ts create mode 100644 packages/align-deps/src/index.ts create mode 100644 packages/align-deps/src/initialize.ts create mode 100644 packages/align-deps/src/manifest.ts create mode 100644 packages/align-deps/src/presets/banned.ts create mode 100644 packages/align-deps/src/presets/microsoft/baseCapabilities.ts create mode 100644 packages/align-deps/src/presets/microsoft/index.ts create mode 100644 packages/align-deps/src/presets/microsoft/profile-0.61.ts create mode 100644 packages/align-deps/src/presets/microsoft/profile-0.62.ts create mode 100644 packages/align-deps/src/presets/microsoft/profile-0.63.ts create mode 100644 packages/align-deps/src/presets/microsoft/profile-0.64.ts create mode 100644 packages/align-deps/src/presets/microsoft/profile-0.65.ts create mode 100644 packages/align-deps/src/presets/microsoft/profile-0.66.ts create mode 100644 packages/align-deps/src/presets/microsoft/profile-0.67.ts create mode 100644 packages/align-deps/src/presets/microsoft/profile-0.68.ts create mode 100644 packages/align-deps/src/presets/microsoft/profile-0.69.ts create mode 100644 packages/align-deps/src/presets/microsoft/profile-0.70.ts create mode 100644 packages/align-deps/src/profiles.ts create mode 100644 packages/align-deps/src/setVersion.ts create mode 100644 packages/align-deps/src/types.ts create mode 100644 packages/align-deps/src/vigilant.ts create mode 100644 packages/align-deps/test/__fixtures__/awesome-repo-extended/node_modules/conan/package.json create mode 100644 packages/align-deps/test/__fixtures__/awesome-repo-extended/node_modules/dutch/package.json create mode 100644 packages/align-deps/test/__fixtures__/awesome-repo-extended/node_modules/john/package.json create mode 100644 packages/align-deps/test/__fixtures__/awesome-repo-extended/node_modules/quaid/package.json create mode 100644 packages/align-deps/test/__fixtures__/awesome-repo-extended/node_modules/react-native/package.json create mode 100644 packages/align-deps/test/__fixtures__/awesome-repo-extended/node_modules/react/package.json create mode 100644 packages/align-deps/test/__fixtures__/awesome-repo-extended/node_modules/t-800/package.json create mode 100644 packages/align-deps/test/__fixtures__/awesome-repo-extended/package.json create mode 100644 packages/align-deps/test/__fixtures__/awesome-repo/node_modules/conan/package.json create mode 100644 packages/align-deps/test/__fixtures__/awesome-repo/node_modules/dutch/package.json create mode 100644 packages/align-deps/test/__fixtures__/awesome-repo/node_modules/john/package.json create mode 100644 packages/align-deps/test/__fixtures__/awesome-repo/node_modules/quaid/package.json create mode 100644 packages/align-deps/test/__fixtures__/awesome-repo/node_modules/react-native/package.json create mode 100644 packages/align-deps/test/__fixtures__/awesome-repo/node_modules/react/package.json create mode 100644 packages/align-deps/test/__fixtures__/awesome-repo/node_modules/t-800/package.json create mode 100644 packages/align-deps/test/__fixtures__/awesome-repo/package.json create mode 100644 packages/align-deps/test/__fixtures__/config-custom-profiles-only/packageSpecificProfiles.js create mode 100644 packages/align-deps/test/__fixtures__/custom-profiles/local-profiles.js create mode 100644 packages/align-deps/test/__fixtures__/custom-profiles/node_modules/custom-profiles-package/index.js create mode 100644 packages/align-deps/test/__fixtures__/custom-profiles/node_modules/custom-profiles-package/package.json create mode 100644 packages/align-deps/test/__fixtures__/custom-profiles/root-level-profiles.js create mode 100644 packages/align-deps/test/__fixtures__/custom-profiles/valid-profiles.js create mode 100644 packages/align-deps/test/__fixtures__/no-profile-satisfying-deps/node_modules/conan/package.json create mode 100644 packages/align-deps/test/__fixtures__/no-profile-satisfying-deps/node_modules/react-native/package.json create mode 100644 packages/align-deps/test/__fixtures__/no-profile-satisfying-deps/node_modules/react/package.json create mode 100644 packages/align-deps/test/__fixtures__/no-profile-satisfying-deps/node_modules/t-800/package.json create mode 100644 packages/align-deps/test/__fixtures__/no-profile-satisfying-deps/package.json create mode 100644 packages/align-deps/test/__mocks__/@rnx-kit/config.js create mode 100644 packages/align-deps/test/__mocks__/chalk.js create mode 100644 packages/align-deps/test/__mocks__/fs.js create mode 100644 packages/align-deps/test/__snapshots__/check.app.test.ts.snap create mode 100644 packages/align-deps/test/__snapshots__/check.test.ts.snap create mode 100644 packages/align-deps/test/__snapshots__/profiles.test.ts.snap create mode 100644 packages/align-deps/test/__snapshots__/vigilant.test.ts.snap create mode 100644 packages/align-deps/test/capabilities.test.ts create mode 100644 packages/align-deps/test/check.app.test.ts create mode 100644 packages/align-deps/test/check.test.ts create mode 100644 packages/align-deps/test/dependencies.test.ts create mode 100644 packages/align-deps/test/findBadPackages.test.ts create mode 100644 packages/align-deps/test/helpers.test.ts create mode 100644 packages/align-deps/test/helpers.ts create mode 100644 packages/align-deps/test/initialize.test.ts create mode 100644 packages/align-deps/test/manifest.test.ts create mode 100644 packages/align-deps/test/profiles.test.ts create mode 100644 packages/align-deps/test/setVersion.test.ts create mode 100644 packages/align-deps/test/vigilant.test.ts create mode 100644 packages/align-deps/tsconfig.json diff --git a/.changeset/lemon-boats-learn.md b/.changeset/lemon-boats-learn.md new file mode 100644 index 000000000..a845151cc --- /dev/null +++ b/.changeset/lemon-boats-learn.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/packages/align-deps/CHANGELOG.md b/packages/align-deps/CHANGELOG.md new file mode 100644 index 000000000..385f2fc15 --- /dev/null +++ b/packages/align-deps/CHANGELOG.md @@ -0,0 +1,918 @@ +# Change Log - @rnx-kit/align-deps + +## 1.13.1 + +### Patch Changes + +- fda5a3fb: `@types/react-native` won't be needed from 0.71 on +- 86bf8afa: Bump `react-native-safe-area-context` for 0.70 + +## 1.13.0 + +### Minor Changes + +- 127b2bee: Do not warn for recent react-native-linear-gradient versions + +## 1.12.23 + +### Patch Changes + +- c067c1be: Allow specifying multiple packages on command line + +## 1.12.22 + +### Patch Changes + +- 17b7666f: react-native-gesture-handler 2.6.0 supports react-native 0.70 + +## 1.12.21 + +### Patch Changes + +- e78e4dae: Added profile for react-native 0.70 +- e3fd2c45: change file not found error to warning + +## 1.12.20 + +### Patch Changes + +- cc0ac7bf: - 0.68 + - @react-native-clipboard/clipboard -> ^1.10 + - react-native-svg -> ^12.3 + - react-native-webview -> ^11.22.6 + - 0.69 + - @react-native-async-storage/async-storage -> ^1.17.7 + - @react-native-masked-view/masked-view -> ^0.2.7 + - react-native-reanimated -> ^2.9 + - react-native-screens -> ^3.14.1 + - react-native-webview -> ^11.23 + +## 1.12.19 + +### Patch Changes + +- ac77ec69: Bump for a build with latest @rnx-kit dependencies + +## 1.12.18 + +### Patch Changes + +- 37245c33: Introducing `@rnx-kit/tools-workspaces`, a collection of tools for working with workspaces. + +## 1.12.17 + +### Patch Changes + +- 641edba5: Add profile for react-native 0.69 + +## 1.12.16 + +### Patch Changes + +- 1edb9acd: Fix dep-check failing to visit dependencies in a repository using pnpm or pnpm-like solutions + +## 1.12.15 + +### Patch Changes + +- 4b777cf9: Bumps `@react-navigation/native` and `@react-navigation/stack` to 6.0.8 and 6.2.0 respectively for 0.66. + +## 1.12.14 + +### Patch Changes + +- 569a099: Bump @rnx-kit/tools-node to v1.2.7 + +## 1.12.13 + +### Patch Changes + +- cb795e3: Omit empty sections from the manifest + +## 1.12.12 + +### Patch Changes + +- 3ee09f6: Fix Rush workspaces not being detected when set up as a post-install step + +## 1.12.11 + +### Patch Changes + +- d950055: Dependencies should be sorted lexicographically + +## 1.12.10 + +### Patch Changes + +- d48475b: Allow specifying `--custom-profiles` with `--init` + +## 1.12.9 + +### Patch Changes + +- b09c0bb: react-native-gestures 2.3.0 works with react-native 0.68 +- b09c0bb: reanimated 2.5.0 supports react-native 0.68 +- b09c0bb: react-native-screens 3.13.1 now supports Fabric + +## 1.12.8 + +### Patch Changes + +- 4a2bd9a: react-native-lazy-index was renamed to @rnx-kit/react-native-lazy-index + +## 1.12.7 + +### Patch Changes + +- 2169c8f: Add more descriptive error message when validating manifest + +## 1.12.6 + +### Patch Changes + +- a4988f1: Explicitly declare support for Node 12+ +- 89af18f: Fix dev version being set regardless of whether a package was configured when running in `--vigilant` mode + +## 1.12.5 + +### Patch Changes + +- 3b43647: Keep version ranges defined in `peerDependencies` if they are a superset +- 6bddfc6: Fix dev version not being set correctly in `--vigilant` mode + +## 1.12.4 + +### Patch Changes + +- 0eb8b8b: Bump workspace-tools to 0.18.2 for performance improvements +- 64ce5a1: Bump react-native-screens for 0.66 and 0.68 + +## 1.12.3 + +### Patch Changes + +- 4c747fd: Add profile for react-native 0.68 +- 730a167: Also look for react-native under `devDependencies` + +## 1.12.2 + +### Patch Changes + +- b7e60e9: Don't fail on profiles that contain only common dependencies + +## 1.12.1 + +### Patch Changes + +- 55106b7: Bump react-native-test-app to 1.0.6 to fix Gradle running out of heap space during the lint task on some machines. + +## 1.12.0 + +### Minor Changes + +- f385a26: Allow declaring common capabilities at the root level + +### Patch Changes + +- 868be32: Bump react-native-test-app to 0.11.4 to support react-native-macos 0.66 +- d10f4b0: Use react-native-test-app 1.0 from react-native 0.66+ + +## 1.11.0 + +### Minor Changes + +- ca8c634: dep-check should pick up `customProfiles` when running in `--vigilant` mode to allow individual packages to use different profiles without having to re-declare which React Native versions they support. + +### Patch Changes + +- 0f7793e: Bump react-native-test-app 0.11.2 to support react-native 0.67 + +## 1.10.2 + +### Patch Changes + +- feb1613: fix(dep-check): bump react-native-test-app to 0.11.0 + +## 1.10.1 + +### Patch Changes + +- 80a9e64: Bump react-native-test-app to 0.10.2 +- 8f868c4: Bump react-native-test-app to 0.10.1 + +## 1.10.0 + +### Minor Changes + +- 32b681b: All dependencies of all packages, including configured ones, should be checked when `--vigilant` is specified. + +## 1.9.6 + +### Patch Changes + +- 9816461: Fix loading of custom profiles from relative paths + +## 1.9.5 + +Tue, 30 Nov 2021 17:24:14 GMT + +### Patches + +- Bump @rnx-kit/console to v1.0.11 +- Bump @rnx-kit/tools-language to v1.2.6 +- Bump @rnx-kit/tools-node to v1.2.6 + +## 1.9.4 + +Thu, 18 Nov 2021 20:51:05 GMT + +### Patches + +- Bump @rnx-kit/console to v1.0.10 +- Bump @rnx-kit/tools-language to v1.2.5 +- Bump @rnx-kit/tools-node to v1.2.5 + +## 1.9.3 + +Tue, 16 Nov 2021 14:33:15 GMT + +### Patches + +- Bump netinfo to fix autolinking on Windows (4123478+tido64@users.noreply.github.com) + +## 1.9.2 + +Mon, 15 Nov 2021 12:33:07 GMT + +### Patches + +- Bump clipboard for react-native update fixes (4123478+tido64@users.noreply.github.com) + +## 1.9.1 + +Fri, 12 Nov 2021 13:04:39 GMT + +### Patches + +- Fix dep-check not being executable (4123478+tido64@users.noreply.github.com) + +## 1.9.0 + +Thu, 11 Nov 2021 17:49:21 GMT + +### Minor changes + +- Bundle dep-check to avoid conflicting dependencies, and to make the installation footprint smaller. (4123478+tido64@users.noreply.github.com) + +## 1.8.18 + +Mon, 08 Nov 2021 10:20:15 GMT + +### Patches + +- Bump @rnx-kit/config to v0.4.19 + +## 1.8.17 + +Fri, 05 Nov 2021 19:24:49 GMT + +### Patches + +- Bump @rnx-kit/config to v0.4.18 +- Bump @rnx-kit/console to v1.0.9 +- Bump @rnx-kit/tools-language to v1.2.4 +- Bump @rnx-kit/tools-node to v1.2.4 + +## 1.8.16 + +Fri, 05 Nov 2021 07:33:42 GMT + +### Patches + +- Bump @rnx-kit/config to v0.4.17 +- Bump @rnx-kit/console to v1.0.8 +- Bump @rnx-kit/tools-language to v1.2.3 +- Bump @rnx-kit/tools-node to v1.2.3 + +## 1.8.15 + +Wed, 03 Nov 2021 18:15:39 GMT + +### Patches + +- Bump @rnx-kit/config to v0.4.16 +- Bump @rnx-kit/console to v1.0.7 +- Bump @rnx-kit/tools-language to v1.2.2 +- Bump @rnx-kit/tools-node to v1.2.2 + +## 1.8.14 + +Mon, 01 Nov 2021 13:46:13 GMT + +### Patches + +- Bump @rnx-kit/config to v0.4.15 +- Bump @rnx-kit/console to v1.0.6 +- Bump @rnx-kit/tools-language to v1.2.1 +- Bump @rnx-kit/tools-node to v1.2.1 + +## 1.8.13 + +Sat, 30 Oct 2021 07:50:51 GMT + +### Patches + +- Bump @rnx-kit/config to v0.4.14 +- Bump @rnx-kit/tools-language to v1.2.0 +- Bump @rnx-kit/tools-node to v1.2.0 + +## 1.8.12 + +Fri, 29 Oct 2021 14:13:32 GMT + +### Patches + +- Recommend react-native-reanimated@^2.2.4 for 0.67 (4123478+tido64@users.noreply.github.com) + +## 1.8.11 + +Fri, 29 Oct 2021 12:14:31 GMT + +### Patches + +- Bump @rnx-kit/config to v0.4.13 +- Bump @rnx-kit/console to v1.0.5 +- Bump @rnx-kit/tools-language to v1.1.4 +- Bump @rnx-kit/tools-node to v1.1.6 + +## 1.8.10 + +Fri, 29 Oct 2021 10:31:10 GMT + +### Patches + +- Bump @rnx-kit/config to v0.4.12 +- Bump @rnx-kit/console to v1.0.4 +- Bump @rnx-kit/tools-language to v1.1.3 +- Bump @rnx-kit/tools-node to v1.1.5 + +## 1.8.9 + +Fri, 29 Oct 2021 08:51:30 GMT + +### Patches + +- Bump @rnx-kit/config to v0.4.11 +- Bump @rnx-kit/console to v1.0.3 +- Bump @rnx-kit/tools-language to v1.1.2 +- Bump @rnx-kit/tools-node to v1.1.4 + +## 1.8.8 + +Tue, 26 Oct 2021 17:22:16 GMT + +### Patches + +- Bumped react-native-reanimated for 0.66, and added profile for 0.67 (4123478+tido64@users.noreply.github.com) + +## 1.8.7 + +Thu, 14 Oct 2021 07:54:03 GMT + +### Patches + +- Bump react-native-test-app to ^0.9.0 (4123478+tido64@users.noreply.github.com) + +## 1.8.6 + +Wed, 29 Sep 2021 11:02:41 GMT + +### Patches + +- add jest, add meta for core testing, stabilize core with react capability (lsciandra@microsoft.com) +- Bump @rnx-kit/config to v0.4.10 + +## 1.8.5 + +Wed, 29 Sep 2021 09:09:11 GMT + +### Patches + +- Make profile for 0.66 public (4123478+tido64@users.noreply.github.com) + +## 1.8.4 + +Mon, 27 Sep 2021 12:28:41 GMT + +### Patches + +- Add react dom and test renderer to capabilities (lsciandra@microsoft.com) +- Bump @rnx-kit/config to v0.4.9 + +## 1.8.3 + +Mon, 27 Sep 2021 10:56:47 GMT + +### Patches + +- Add metro babel preset to capabilities (lsciandra@microsoft.com) +- Bump @rnx-kit/config to v0.4.8 + +## 1.8.2 + +Tue, 14 Sep 2021 15:28:16 GMT + +### Patches + +- Bump async-storage to 1.15.8. It contains fixes for building Android on react-native 0.65. (4123478+tido64@users.noreply.github.com) + +## 1.8.1 + +Mon, 13 Sep 2021 17:38:26 GMT + +### Patches + +- Updated profile 0.65 with latest versions. Added a preliminary profile for 0.66. (4123478+tido64@users.noreply.github.com) + +## 1.8.0 + +Wed, 08 Sep 2021 07:04:15 GMT + +### Minor changes + +- Add support for dependencies and meta packages (4123478+tido64@users.noreply.github.com) + +## 1.7.11 + +Wed, 08 Sep 2021 06:42:50 GMT + +### Patches + +- Also check workspace root package (4123478+tido64@users.noreply.github.com) +- Bump @rnx-kit/dep-check to v1.7.11 (4123478+tido64@users.noreply.github.com) + +## 1.7.10 + +Mon, 06 Sep 2021 06:57:59 GMT + +### Patches + +- When upgrading profile version, also remove `reactNativeDevVersion` if `kitType` is `app` (4123478+tido64@users.noreply.github.com) + +## 1.7.9 + +Fri, 03 Sep 2021 12:18:30 GMT + +### Patches + +- Preserve the indentation when modifying `package.json` (4123478+tido64@users.noreply.github.com) +- Bump @rnx-kit/dep-check to v1.7.9 (4123478+tido64@users.noreply.github.com) + +## 1.7.8 + +Fri, 03 Sep 2021 09:49:28 GMT + +### Patches + +- Fix broken `--init` due to loose and init being mutually exclusive, but `--loose` has a default value. (4123478+tido64@users.noreply.github.com) + +## 1.7.7 + +Tue, 31 Aug 2021 10:50:41 GMT + +### Patches + +- Bump @rnx-kit/dep-check to v1.7.7 (4123478+tido64@users.noreply.github.com) + +## 1.7.6 + +Tue, 31 Aug 2021 06:43:13 GMT + +### Patches + +- Stricter handling of errors (4123478+tido64@users.noreply.github.com) +- Bump @rnx-kit/dep-check to v1.7.6 (4123478+tido64@users.noreply.github.com) + +## 1.7.5 + +Fri, 27 Aug 2021 18:41:43 GMT + +### Patches + +- Bump @rnx-kit/dep-check to v1.7.5 (4123478+tido64@users.noreply.github.com) + +## 1.7.4 + +Wed, 25 Aug 2021 08:52:48 GMT + +### Patches + +- Bump @rnx-kit/dep-check to v1.7.4 (afoxman@microsoft.com) + +## 1.7.3 + +Wed, 25 Aug 2021 07:32:57 GMT + +### Patches + +- Bump @rnx-kit/dep-check to v1.7.3 (afoxman@microsoft.com) + +## 1.7.2 + +Sat, 21 Aug 2021 08:22:48 GMT + +### Patches + +- Integrate tools package and other common libraries throughout monorepo, removing custom code. (afoxman@microsoft.com) +- Bump @rnx-kit/dep-check to v1.7.2 (afoxman@microsoft.com) + +## 1.7.1 + +Thu, 19 Aug 2021 07:59:20 GMT + +### Patches + +- Fix error messages accumulating when gathering requirements (4123478+tido64@users.noreply.github.com) + +## 1.7.0 + +Wed, 18 Aug 2021 14:54:20 GMT + +### Minor changes + +- Allow apps to depend on a newer version of React Native than their dependencies declare support for via the `--loose` flag. (4123478+tido64@users.noreply.github.com) + +## 1.6.0 + +Tue, 17 Aug 2021 09:36:56 GMT + +### Minor changes + +- Add command for setting react-native version (4123478+tido64@users.noreply.github.com) + +## 1.5.21 + +Fri, 13 Aug 2021 13:30:40 GMT + +### Patches + +- Correct hermes-engine version for react-native 0.65 (4123478+tido64@users.noreply.github.com) + +## 1.5.20 + +Fri, 06 Aug 2021 22:07:45 GMT + +### Patches + +- Bump @rnx-kit/dep-check to v1.5.20 (afoxman@microsoft.com) + +## 1.5.19 + +Fri, 06 Aug 2021 18:05:53 GMT + +### Patches + +- Bump @rnx-kit/dep-check to v1.5.19 (afoxman@microsoft.com) + +## 1.5.18 + +Wed, 04 Aug 2021 10:08:23 GMT + +### Patches + +- Bump @rnx-kit/dep-check to v1.5.18 (4123478+tido64@users.noreply.github.com) + +## 1.5.17 + +Thu, 29 Jul 2021 19:42:04 GMT + +### Patches + +- Bump @rnx-kit/dep-check to v1.5.17 (4123478+tido64@users.noreply.github.com) + +## 1.5.16 + +Mon, 26 Jul 2021 15:59:59 GMT + +### Patches + +- Add 'core' as an alias for react-native for out-of-tree platform packages that have a dependency on `react-native` core code, and not the Android/iOS specific bits. Currently, one would have to add a random capability that resolves to `react-native`, e.g. `core-android`, despite the capability not really being used. (4123478+tido64@users.noreply.github.com) +- Bump @rnx-kit/dep-check to v1.5.16 (4123478+tido64@users.noreply.github.com) + +## 1.5.15 + +Tue, 13 Jul 2021 13:40:11 GMT + +### Patches + +- bump RNTA to 0.7 (lsciandra@microsoft.com) + +## 1.5.14 + +Mon, 12 Jul 2021 17:30:15 GMT + +### Patches + +- Bump @rnx-kit/dep-check to v1.5.14 (4123478+tido64@users.noreply.github.com) + +## 1.5.13 + +Mon, 12 Jul 2021 08:34:12 GMT + +### Patches + +- Added link to documentation in output (4123478+tido64@users.noreply.github.com) + +## 1.5.12 + +Mon, 12 Jul 2021 07:51:46 GMT + +### Patches + +- Bump @rnx-kit/dep-check to v1.5.12 (afoxman@microsoft.com) + +## 1.5.11 + +Fri, 09 Jul 2021 12:17:59 GMT + +### Patches + +- Bump checkbox and test-app to latest (4123478+tido64@users.noreply.github.com) + +## 1.5.10 + +Thu, 01 Jul 2021 13:59:39 GMT + +### Patches + +- Bump netinfo to 5.9.10 (4123478+tido64@users.noreply.github.com) + +## 1.5.9 + +Mon, 28 Jun 2021 14:19:44 GMT + +### Patches + +- Added missing dependency (4123478+tido64@users.noreply.github.com) + +## 1.5.8 + +Fri, 25 Jun 2021 16:53:16 GMT + +### Patches + +- Bump react-native-test-app to 0.6.3 (4123478+tido64@users.noreply.github.com) + +## 1.5.7 + +Wed, 23 Jun 2021 17:54:11 GMT + +### Patches + +- Use common console logger (4123478+tido64@users.noreply.github.com) + +## 1.5.6 + +Tue, 22 Jun 2021 15:04:23 GMT + +### Patches + +- Bumped chalk to 4.1.0, and workspace-tools to 0.16.2 (4123478+tido64@users.noreply.github.com) + +## 1.5.5 + +Mon, 21 Jun 2021 17:32:05 GMT + +### Patches + +- Promote @react-native-masked-view/masked-view over @react-native-community/masked-view (4123478+tido64@users.noreply.github.com) + +## 1.5.4 + +Mon, 21 Jun 2021 11:43:28 GMT + +### Patches + +- Warn about renamed packages: `@react-native-community/async-storage` -> `@react-native-async-storage/async-storage` and `@react-native-community/masked-view` -> `@react-native-masked-view/masked-view` (4123478+tido64@users.noreply.github.com) + +## 1.5.3 + +Thu, 17 Jun 2021 06:05:20 GMT + +### Patches + +- Bumped react-native 0.64.1 -> 0.64.2 (4123478+tido64@users.noreply.github.com) + +## 1.5.2 + +Fri, 04 Jun 2021 12:36:37 GMT + +### Patches + +- Fixed older yargs versions not ignoring flags with default values when looking for conflicts (4123478+tido64@users.noreply.github.com) + +## 1.5.1 + +Fri, 04 Jun 2021 09:02:33 GMT + +### Patches + +- Added --exclude-packages to vigilant mode (4123478+tido64@users.noreply.github.com) + +## 1.5.0 + +Wed, 02 Jun 2021 17:08:58 GMT + +### Minor changes + +- Add --vigilant flag for zero-config mode (4123478+tido64@users.noreply.github.com) + +## 1.4.2 + +Thu, 27 May 2021 06:09:59 GMT + +### Patches + +- Fix 'devOnly' being ignored in custom profiles (4123478+tido64@users.noreply.github.com) + +## 1.4.1 + +Wed, 26 May 2021 13:22:22 GMT + +### Patches + +- Removed core-win32 capability (4123478+tido64@users.noreply.github.com) + +## 1.4.0 + +Wed, 26 May 2021 06:53:03 GMT + +### Minor changes + +- Added support for custom profiles (4123478+tido64@users.noreply.github.com) + +### Patches + +- Bump @rnx-kit/config to v0.2.7 (4123478+tido64@users.noreply.github.com) + +## 1.3.0 + +Thu, 20 May 2021 16:06:43 GMT + +### Minor changes + +- Add support for workspaces (4123478+tido64@users.noreply.github.com) + +## 1.2.2 + +Thu, 20 May 2021 15:24:25 GMT + +### Patches + +- Bump react-native-test-app to 0.5.9 to address an issue with linters complaining about an old version of Dagger being used. (4123478+tido64@users.noreply.github.com) + +## 1.2.1 + +Tue, 18 May 2021 09:25:17 GMT + +### Patches + +- Ignore whitespace differences (4123478+tido64@users.noreply.github.com) + +## 1.2.0 + +Sat, 15 May 2021 09:02:22 GMT + +### Minor changes + +- Added command for initializing a configuration (4123478+tido64@users.noreply.github.com) + +## 1.1.10 + +Sat, 15 May 2021 08:55:08 GMT + +### Patches + +- Print instructions when changes are needed. (4123478+tido64@users.noreply.github.com) + +## 1.1.9 + +Sat, 15 May 2021 08:49:14 GMT + +### Patches + +- Exclude dev-only capabilities from requirements (4123478+tido64@users.noreply.github.com) + +## 1.1.8 + +Wed, 12 May 2021 11:52:17 GMT + +### Patches + +- Avoid installing unnecessary core capabilities (4123478+tido64@users.noreply.github.com) + +## 1.1.7 + +Tue, 11 May 2021 15:41:12 GMT + +### Patches + +- Bump react-native-test-app for Xcode 12.5 fixes (4123478+tido64@users.noreply.github.com) + +## 1.1.6 + +Tue, 11 May 2021 15:28:18 GMT + +### Patches + +- Allow dev-only dependencies should always be added (4123478+tido64@users.noreply.github.com) +- Rollback @react-navigation/native as 5.9.6 doesn't exist (4123478+tido64@users.noreply.github.com) + +## 1.1.5 + +Mon, 10 May 2021 21:58:48 GMT + +### Patches + +- Allow direct dependency on react-native-test-app (4123478+tido64@users.noreply.github.com) + +## 1.1.4 + +Mon, 10 May 2021 14:10:30 GMT + +### Patches + +- Libraries should not re-declare transitive dependencies (4123478+tido64@users.noreply.github.com) + +## 1.1.3 + +Sat, 08 May 2021 20:35:26 GMT + +### Patches + +- Fix a crash in react-native-lazy-index when non-JS files are read (4123478+tido64@users.noreply.github.com) + +## 1.1.2 + +Wed, 05 May 2021 21:00:14 GMT + +### Patches + +- Add preliminary profile for 0.65 (4123478+tido64@users.noreply.github.com) + +## 1.1.1 + +Wed, 05 May 2021 20:55:03 GMT + +### Patches + +- Bump react-native to address build issues with Xcode 12.5 (4123478+tido64@users.noreply.github.com) + +## 1.1.0 + +Wed, 05 May 2021 19:51:01 GMT + +### Minor changes + +- Apply transitive requirements (4123478+tido64@users.noreply.github.com) + +### Patches + +- Bump @rnx-kit/config to v0.2.6 (4123478+tido64@users.noreply.github.com) + +## 1.0.3 + +Thu, 29 Apr 2021 13:47:02 GMT + +### Patches + +- Bump react-native to latest 0.63.x (4123478+tido64@users.noreply.github.com) + +## 1.0.2 + +Wed, 28 Apr 2021 16:03:56 GMT + +### Patches + +- Expose cli for cli related integrations (4123478+tido64@users.noreply.github.com) + +## 1.0.1 + +Wed, 28 Apr 2021 15:54:06 GMT + +### Patches + +- Add 'hermes' capability (4123478+tido64@users.noreply.github.com) + +## 1.0.0 + +Tue, 27 Apr 2021 19:43:40 GMT + +### Minor changes + +- Initial dependency checker (4123478+tido64@users.noreply.github.com) + +### Patches + +- Bump @rnx-kit/config to v0.2.4 (4123478+tido64@users.noreply.github.com) diff --git a/packages/align-deps/README.md b/packages/align-deps/README.md new file mode 100644 index 000000000..7f78076f7 --- /dev/null +++ b/packages/align-deps/README.md @@ -0,0 +1,394 @@ + + +# @rnx-kit/align-deps + +[![Build](https://github.com/microsoft/rnx-kit/actions/workflows/build.yml/badge.svg)](https://github.com/microsoft/rnx-kit/actions/workflows/build.yml) +[![npm version](https://img.shields.io/npm/v/@rnx-kit/align-deps)](https://www.npmjs.com/package/@rnx-kit/align-deps) + + + +`@rnx-kit/align-deps` manages React Native dependencies for a package, based on +its needs and requirements. + +If you want to learn how align-deps is used at Microsoft, and see a demo of how +it works in a monorepo - you can watch the +["Improve all the repos – exploring Microsoft’s DevExp"](https://youtu.be/DAEnPV78rQc?t=1085) +talk by @kelset and @tido64 from React Native Europe 2021. + +## Installation + +```sh +yarn add @rnx-kit/align-deps --dev +``` + +or if you're using npm + +```sh +npm add --save-dev @rnx-kit/align-deps +``` + +## Usage + +```sh +yarn rnx-align-deps [options] [packages...] +``` + +Listing paths to packages that should be checked is optional. If omitted, +align-deps will look for the closest `package.json` using Node module +resolution. If the target package is a root package defining workspaces, they +will all be included. + +Examples: + +- Ensure dependencies are compatible with react-native 0.64 without a config: + ```sh + yarn rnx-dep-check --vigilant 0.64 + ``` +- Initialize a config for your app (or library): + ```sh + yarn rnx-dep-check --init app + # or specify `library` for a library + ``` +- Apply changes suggested by dep-check: + ```sh + yarn rnx-dep-check --write + ``` +- Interactively update supported react-native versions (or bump version used for + development): + ```sh + yarn rnx-dep-check --set-version + ``` + +### `--custom-profiles ` + +Path to custom profiles. This can be a path to a JSON file, a `.js` file, or a +module name. The module must default export an object similar to the one below: + +```js +module.exports = { + 0.63: { + "my-capability": { + name: "my-module", + version: "1.0.0", + }, + }, + 0.64: { + "my-capability": { + name: "my-module", + version: "1.1.0", + }, + }, +}; +``` + +For a more complete example, have a look at the +[default profiles](https://github.com/microsoft/rnx-kit/blob/769e9fa290929effd5111884f1637c21326b5a95/packages/dep-check/src/profiles.ts#L11). + +> #### Note +> +> This specific flag may only be used with `--vigilant`. You can specify custom +> profiles in normal mode by adding `customProfiles` to your package +> [configuration](#configure). + +### `--exclude-packages` + +Comma-separated list of package names to exclude from inspection. + +> #### Note +> +> `--exclude-packages` will only exclude packages that do not have a +> configuration. Packages that have a configuration, will still be checked. This +> flag may only be used with `--vigilant`. + +### `--init ` + +When integrating `@rnx-kit/dep-check` for the first time, it may be a cumbersome +to manually add all capabilities yourself. You can run this tool with `--init`, +and it will try to add a sensible configuration based on what is currently +defined in the specified `package.json`. + +### `--set-version` + +Sets `reactNativeVersion` and `reactNativeDevVersion` for any configured +package. The value should be a comma-separated list of `react-native` versions +to set. The first number specifies the development version. For example, +`0.64,0.63` will set the following values: + +```json +{ + "rnx-kit": { + "reactNativeVersion": "^0.63.0 || ^0.64.0", + "reactNativeDevVersion": "^0.64.0" + } +} +``` + +If the version numbers are omitted, an _interactive prompt_ will appear. + +> #### Note +> +> A `rnx-dep-check --write` run will be invoked right after changes have been +> made. As such, this flag will fail if changes are needed before making any +> modifications. + +### `--vigilant` + +Also inspect packages that are not configured. Specify a comma-separated list of +profile versions to compare against, e.g. `0.63,0.64`. The first number +specifies the target version. + +### `--write` + +Writes all proposed changes to the specified `package.json`. + +## Configure + +`@rnx-kit/dep-check` must first be configured before it can be used. It uses +`@rnx-kit/config` to retrieve your kit configuration. Your configuration can be +specified either in a file, `rnx-kit.config.js`, or in an `"rnx-kit"` section of +your `package.json`. + +| Option | Type | Default | Description | +| :---------------------- | :--------------------- | :-------------------- | :---------------------------------------------------------------------------------------------------------- | +| `kitType` | `"app"` \| `"library"` | `"library"` | Whether this kit is an "app" or a "library". Determines how dependencies are declared. | +| `reactNativeVersion` | string | (required) | Supported versions of React Native. The value can be a specific version or a range. | +| `reactNativeDevVersion` | string | `minVersion(version)` | The version of React Native to use for development. If omitted, the minimum supported version will be used. | +| `capabilities` | Capabilities[] | `[]` | List of used/provided capabilities. A full list can be found below. | +| `customProfiles` | string | `undefined` | Path to custom profiles. This can be a path to a JSON file, a `.js` file, or a module name. | + +## Capabilities + +The following table contains the currently supported capabilities and what they +resolve to: + +
+Capabilities Table + + + + +| Capability | 0.70 | 0.69 | 0.68 | 0.67 | 0.66 | 0.65 | 0.64 | 0.63 | 0.62 | 0.61 | +| ------------------------------------ | ----------------------------------------------------------------- | ----------------------------------------------------------------- | ----------------------------------------------------------------- | ----------------------------------------------------------------- | ----------------------------------------------------------------- | ----------------------------------------------------------------- | ----------------------------------------------------------------- | ----------------------------------------------------------------- | ----------------------------------------------------------------- | ----------------------------------------------------------------- | +| core | react-native@^0.70.0 | react-native@^0.69.0 | react-native@^0.68.0 | react-native@^0.67.0 | react-native@^0.66.0 | react-native@^0.65.0 | react-native@^0.64.2 | react-native@^0.63.2 | react-native@^0.62.3 | react-native@^0.61.5 | +| core-android | react-native@^0.70.0 | react-native@^0.69.0 | react-native@^0.68.0 | react-native@^0.67.0 | react-native@^0.66.0 | react-native@^0.65.0 | react-native@^0.64.2 | react-native@^0.63.2 | react-native@^0.62.3 | react-native@^0.61.5 | +| core-ios | react-native@^0.70.0 | react-native@^0.69.0 | react-native@^0.68.0 | react-native@^0.67.0 | react-native@^0.66.0 | react-native@^0.65.0 | react-native@^0.64.2 | react-native@^0.63.2 | react-native@^0.62.3 | react-native@^0.61.5 | +| core-macos | react-native-macos@^0.70.0 | react-native-macos@^0.69.0 | react-native-macos@^0.68.0 | react-native-macos@^0.67.0 | react-native-macos@^0.66.0 | react-native-macos@^0.65.0 | react-native-macos@^0.64.0 | react-native-macos@^0.63.0 | react-native-macos@^0.62.0 | react-native-macos@^0.61.0 | +| core-windows | react-native-windows@^0.70.0 | react-native-windows@^0.69.0 | react-native-windows@^0.68.0 | react-native-windows@^0.67.0 | react-native-windows@^0.66.0 | react-native-windows@^0.65.0 | react-native-windows@^0.64.0 | react-native-windows@^0.63.0 | react-native-windows@^0.62.0 | react-native-windows@^0.61.0 | +| animation | react-native-reanimated@^2.10.0 | react-native-reanimated@^2.9.0 | react-native-reanimated@^2.5.0 | react-native-reanimated@^2.2.4 | react-native-reanimated@^2.2.3 | react-native-reanimated@^2.2.1 | react-native-reanimated@^2.1.0 | react-native-reanimated@^1.13.3 | react-native-reanimated@^1.13.3 | react-native-reanimated@^1.13.3 | +| babel-preset-react-native | metro-react-native-babel-preset@^0.72.1 | metro-react-native-babel-preset@^0.70.3 | metro-react-native-babel-preset@^0.67.0 | metro-react-native-babel-preset@^0.66.2 | metro-react-native-babel-preset@^0.66.2 | metro-react-native-babel-preset@^0.66.0 | metro-react-native-babel-preset@^0.64.0 | metro-react-native-babel-preset@^0.59.0 | metro-react-native-babel-preset@^0.58.0 | metro-react-native-babel-preset@^0.56.0 | +| base64 | react-native-base64@^0.2.1 | react-native-base64@^0.2.1 | react-native-base64@^0.2.1 | react-native-base64@^0.2.1 | react-native-base64@^0.2.1 | react-native-base64@^0.2.1 | react-native-base64@^0.2.1 | react-native-base64@^0.2.1 | react-native-base64@^0.2.1 | react-native-base64@^0.2.1 | +| checkbox | @react-native-community/checkbox@^0.5.8 | @react-native-community/checkbox@^0.5.8 | @react-native-community/checkbox@^0.5.8 | @react-native-community/checkbox@^0.5.8 | @react-native-community/checkbox@^0.5.8 | @react-native-community/checkbox@^0.5.8 | @react-native-community/checkbox@^0.5.8 | @react-native-community/checkbox@^0.5.7 | @react-native-community/checkbox@^0.5.7 | @react-native-community/checkbox@^0.5.7 | +| clipboard | @react-native-clipboard/clipboard@^1.10.0 | @react-native-clipboard/clipboard@^1.10.0 | @react-native-clipboard/clipboard@^1.10.0 | @react-native-clipboard/clipboard@^1.9.0 | @react-native-clipboard/clipboard@^1.9.0 | @react-native-clipboard/clipboard@^1.9.0 | @react-native-clipboard/clipboard@^1.8.3 | @react-native-community/clipboard@^1.5.1 | @react-native-community/clipboard@^1.5.1 | @react-native-community/clipboard@^1.5.1 | +| core/testing | Meta package for installing `core`, `jest`, `react-test-renderer` | Meta package for installing `core`, `jest`, `react-test-renderer` | Meta package for installing `core`, `jest`, `react-test-renderer` | Meta package for installing `core`, `jest`, `react-test-renderer` | Meta package for installing `core`, `jest`, `react-test-renderer` | Meta package for installing `core`, `jest`, `react-test-renderer` | Meta package for installing `core`, `jest`, `react-test-renderer` | Meta package for installing `core`, `jest`, `react-test-renderer` | Meta package for installing `core`, `jest`, `react-test-renderer` | Meta package for installing `core`, `jest`, `react-test-renderer` | +| datetime-picker | @react-native-community/datetimepicker@^6.0.2 | @react-native-community/datetimepicker@^6.0.2 | @react-native-community/datetimepicker@^6.0.2 | @react-native-community/datetimepicker@^3.5.2 | @react-native-community/datetimepicker@^3.5.2 | @react-native-community/datetimepicker@^3.5.2 | @react-native-community/datetimepicker@^3.4.6 | @react-native-community/datetimepicker@^3.0.9 | @react-native-community/datetimepicker@^3.0.9 | @react-native-community/datetimepicker@^3.0.9 | +| filesystem | react-native-fs@^2.18.0 | react-native-fs@^2.18.0 | react-native-fs@^2.18.0 | react-native-fs@^2.18.0 | react-native-fs@^2.18.0 | react-native-fs@^2.18.0 | react-native-fs@^2.17.0 | react-native-fs@^2.16.6 | react-native-fs@^2.16.6 | react-native-fs@^2.16.6 | +| floating-action | react-native-floating-action@^1.22.0 | react-native-floating-action@^1.22.0 | react-native-floating-action@^1.22.0 | react-native-floating-action@^1.22.0 | react-native-floating-action@^1.22.0 | react-native-floating-action@^1.22.0 | react-native-floating-action@^1.21.0 | react-native-floating-action@^1.21.0 | react-native-floating-action@^1.18.0 | react-native-floating-action@^1.18.0 | +| gestures | react-native-gesture-handler@^2.6.0 | react-native-gesture-handler@^2.5.0 | react-native-gesture-handler@^2.3.2 | react-native-gesture-handler@^1.10.3 | react-native-gesture-handler@^1.10.3 | react-native-gesture-handler@^1.10.3 | react-native-gesture-handler@^1.10.3 | react-native-gesture-handler@^1.10.3 | react-native-gesture-handler@^1.9.0 | react-native-gesture-handler@^1.9.0 | +| hermes | hermes-engine@~0.11.0 | hermes-engine@~0.11.0 | hermes-engine@~0.11.0 | hermes-engine@~0.9.0 | hermes-engine@~0.9.0 | hermes-engine@~0.8.1 | hermes-engine@~0.7.0 | hermes-engine@~0.5.0 | hermes-engine@~0.4.0 | hermes-engine@^0.2.1 | +| hooks | @react-native-community/hooks@^2.8.0 | @react-native-community/hooks@^2.8.0 | @react-native-community/hooks@^2.8.0 | @react-native-community/hooks@^2.8.0 | @react-native-community/hooks@^2.8.0 | @react-native-community/hooks@^2.8.0 | @react-native-community/hooks@^2.6.0 | @react-native-community/hooks@^2.6.0 | @react-native-community/hooks@^2.6.0 | @react-native-community/hooks@^2.6.0 | +| html | react-native-render-html@^6.1.0 | react-native-render-html@^6.1.0 | react-native-render-html@^6.1.0 | react-native-render-html@^6.1.0 | react-native-render-html@^6.1.0 | react-native-render-html@^5.1.1 | react-native-render-html@^5.1.1 | react-native-render-html@^5.1.0 | react-native-render-html@^5.1.0 | react-native-render-html@^5.1.0 | +| jest | jest@^26.6.3 | jest@^26.6.3 | jest@^26.6.3 | jest@^26.6.3 | jest@^26.6.3 | jest@^26.6.3 | jest@^26.5.2 | jest@^24.9.0 | jest@^24.8.0 | jest@^24.8.0 | +| lazy-index | @rnx-kit/react-native-lazy-index@^2.1.7 | @rnx-kit/react-native-lazy-index@^2.1.7 | @rnx-kit/react-native-lazy-index@^2.1.7 | @rnx-kit/react-native-lazy-index@^2.1.7 | @rnx-kit/react-native-lazy-index@^2.1.7 | react-native-lazy-index@^2.1.1 | react-native-lazy-index@^2.1.1 | react-native-lazy-index@^2.1.1 | react-native-lazy-index@^2.1.1 | react-native-lazy-index@^2.1.1 | +| masked-view | @react-native-masked-view/masked-view@^0.2.7 | @react-native-masked-view/masked-view@^0.2.7 | @react-native-masked-view/masked-view@^0.2.6 | @react-native-masked-view/masked-view@^0.2.6 | @react-native-masked-view/masked-view@^0.2.6 | @react-native-masked-view/masked-view@^0.2.6 | @react-native-masked-view/masked-view@^0.2.4 | @react-native-masked-view/masked-view@^0.2.4 | @react-native-masked-view/masked-view@^0.2.4 | @react-native-masked-view/masked-view@^0.2.4 | +| metro | metro@^0.72.1 | metro@^0.70.1 | metro@^0.67.0 | metro@^0.66.2 | metro@^0.66.2 | metro@^0.66.0 | metro@^0.64.0 | metro@^0.59.0 | metro@^0.58.0 | metro@^0.56.0 | +| metro-config | metro-config@^0.72.1 | metro-config@^0.70.1 | metro-config@^0.67.0 | metro-config@^0.66.2 | metro-config@^0.66.2 | metro-config@^0.66.0 | metro-config@^0.64.0 | metro-config@^0.59.0 | metro-config@^0.58.0 | metro-config@^0.56.0 | +| metro-core | metro-core@^0.72.1 | metro-core@^0.70.1 | metro-core@^0.67.0 | metro-core@^0.66.2 | metro-core@^0.66.2 | metro-core@^0.66.0 | metro-core@^0.64.0 | metro-core@^0.59.0 | metro-core@^0.58.0 | metro-core@^0.56.0 | +| metro-react-native-babel-transformer | metro-react-native-babel-transformer@^0.72.1 | metro-react-native-babel-transformer@^0.70.1 | metro-react-native-babel-transformer@^0.67.0 | metro-react-native-babel-transformer@^0.66.2 | metro-react-native-babel-transformer@^0.66.2 | metro-react-native-babel-transformer@^0.66.0 | metro-react-native-babel-transformer@^0.64.0 | metro-react-native-babel-transformer@^0.59.0 | metro-react-native-babel-transformer@^0.58.0 | metro-react-native-babel-transformer@^0.56.0 | +| metro-resolver | metro-resolver@^0.72.1 | metro-resolver@^0.70.1 | metro-resolver@^0.67.0 | metro-resolver@^0.66.2 | metro-resolver@^0.66.2 | metro-resolver@^0.66.0 | metro-resolver@^0.64.0 | metro-resolver@^0.59.0 | metro-resolver@^0.58.0 | metro-resolver@^0.56.0 | +| metro-runtime | metro-runtime@^0.72.1 | metro-runtime@^0.70.1 | metro-runtime@^0.67.0 | metro-runtime@^0.66.2 | metro-runtime@^0.66.2 | metro-runtime@^0.66.0 | metro-runtime@^0.64.0 | metro-runtime@^0.59.0 | metro-runtime@^0.58.0 | metro-runtime@^0.56.0 | +| modal | react-native-modal@^13.0.0 | react-native-modal@^13.0.0 | react-native-modal@^13.0.0 | react-native-modal@^13.0.0 | react-native-modal@^13.0.0 | react-native-modal@^13.0.0 | react-native-modal@^11.10.0 | react-native-modal@^11.5.6 | react-native-modal@^11.5.6 | react-native-modal@^11.5.6 | +| navigation/native | @react-navigation/native@^6.0.8 | @react-navigation/native@^6.0.8 | @react-navigation/native@^6.0.8 | @react-navigation/native@^6.0.8 | @react-navigation/native@^6.0.8 | @react-navigation/native@^5.9.8 | @react-navigation/native@^5.9.8 | @react-navigation/native@^5.9.4 | @react-navigation/native@^5.7.6 | @react-navigation/native@^5.7.6 | +| navigation/stack | @react-navigation/stack@^6.2.0 | @react-navigation/stack@^6.2.0 | @react-navigation/stack@^6.2.0 | @react-navigation/stack@^6.2.0 | @react-navigation/stack@^6.2.0 | @react-navigation/stack@^5.14.9 | @react-navigation/stack@^5.14.9 | @react-navigation/stack@^5.14.4 | @react-navigation/stack@^5.9.3 | @react-navigation/stack@^5.9.3 | +| netinfo | @react-native-community/netinfo@^9.0.0 | @react-native-community/netinfo@^8.0.0 | @react-native-community/netinfo@^7.0.0 | @react-native-community/netinfo@^7.0.0 | @react-native-community/netinfo@^7.0.0 | @react-native-community/netinfo@^7.0.0 | @react-native-community/netinfo@^6.0.2 | @react-native-community/netinfo@^5.9.10 | @react-native-community/netinfo@^5.9.10 | @react-native-community/netinfo@^5.7.1 | +| popover | react-native-popover-view@^5.0.0 | react-native-popover-view@^5.0.0 | react-native-popover-view@^4.0.3 | react-native-popover-view@^4.0.3 | react-native-popover-view@^4.0.3 | react-native-popover-view@^4.0.3 | react-native-popover-view@^4.0.3 | react-native-popover-view@^3.1.1 | react-native-popover-view@^3.1.1 | react-native-popover-view@^3.1.1 | +| react | react@18.1.0 | react@18.0.0 | react@17.0.2 | react@17.0.2 | react@17.0.2 | react@17.0.2 | react@17.0.1 | react@16.13.1 | react@16.11.0 | react@16.9.0 | +| react-dom | react-dom@^18.1.0 | react-dom@^18.0.0 | react-dom@17.0.2 | react-dom@17.0.2 | react-dom@17.0.2 | react-dom@17.0.2 | react-dom@17.0.1 | react-dom@16.13.1 | react-dom@16.11.0 | react-dom@16.9.0 | +| react-test-renderer | react-test-renderer@18.1.0 | react-test-renderer@18.0.0 | react-test-renderer@17.0.2 | react-test-renderer@17.0.2 | react-test-renderer@17.0.2 | react-test-renderer@17.0.2 | react-test-renderer@17.0.1 | react-test-renderer@16.13.1 | react-test-renderer@16.11.0 | react-test-renderer@16.9.0 | +| safe-area | react-native-safe-area-context@^4.4.1 | react-native-safe-area-context@^4.3.1 | react-native-safe-area-context@^3.2.0 | react-native-safe-area-context@^3.2.0 | react-native-safe-area-context@^3.2.0 | react-native-safe-area-context@^3.2.0 | react-native-safe-area-context@^3.2.0 | react-native-safe-area-context@^3.2.0 | react-native-safe-area-context@^3.1.9 | react-native-safe-area-context@^3.1.9 | +| screens | react-native-screens@^3.14.1 | react-native-screens@^3.14.1 | react-native-screens@^3.13.1 | react-native-screens@^3.9.0 | react-native-screens@^3.9.0 | react-native-screens@^3.7.0 | react-native-screens@^3.1.1 | react-native-screens@^2.18.1 | react-native-screens@^2.10.1 | react-native-screens@^2.10.1 | +| shimmer | react-native-shimmer@^0.5.0 | react-native-shimmer@^0.5.0 | react-native-shimmer@^0.5.0 | react-native-shimmer@^0.5.0 | react-native-shimmer@^0.5.0 | react-native-shimmer@^0.5.0 | react-native-shimmer@^0.5.0 | react-native-shimmer@^0.5.0 | react-native-shimmer@^0.5.0 | react-native-shimmer@^0.5.0 | +| sqlite | react-native-sqlite-storage@^6.0.1 | react-native-sqlite-storage@^6.0.1 | react-native-sqlite-storage@^5.0.0 | react-native-sqlite-storage@^5.0.0 | react-native-sqlite-storage@^5.0.0 | react-native-sqlite-storage@^5.0.0 | react-native-sqlite-storage@^5.0.0 | react-native-sqlite-storage@^3.3.11 | react-native-sqlite-storage@^3.3.11 | react-native-sqlite-storage@^3.3.11 | +| storage | @react-native-async-storage/async-storage@^1.17.10 | @react-native-async-storage/async-storage@^1.17.7 | @react-native-async-storage/async-storage@^1.17.3 | @react-native-async-storage/async-storage@^1.15.16 | @react-native-async-storage/async-storage@^1.15.9 | @react-native-async-storage/async-storage@^1.15.8 | @react-native-async-storage/async-storage@^1.15.8 | @react-native-community/async-storage@^1.12.1 | @react-native-community/async-storage@^1.12.1 | @react-native-community/async-storage@^1.12.1 | +| svg | react-native-svg@^12.3.0 | react-native-svg@^12.3.0 | react-native-svg@^12.3.0 | react-native-svg@^12.1.1 | react-native-svg@^12.1.1 | react-native-svg@^12.1.1 | react-native-svg@^12.1.1 | react-native-svg@^12.1.1 | react-native-svg@^12.1.1 | react-native-svg@^12.1.1 | +| test-app | react-native-test-app@^1.6.9 | react-native-test-app@^1.3.10 | react-native-test-app@^1.3.5 | react-native-test-app@^1.1.7 | react-native-test-app@^1.0.6 | react-native-test-app@^0.11.4 | react-native-test-app@^0.11.4 | react-native-test-app@^0.11.4 | react-native-test-app@^0.11.4 | react-native-test-app@^0.11.4 | +| webview | react-native-webview@^11.23.0 | react-native-webview@^11.23.0 | react-native-webview@^11.22.6 | react-native-webview@^11.13.0 | react-native-webview@^11.13.0 | react-native-webview@^11.13.0 | react-native-webview@^11.4.2 | react-native-webview@^11.4.2 | react-native-webview@^11.0.3 | react-native-webview@^11.0.3 | + + + +
+ +To add new capabilities, first add it to +[`/packages/config/src/kitConfig.ts`](https://github.com/microsoft/rnx-kit/blob/62da26011c9ff86a24eed63356c68f6999034d34/packages/config/src/kitConfig.ts#L4), +then update the +[profiles](https://github.com/microsoft/rnx-kit/tree/main/packages/dep-check/src/profiles). +For an example, have a look at how the +[`hermes` capability was added](https://github.com/microsoft/rnx-kit/commit/c79828791a6ac5cf19b4abfff6347542af49eaec). + +If you're looking to update capabilities to a more recent version, run +`yarn update-profile` to help determine whether we need to bump any packages. + +## Custom Profiles + +A custom profile is a list of capabilities that map to specific versions of +packages. It can be a JSON file, or a JS file that default exports it. Custom +profiles are consumed via the [`--custom-profiles`](#--custom-profiles-module) +flag. + +For example, this custom profile adds `my-capability` to profile versions 0.63 +and 0.64: + +```js +module.exports = { + 0.63: { + "my-capability": { + name: "my-module", + version: "1.0.0", + }, + }, + 0.64: { + "my-capability": { + name: "my-module", + version: "1.1.0", + }, + }, +}; +``` + +If you have capabilities that should be the same across all versions, you can +declare them at the root level like below: + +```js +module.exports = { + "my-capability": { + name: "my-module", + version: "1.0.0", + }, + 0.64: { + // This entry will override the common version + "my-capability": { + name: "my-module", + version: "1.1.0", + }, + }, +}; +``` + +For a more complete example, have a look at the +[default profiles](https://github.com/microsoft/rnx-kit/blob/769e9fa290929effd5111884f1637c21326b5a95/packages/dep-check/src/profiles.ts#L11). + +### Custom Capabilities + +Normally, a capability resolves to a version of a package. For instance, `core` +is a capability that resolves to `react-native`: + +```js +{ + "core": { + name: "react-native", + version: "0.0.0", + }, +} +``` + +A capability can depend on other capabilities. For example, we can ensure that +`react-native` gets installed along with `react-native-windows` by declaring +that `core-windows` depends on `core`: + +```js +{ + "core-windows": { + name: "react-native-windows", + version: "0.0.0", + capabilities: ["core"], + }, +} +``` + +You can also create capabilities that don't resolve to a package, but to a list +of capabilities instead: + +```js +{ + "core/all": { + name: "#meta", + capabilities: [ + "core-android", + "core-ios", + "core-macos", + "core-windows", + ], + }, +} +``` + +We call these **meta** capabilities. To make it easier to identify them (both +for humans and machines), the `name` field must be set to `#meta`, and the +`capabilities` field must be a non-empty array of other capabilities. The +`version` field is no longer used and can be dropped. To use a meta capability +in your rnx-kit configuration, there's nothing specific to be done — for +instance: + +```diff + { + "name": "my-package", + ... + "rnx-kit": { + "reactNativeVersion": "0.64", + "capabilities": [ ++ "core/all" + ], + "customProfiles": "my-custom-profiles" + } + } +``` + +## Terminology + +| Terminology | Definition (as used in `dep-check`'s context) | +| :--------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| capability | A capability is in essence a feature that the kit uses. A capability is usually mapped to an npm package. Which versions of the package is determined by a profile (see below). | +| package manifest | This normally refers to a package's `package.json`. | +| profile | A profile is a mapping of capabilities to npm packages at a specific version or version range. Versions will vary depending on which React Native version a profile is meant for. | + +## Contribution + +### Updating an Existing Profile + +Updating an existing profile is unfortunately a manual process. + +We have a script that fetches the latest version of all capabilities and +presents them in a table together with the current versions. + +```sh +yarn update-profile +``` + +Outputs something like: + +``` +| Capability | Name | Version | Latest | Homepage | +| ------------ | ------------- | --------- | ------ | ----------------------------------------------- | +| core | react-native | ^0.68.0-0 | 0.68.2 | https://github.com/facebook/react-native#readme | +| core-android | react-native | ^0.68.0-0 | 0.68.2 | https://github.com/facebook/react-native#readme | +| core-ios | react-native | ^0.68.0-0 | 0.68.2 | https://github.com/facebook/react-native#readme | +| hermes | hermes-engine | ~0.11.0 | = | | +| react | react | 17.0.2 | 18.1.0 | https://reactjs.org/ | +| ... | +``` + +With this information, we can see which packages have been updated since the +last profile, and scan their change logs for interesting changes that may affect +compatibility. + +### Adding a Profile for a New Version of `react-native` + +The `update-profile` script can also be used to add a profile. For instance, to +add a profile for `react-native` 0.69, run: + +```sh +yarn update-profile 0.69 +``` + +The script will try to figure out what version of `react`, `metro`, etc. should +be set to, and write to `src/profiles/profile-0.69.ts`. Please verify that this +profile looks correct before checking it in. diff --git a/packages/align-deps/package.json b/packages/align-deps/package.json new file mode 100644 index 000000000..74af5479a --- /dev/null +++ b/packages/align-deps/package.json @@ -0,0 +1,63 @@ +{ + "private": true, + "name": "@rnx-kit/align-deps", + "version": "1.13.1", + "description": "Dependency checker for npm packages", + "homepage": "https://github.com/microsoft/rnx-kit/tree/main/packages/align-deps#readme", + "license": "MIT", + "files": [ + "lib/**/*.d.ts", + "lib/index.js" + ], + "main": "lib/index.js", + "types": "lib/index.d.ts", + "bin": { + "rnx-align-deps": "lib/index.js" + }, + "repository": { + "type": "git", + "url": "https://github.com/microsoft/rnx-kit", + "directory": "packages/align-deps" + }, + "scripts": { + "build": "rnx-kit-scripts build", + "bundle": "rnx-kit-scripts bundle", + "format": "rnx-kit-scripts format", + "lint": "rnx-kit-scripts lint", + "test": "rnx-kit-scripts test", + "update-profile": "node scripts/update-profile.mjs", + "update-readme": "node scripts/update-readme.js" + }, + "devDependencies": { + "@rnx-kit/config": "*", + "@rnx-kit/console": "*", + "@rnx-kit/scripts": "*", + "@rnx-kit/tools-language": "*", + "@rnx-kit/tools-node": "*", + "@rnx-kit/tools-workspaces": "*", + "@types/jest": "^27.0.0", + "@types/lodash": "^4.14.172", + "@types/pacote": "^11.0.0", + "@types/prompts": "^2.0.0", + "@types/semver": "^7.0.0", + "@types/yargs": "^16.0.0", + "chalk": "^4.1.0", + "detect-indent": "^6.0.0", + "jest-diff": "^26.0.0", + "lodash": "^4.17.21", + "markdown-table": "^2.0.0", + "pacote": "^12.0.0", + "prompts": "^2.4.0", + "semver": "^7.0.0", + "yargs": "^16.0.0" + }, + "engines": { + "node": ">=12.13" + }, + "eslintConfig": { + "extends": "@rnx-kit/eslint-config" + }, + "jest": { + "preset": "@rnx-kit/scripts" + } +} diff --git a/packages/align-deps/scripts/update-profile.mjs b/packages/align-deps/scripts/update-profile.mjs new file mode 100755 index 000000000..6b5373a9e --- /dev/null +++ b/packages/align-deps/scripts/update-profile.mjs @@ -0,0 +1,399 @@ +#!/usr/bin/env node +// @ts-check + +import { existsSync as fileExists } from "fs"; +import * as fs from "fs/promises"; +import markdownTable from "markdown-table"; +import pacote from "pacote"; +import * as path from "path"; +import semverCoerce from "semver/functions/coerce.js"; +import semverCompare from "semver/functions/compare.js"; +import { fileURLToPath } from "url"; +import { isMetaPackage } from "../lib/capabilities.js"; + +/** + * @typedef {import("../src/types").MetaPackage} MetaPackage + * @typedef {import("../src/types").Package} Package + * @typedef {import("../src/types").Profile} Profile + * + * @typedef {{ + * name: string; + * version: string; + * latest: string; + * homepage: string; + * dependencies?: Record; + * peerDependencies?: Record; + * }} PackageInfo + */ + +/** + * Fetches package manifest from npm. + * @param {MetaPackage | Package} pkg + * @param {string=} defaultTag + * @returns {Promise} + */ +async function fetchPackageInfo(pkg, defaultTag = "latest") { + if (isMetaPackage(pkg)) { + return Promise.resolve(); + } + + const { name, version } = pkg; + const manifest = await pacote.manifest(name, { + defaultTag, + fullMetadata: true, + }); + return { + name, + version, + latest: manifest.version, + homepage: manifest.homepage, + dependencies: manifest.dependencies, + peerDependencies: manifest.peerDependencies, + }; +} + +/** + * @param {string} packageName + * @param {Record?} dependencies + * @returns {string} + */ +function getPackageVersion(packageName, dependencies) { + const packageVersion = dependencies?.[packageName]; + if (!packageVersion) { + throw new Error(`Failed to get '${packageName}' version`); + } + return semverCoerce(packageVersion).version; +} + +/** + * Returns the path to a profile. + * @param {string} preset + * @param {string} profileVersion + * @returns {string} + */ +function getProfilePath(preset, profileVersion) { + const __dirname = path.dirname(fileURLToPath(import.meta.url)); + return path.relative( + process.cwd(), + path.join( + __dirname, + "..", + "src", + "presets", + preset, + `profile-${profileVersion}.ts` + ) + ); +} + +/** + * Generates a profile. + * @param {{ + * preset: string; + * targetVersion: string; + * reactVersion: string; + * metroVersion: string; + * }} versions + * @returns {string} + */ +function generateFromTemplate({ + preset, + targetVersion, + reactVersion, + metroVersion, +}) { + const nextVersionCoerced = semverCoerce(targetVersion); + const currentVersion = `${nextVersionCoerced.major}.${ + nextVersionCoerced.minor - 1 + }`; + + const currentProfile = getProfilePath(preset, currentVersion); + if (!fileExists(currentProfile)) { + throw new Error(`Could not find '${currentProfile}'`); + } + + const currentVersionVarName = `${nextVersionCoerced.major}_${ + nextVersionCoerced.minor - 1 + }`; + return `import type { Profile, Package } from "../../types"; +import profile_${currentVersionVarName} from "./profile-${currentVersion}"; + +const reactNative: Package = { + name: "react-native", + version: "^${targetVersion}.0", + capabilities: ["react"], +}; + +const profile: Profile = { + ...profile_${currentVersionVarName}, + react: { + name: "react", + version: "${reactVersion}", + }, + "react-dom": { + name: "react-dom", + version: "^${reactVersion}", + capabilities: ["react"], + }, + "react-test-renderer": { + name: "react-test-renderer", + version: "${reactVersion}", + capabilities: ["react"], + devOnly: true, + }, + + core: reactNative, + "core-android": reactNative, + "core-ios": reactNative, + "core-macos": { + name: "react-native-macos", + version: "^${targetVersion}.0", + capabilities: ["react"], + }, + "core-windows": { + name: "react-native-windows", + version: "^${targetVersion}.0", + capabilities: ["core"], + }, + + "babel-preset-react-native": { + name: "metro-react-native-babel-preset", + version: "^${metroVersion}", + devOnly: true, + }, + metro: { + name: "metro", + version: "^${metroVersion}", + devOnly: true, + }, + "metro-config": { + name: "metro-config", + version: "^${metroVersion}", + devOnly: true, + }, + "metro-core": { + name: "metro-core", + version: "^${metroVersion}", + devOnly: true, + }, + "metro-react-native-babel-transformer": { + name: "metro-react-native-babel-transformer", + version: "^${metroVersion}", + devOnly: true, + }, + "metro-resolver": { + name: "metro-resolver", + version: "^${metroVersion}", + devOnly: true, + }, + "metro-runtime": { + name: "metro-runtime", + version: "^${metroVersion}", + devOnly: true, + }, +}; + +export default profile; +`; +} + +/** + * Fetches package versions for specified react-native version. + * @param {string} preset + * @param {string} targetVersion + * @param {Profile} latestProfile + * @returns {Promise} + */ +async function makeProfile(preset, targetVersion, latestProfile) { + const reactNativeInfo = await fetchPackageInfo( + latestProfile["core"], + `^${targetVersion}.0-0` + ); + if (!reactNativeInfo) { + throw new Error(`Failed to get manifest of 'react-native@${targetVersion}`); + } + + const { dependencies, peerDependencies } = reactNativeInfo; + if (!dependencies) { + throw new Error( + `Failed to get dependencies of 'react-native@${targetVersion}` + ); + } + if (!peerDependencies) { + throw new Error( + `Failed to get peer dependencies of 'react-native@${targetVersion}` + ); + } + + // Fetch `metro` version from `@react-native-community/cli-plugin-metro` > `@react-native-community/cli` + const cliMetroPluginDependencies = await [ + "@react-native-community/cli", + "@react-native-community/cli-plugin-metro", + ].reduce(async (dependencies, packageName) => { + try { + const packageInfo = await pacote.manifest(packageName, { + defaultTag: getPackageVersion(packageName, await dependencies), + fullMetadata: true, + }); + return packageInfo.dependencies; + } catch (e) { + if (e.code === "ETARGET") { + // Some packages, such as `@react-native-community/cli`, are still in + // alpha or beta while react-native RCs. Try again with the `next` tag. + const packageInfo = await pacote.manifest(packageName, { + defaultTag: "next", + fullMetadata: true, + }); + return packageInfo.dependencies; + } else { + throw e; + } + } + }, Promise.resolve(dependencies)); + + return generateFromTemplate({ + preset, + targetVersion, + reactVersion: getPackageVersion("react", peerDependencies), + metroVersion: getPackageVersion("metro", cliMetroPluginDependencies), + }); +} + +/** + * Displays a table of all capabilities that resolve to a package, its current + * version, and the latest available version. + * + * If `targetVersion` is specified, also generates a profile. + * + * Note that this script spawns a new process for each capability in parallel. + * It currently does not honor throttling hints of any kind. + * + * @param {{ preset?: string; targetVersion?: string; force?: boolean; }} options + */ +async function main({ + preset: presetName = "microsoft", + targetVersion = "", + force, +}) { + const { preset } = await import(`../lib/presets/${presetName}/index.js`); + const allVersions = /** @type {import("../src/types").ProfileVersion[]} */ ( + Object.keys(preset) + .sort((lhs, rhs) => semverCompare(semverCoerce(lhs), semverCoerce(rhs))) + .reverse() + ); + + const latestProfile = preset[allVersions[0]]; + + if (targetVersion) { + if (!force && preset[targetVersion]) { + console.error( + `Profile for '${targetVersion}' already exists. To overwrite it anyway, re-run with '--force'.` + ); + process.exit(1); + } + + try { + const newProfile = await makeProfile( + presetName, + targetVersion, + latestProfile + ); + if (newProfile) { + const dst = getProfilePath(presetName, targetVersion); + fs.writeFile(dst, newProfile).then(() => { + console.log(`Wrote to '${dst}'`); + }); + } + } catch (e) { + if (e.distTags) { + console.error( + [ + e.message, + "Available tags:", + ...Object.entries(e.distTags).map( + ([tag, version]) => ` - ${tag}: ${version}` + ), + ].join("\n") + ); + } else { + console.error(e); + } + process.exit(1); + } + } + + const ignoredCapabilities = [ + "babel-preset-react-native", + "core", + "core-android", + "core-ios", + "core-macos", + "core-windows", + "hermes", + "metro", + "metro-config", + "metro-core", + "metro-react-native-babel-transformer", + "metro-resolver", + "metro-runtime", + "react", + "react-dom", + "react-test-renderer", + ]; + + /** @type {Record} */ + const delta = {}; + await Promise.all( + Object.entries(latestProfile) + .filter(([capability]) => { + return !ignoredCapabilities.includes(capability); + }) + .map(async ([capability, pkg]) => { + await fetchPackageInfo(pkg).then((info) => { + if (info) { + delta[capability] = info; + } + }); + }) + ); + + const table = markdownTable([ + ["Capability", "Name", "Version", "Latest", "Homepage"], + ...Object.keys(delta) + .sort() + .map((capability) => { + const { name, version, latest, homepage } = delta[capability]; + return [ + capability, + name, + version, + version.endsWith(latest) ? "=" : latest, + homepage, + ]; + }), + ]); + console.log(table); +} + +const options = (() => { + const options = {}; + process.argv.slice(2).forEach((arg) => { + switch (arg) { + case "--force": + options.force = true; + break; + default: + if (!/^\d+\.\d+$/.test(arg)) { + console.error( + `Expected version in the format '.', got: ${arg}` + ); + process.exit(1); + } + options.targetVersion = arg; + break; + } + }); + return options; +})(); + +main(options); diff --git a/packages/align-deps/scripts/update-readme.js b/packages/align-deps/scripts/update-readme.js new file mode 100755 index 000000000..2afc55e3d --- /dev/null +++ b/packages/align-deps/scripts/update-readme.js @@ -0,0 +1,78 @@ +#!/usr/bin/env node +// @ts-check + +const fs = require("fs"); +const markdownTable = require("markdown-table"); +const { preset } = require("../lib/presets/microsoft"); + +const README = "README.md"; +const TOKEN_START = ""; +const TOKEN_END = ""; + +/** + * Returns whether specified capability is a core capability. + * @param capability {string} + * @returns {boolean} + */ +function isCoreCapability(capability) { + return capability === "core" || capability.startsWith("core-"); +} + +/** + * Compare function that places core capabilities first. + * @param lhs {string} + * @param rhs {string} + * @returns {number} + */ +function sortCoreFirst(lhs, rhs) { + if (isCoreCapability(lhs)) { + if (!isCoreCapability(rhs)) { + return -1; + } + } else if (isCoreCapability(rhs)) { + return 1; + } + + if (lhs === rhs) { + return 0; + } + + return lhs < rhs ? -1 : 1; +} + +const allVersions = /** @type {import("../src/types").ProfileVersion[]} */ ( + Object.keys(preset).reverse() +); +const allCapabilities = /** @type {import("@rnx-kit/config").Capability[]} */ ( + Object.keys(preset[allVersions[0]]).sort(sortCoreFirst) +); + +const table = markdownTable([ + ["Capability", ...allVersions], + ...allCapabilities.map((capability) => { + return [ + capability, + ...allVersions.map((profileVersion) => { + const pkg = preset[profileVersion][capability]; + if ("version" in pkg) { + const { name, version } = pkg; + return `${name}@${version}`; + } else { + return `Meta package for installing ${pkg.capabilities + .map((name) => `\`${name}\``) + .join(", ")}`; + } + }), + ]; + }), +]); + +const readme = fs.readFileSync(README, { encoding: "utf-8" }); +const updatedReadme = readme.replace( + new RegExp(`${TOKEN_START}([^]+)${TOKEN_END}`), + `${TOKEN_START}\n\n${table}\n\n${TOKEN_END}` +); + +if (updatedReadme !== readme) { + fs.writeFileSync(README, updatedReadme); +} diff --git a/packages/align-deps/src/capabilities.ts b/packages/align-deps/src/capabilities.ts new file mode 100644 index 000000000..e85696f4e --- /dev/null +++ b/packages/align-deps/src/capabilities.ts @@ -0,0 +1,147 @@ +import type { Capability, KitCapabilities } from "@rnx-kit/config"; +import type { PackageManifest } from "@rnx-kit/tools-node/package"; +import semverMinVersion from "semver/ranges/min-version"; +import { getProfilesFor, getProfileVersionsFor } from "./profiles"; +import { concatVersionRanges, keysOf } from "./helpers"; +import type { + CapabilitiesOptions, + MetaPackage, + Package, + Profile, +} from "./types"; + +export function capabilitiesFor( + { dependencies, devDependencies, peerDependencies }: PackageManifest, + { kitType = "library", customProfilesPath }: CapabilitiesOptions = {} +): Partial | undefined { + const targetReactNativeVersion = + peerDependencies?.["react-native"] || + dependencies?.["react-native"] || + devDependencies?.["react-native"]; + if (!targetReactNativeVersion) { + return undefined; + } + + const profiles = getProfilesFor(targetReactNativeVersion, customProfilesPath); + const packageToCapabilityMap: Record = {}; + profiles.forEach((profile) => { + keysOf(profile).reduce((result, capability) => { + const { name } = profile[capability]; + if (!result[name]) { + result[name] = [capability]; + } else { + result[name].push(capability); + } + return result; + }, packageToCapabilityMap); + }); + + const reactNativeVersion = concatVersionRanges( + getProfileVersionsFor(targetReactNativeVersion) + ); + + return { + reactNativeVersion, + ...(kitType === "library" + ? { + reactNativeDevVersion: + devDependencies?.["react-native"] || + semverMinVersion(reactNativeVersion)?.version, + } + : undefined), + kitType, + capabilities: Array.from( + keysOf({ + ...dependencies, + ...peerDependencies, + ...devDependencies, + }).reduce>((result, dependency) => { + if (dependency in packageToCapabilityMap) { + packageToCapabilityMap[dependency].forEach((capability) => { + result.add(capability); + }); + } + return result; + }, new Set()) + ).sort(), + }; +} + +export function isMetaPackage(pkg: MetaPackage | Package): pkg is MetaPackage { + return pkg.name === "#meta" && Array.isArray(pkg.capabilities); +} + +function resolveCapability( + capability: Capability, + profile: Profile, + dependencies: Record, + unresolvedCapabilities: Set, + resolved = new Set() +): void { + if (resolved.has(capability)) { + return; + } + + // Make sure we don't end in a loop + resolved.add(capability); + + const pkg = profile[capability]; + if (!pkg) { + unresolvedCapabilities.add(capability); + return; + } + + pkg.capabilities?.forEach((capability) => + resolveCapability( + capability, + profile, + dependencies, + unresolvedCapabilities, + resolved + ) + ); + + if (!isMetaPackage(pkg)) { + const { name, version } = pkg; + if (name in dependencies) { + const versions = dependencies[name]; + if (!versions.find((current) => current.version === version)) { + versions.push(pkg); + } + } else { + dependencies[name] = [pkg]; + } + } +} + +export function resolveCapabilities( + capabilities: Capability[], + profiles: Profile[] +): Record { + const unresolvedCapabilities = new Set(); + const packages = capabilities.reduce>( + (dependencies, capability) => { + profiles.forEach((profile) => { + resolveCapability( + capability, + profile, + dependencies, + unresolvedCapabilities + ); + }); + return dependencies; + }, + {} + ); + + if (unresolvedCapabilities.size > 0) { + const message = Array.from(unresolvedCapabilities).reduce( + (lines, capability) => (lines += `\n ${capability}`), + "The following capabilities could not be resolved for one or more profiles:" + ); + + console.warn(message); + } + + return packages; +} diff --git a/packages/align-deps/src/check.ts b/packages/align-deps/src/check.ts new file mode 100644 index 000000000..3bb07912c --- /dev/null +++ b/packages/align-deps/src/check.ts @@ -0,0 +1,158 @@ +import { getKitCapabilities, getKitConfig } from "@rnx-kit/config"; +import { error, info, warn } from "@rnx-kit/console"; +import { isPackageManifest, readPackage } from "@rnx-kit/tools-node/package"; +import chalk from "chalk"; +import { diffLinesUnified } from "jest-diff"; +import path from "path"; +import { getRequirements } from "./dependencies"; +import { findBadPackages } from "./findBadPackages"; +import { modifyManifest } from "./helpers"; +import { updatePackageManifest } from "./manifest"; +import { getProfilesFor, resolveCustomProfiles } from "./profiles"; +import type { CheckConfig, CheckOptions, Command } from "./types"; + +export function getCheckConfig( + manifestPath: string, + { + loose, + uncheckedReturnCode = 0, + supportedVersions, + targetVersion, + }: CheckOptions +): number | CheckConfig { + const manifest = readPackage(manifestPath); + if (!isPackageManifest(manifest)) { + error( + `'${manifestPath}' does not contain a valid package manifest - please make sure it's not missing 'name' or 'version'` + ); + return 1; + } + + const badPackages = findBadPackages(manifest); + if (badPackages) { + warn( + `Known bad packages are found in '${manifest.name}':\n` + + badPackages + .map((pkg) => ` ${pkg.name}@${pkg.version}: ${pkg.reason}`) + .join("\n") + ); + } + + const projectRoot = path.dirname(manifestPath); + const kitConfig = getKitConfig({ cwd: projectRoot }); + if (!kitConfig) { + return uncheckedReturnCode; + } + + const { + reactNativeVersion: targetReactNativeVersion, + reactNativeDevVersion, + kitType, + capabilities: targetCapabilities, + customProfiles, + } = getKitCapabilities({ + // React Native versions declared in the package's config should always + // override the ones specified with the `--vigilant` flag. + ...(supportedVersions + ? { reactNativeVersion: supportedVersions } + : undefined), + // We should not set dev version if the package is configured. It may have + // been intentionally left out to reuse `reactNativeVersion`. + ...(!kitConfig.reactNativeVersion && targetVersion + ? { reactNativeDevVersion: targetVersion } + : undefined), + ...kitConfig, + }); + + const customProfilesPath = resolveCustomProfiles(projectRoot, customProfiles); + + const { reactNativeVersion, capabilities: requiredCapabilities } = + getRequirements( + targetReactNativeVersion, + kitType, + manifest, + projectRoot, + customProfilesPath, + { loose } + ); + requiredCapabilities.push(...targetCapabilities); + + return { + kitType, + reactNativeVersion, + reactNativeDevVersion, + capabilities: requiredCapabilities, + customProfilesPath, + manifest, + }; +} + +export function checkPackageManifest( + manifestPath: string, + options: CheckOptions +): number { + const result = options.config || getCheckConfig(manifestPath, options); + if (typeof result === "number") { + return result; + } + + const { + kitType, + reactNativeVersion, + reactNativeDevVersion, + capabilities, + customProfilesPath, + manifest, + } = result; + + if (capabilities.length === 0) { + return options.uncheckedReturnCode || 0; + } + + const updatedManifest = updatePackageManifest( + manifest, + capabilities, + getProfilesFor(reactNativeVersion, customProfilesPath), + getProfilesFor(reactNativeDevVersion, customProfilesPath), + kitType + ); + + // Don't fail when manifests only have whitespace differences. + const updatedManifestJson = JSON.stringify(updatedManifest, undefined, 2); + const normalizedManifestJson = JSON.stringify(manifest, undefined, 2); + + if (updatedManifestJson !== normalizedManifestJson) { + if (options.write) { + modifyManifest(manifestPath, updatedManifest); + } else { + const diff = diffLinesUnified( + normalizedManifestJson.split("\n"), + updatedManifestJson.split("\n"), + { + aAnnotation: "Current", + aColor: chalk.red, + bAnnotation: "Expected", + bColor: chalk.green, + } + ); + console.log(diff); + + error( + "Changes are needed to satisfy all requirements. Re-run with `--write` to have dep-check apply them." + ); + + const url = chalk.bold("https://aka.ms/dep-check"); + info(`Visit ${url} for more information about dep-check.`); + + return 1; + } + } + + return 0; +} + +export function makeCheckCommand(options: CheckOptions): Command { + return (manifest: string) => { + return checkPackageManifest(manifest, options); + }; +} diff --git a/packages/align-deps/src/cli.ts b/packages/align-deps/src/cli.ts new file mode 100644 index 000000000..c4241fe09 --- /dev/null +++ b/packages/align-deps/src/cli.ts @@ -0,0 +1,242 @@ +#!/usr/bin/env node + +import type { KitType } from "@rnx-kit/config"; +import { error, warn } from "@rnx-kit/console"; +import { hasProperty } from "@rnx-kit/tools-language/properties"; +import { findPackageDir } from "@rnx-kit/tools-node/package"; +import { + findWorkspacePackages, + findWorkspaceRoot, +} from "@rnx-kit/tools-workspaces"; +import isString from "lodash/isString"; +import * as path from "path"; +import { makeCheckCommand } from "./check"; +import { initializeConfig } from "./initialize"; +import { resolveCustomProfiles } from "./profiles"; +import { makeSetVersionCommand } from "./setVersion"; +import type { Args, Command } from "./types"; +import { makeVigilantCommand } from "./vigilant"; + +function ensureKitType(type: string): KitType | undefined { + switch (type) { + case "app": + case "library": + return type; + default: + return undefined; + } +} + +async function getManifests( + packages: (string | number)[] | undefined +): Promise { + if (Array.isArray(packages)) { + return packages.reduce((result, pkg) => { + const dir = findPackageDir(pkg.toString()); + if (dir) { + result.push(path.join(dir, "package.json")); + } + return result; + }, []); + } + + const packageDir = findPackageDir(); + if (!packageDir) { + return undefined; + } + + // Make sure we don't return all packages when dep-check is run inside a + // package that just happened to be part of a workspace. + const currentPackageJson = path.join(packageDir, "package.json"); + try { + if ((await findWorkspaceRoot()) !== packageDir) { + return [currentPackageJson]; + } + } catch (_) { + return [currentPackageJson]; + } + + try { + const allPackages = (await findWorkspacePackages()).map((p) => + path.join(p, "package.json") + ); + allPackages.push(currentPackageJson); + return allPackages; + } catch (e) { + if (hasProperty(e, "message")) { + error(e.message); + return undefined; + } + + throw e; + } +} + +function makeInitializeCommand( + kitType: string, + customProfiles: string | undefined +): Command | undefined { + const verifiedKitType = ensureKitType(kitType); + if (!verifiedKitType) { + error(`Invalid kit type: '${kitType}'`); + return undefined; + } + + return (manifest: string) => { + initializeConfig(manifest, { + kitType: verifiedKitType, + customProfilesPath: customProfiles, + }); + return 0; + }; +} + +function reportConflicts(conflicts: [string, string][], args: Args): boolean { + return conflicts.reduce((result, [lhs, rhs]) => { + if (lhs in args && rhs in args) { + error(`--${lhs} and --${rhs} cannot both be specified at the same time.`); + return true; + } + return result; + }, false); +} + +async function makeCommand(args: Args): Promise { + const conflicts: [string, string][] = [ + ["init", "vigilant"], + ["init", args.write ? "write" : "no-write"], + ["set-version", args.write ? "write" : "no-write"], + ]; + if (reportConflicts(conflicts, args)) { + return undefined; + } + + const { + "custom-profiles": customProfiles, + "exclude-packages": excludePackages, + init, + loose, + "set-version": setVersion, + vigilant, + write, + } = args; + + if (isString(init)) { + return makeInitializeCommand(init, customProfiles?.toString()); + } + + // When `--set-version` is without a value, `setVersion` is an empty string if + // invoked directly. When invoked via `@react-native-community/cli`, + // `setVersion` is `true` instead. + if (setVersion || isString(setVersion)) { + return makeSetVersionCommand(setVersion); + } + + if (isString(vigilant)) { + const customProfilesPath = resolveCustomProfiles( + process.cwd(), + customProfiles?.toString() + ); + return makeVigilantCommand({ + customProfiles: customProfilesPath, + excludePackages: excludePackages?.toString(), + loose, + versions: vigilant.toString(), + write, + }); + } + + return makeCheckCommand({ loose, write }); +} + +export async function cli({ packages, ...args }: Args): Promise { + const command = await makeCommand(args); + if (!command) { + process.exit(1); + } + + const manifests = await getManifests(packages); + if (!manifests) { + error("Could not find package root"); + process.exit(1); + } + + // We will optimistically run through all packages regardless of failures. In + // most scenarios, this should be fine: Both init and check+write write to + // disk only when everything is in order for the target package. Packages with + // invalid or missing configurations are skipped. + const exitCode = manifests.reduce((exitCode: number, manifest: string) => { + try { + return command(manifest) || exitCode; + } catch (e) { + if (hasProperty(e, "message")) { + const currentPackageJson = path.relative(process.cwd(), manifest); + + if (hasProperty(e, "code") && e.code === "ENOENT") { + warn(`${currentPackageJson}: ${e.message}`); + return exitCode; + } + + error(`${currentPackageJson}: ${e.message}`); + return exitCode || 1; + } + + throw e; + } + }, 0); + + process.exit(exitCode); +} + +if (require.main === module) { + require("yargs").usage( + "$0 [packages...]", + "Dependency checker for React Native apps", + { + "custom-profiles": { + description: + "Path to custom profiles. This can be a path to a JSON file, a `.js` file, or a module name.", + type: "string", + requiresArg: true, + }, + "exclude-packages": { + description: + "Comma-separated list of package names to exclude from inspection.", + type: "string", + requiresArg: true, + implies: "vigilant", + }, + init: { + description: + "Writes an initial kit config to the specified 'package.json'.", + choices: ["app", "library"], + conflicts: ["vigilant"], + }, + loose: { + default: false, + description: + "Determines how strict the React Native version requirement should be. Useful for apps that depend on a newer React Native version than their dependencies declare support for.", + type: "boolean", + }, + "set-version": { + description: + "Sets `reactNativeVersion` and `reactNativeDevVersion` for any configured package. There is an interactive prompt if no value is provided. The value should be a comma-separated list of `react-native` versions to set, where the first number specifies the development version. Example: `0.64,0.63`", + type: "string", + conflicts: ["init", "vigilant"], + }, + vigilant: { + description: + "Inspects packages regardless of whether they've been configured. Specify a comma-separated list of profile versions to compare against, e.g. `0.63,0.64`. The first number specifies the target version.", + type: "string", + requiresArg: true, + conflicts: ["init"], + }, + write: { + default: false, + description: "Writes changes to the specified 'package.json'.", + type: "boolean", + }, + }, + cli + ).argv; +} diff --git a/packages/align-deps/src/dependencies.ts b/packages/align-deps/src/dependencies.ts new file mode 100644 index 000000000..b9b444034 --- /dev/null +++ b/packages/align-deps/src/dependencies.ts @@ -0,0 +1,167 @@ +import type { Capability, KitConfig, KitType } from "@rnx-kit/config"; +import { getKitCapabilities, getKitConfig } from "@rnx-kit/config"; +import { error, warn } from "@rnx-kit/console"; +import type { PackageManifest } from "@rnx-kit/tools-node/package"; +import { + findPackageDependencyDir, + readPackage, +} from "@rnx-kit/tools-node/package"; +import { concatVersionRanges } from "./helpers"; +import { + getProfilesFor, + getProfileVersionsFor, + profilesSatisfying, +} from "./profiles"; +import type { CheckOptions, Profile, ProfileVersion } from "./types"; + +type Requirements = Required< + Pick +>; + +type Trace = { + module: string; + reactNativeVersion: string; + profiles: ProfileVersion[]; +}; + +function isCoreCapability(capability: Capability): boolean { + return capability.startsWith("core-"); +} + +function isDevOnlyCapability( + capability: Capability, + profiles: Partial[] +): boolean { + return profiles.some((profile) => profile[capability]?.devOnly); +} + +export function visitDependencies( + { dependencies }: PackageManifest, + projectRoot: string, + visitor: (module: string, modulePath: string) => void, + visited: Set = new Set() +): void { + if (!dependencies) { + return; + } + + Object.keys(dependencies).forEach((dependency) => { + if (visited.has(dependency)) { + return; + } + + visited.add(dependency); + + const packageDir = findPackageDependencyDir(dependency, { + startDir: projectRoot, + resolveSymlinks: true, + }); + if (!packageDir) { + warn(`Unable to resolve module '${dependency}'`); + return; + } + + visitor(dependency, packageDir); + + const manifest = readPackage(packageDir); + visitDependencies(manifest, packageDir, visitor, visited); + }); +} + +export function getRequirements( + targetReactNativeVersion: string, + kitType: KitType, + targetManifest: PackageManifest, + projectRoot: string, + customProfiles: string | undefined, + { loose }: Pick +): Requirements { + let profileVersions = getProfileVersionsFor(targetReactNativeVersion); + if (profileVersions.length === 0) { + throw new Error( + `No profile could satisfy React Native version: ${targetReactNativeVersion}` + ); + } + + const allCapabilities = new Set(); + if (kitType === "app") { + const trace: Trace[] = [ + { + module: targetManifest.name, + reactNativeVersion: targetReactNativeVersion, + profiles: profileVersions, + }, + ]; + + visitDependencies(targetManifest, projectRoot, (module, modulePath) => { + const kitConfig = getKitConfig({ cwd: modulePath }); + if (!kitConfig?.reactNativeVersion) { + return; + } + + const { reactNativeVersion, capabilities } = + getKitCapabilities(kitConfig); + + const validVersions = profilesSatisfying( + profileVersions, + reactNativeVersion + ); + if (validVersions.length != profileVersions.length) { + trace.push({ + module, + reactNativeVersion, + profiles: validVersions, + }); + } + + if (Array.isArray(capabilities)) { + capabilities.forEach((capability) => allCapabilities.add(capability)); + } + + // In strict mode, we want to continue so we can catch all dependencies + // that cannot be satisfied. Whereas in loose mode, we can ignore them and + // carry on with the profiles that satisfy the rest. + if (validVersions.length > 0) { + profileVersions = validVersions; + } + }); + + if (trace[trace.length - 1].profiles.length === 0) { + const message = "No React Native profile could satisfy all dependencies"; + const fullTrace = [ + message, + ...trace.map(({ module, reactNativeVersion, profiles }) => { + const satisfiedVersions = profiles.join(", "); + return ` [${satisfiedVersions}] satisfies '${module}' because it supports '${reactNativeVersion}'`; + }), + ].join("\n"); + if (loose) { + warn(fullTrace); + } else { + error(fullTrace); + throw new Error(message); + } + } + + const profiles = getProfilesFor(profileVersions, customProfiles); + allCapabilities.forEach((capability) => { + /** + * Core capabilities are capabilities that must always be declared by the + * hosting app and should not be included when gathering requirements. + * This is to avoid forcing an app to install dependencies it does not + * need, e.g. `react-native-windows` when the app only supports iOS. + */ + if ( + isCoreCapability(capability) || + isDevOnlyCapability(capability, profiles) + ) { + allCapabilities.delete(capability); + } + }); + } + + return { + reactNativeVersion: concatVersionRanges(profileVersions), + capabilities: Array.from(allCapabilities), + }; +} diff --git a/packages/align-deps/src/findBadPackages.ts b/packages/align-deps/src/findBadPackages.ts new file mode 100644 index 000000000..3ee94a087 --- /dev/null +++ b/packages/align-deps/src/findBadPackages.ts @@ -0,0 +1,33 @@ +import type { PackageManifest } from "@rnx-kit/tools-node/package"; +import semverSubset from "semver/ranges/subset"; +import banned from "./presets/banned"; +import type { ExcludedPackage } from "./types"; + +function isBanned(name: string, version: string): ExcludedPackage | undefined { + const info = banned.find((pkg) => pkg.name === name); + return info && semverSubset(version, info.version) ? info : undefined; +} + +export function findBadPackages({ + dependencies, + peerDependencies, + devDependencies, +}: PackageManifest): ExcludedPackage[] | undefined { + const foundBadPackages = [ + dependencies, + peerDependencies, + devDependencies, + ].reduce>((badPackages, deps) => { + if (deps) { + Object.keys(deps).reduce((result, name) => { + const info = isBanned(name, deps[name]); + if (info) { + result.add(info); + } + return result; + }, badPackages); + } + return badPackages; + }, new Set()); + return foundBadPackages.size > 0 ? Array.from(foundBadPackages) : undefined; +} diff --git a/packages/align-deps/src/helpers.ts b/packages/align-deps/src/helpers.ts new file mode 100644 index 000000000..8c4e58a2d --- /dev/null +++ b/packages/align-deps/src/helpers.ts @@ -0,0 +1,42 @@ +import type { PackageManifest } from "@rnx-kit/tools-node/package"; +import { writePackage } from "@rnx-kit/tools-node/package"; +import detectIndent from "detect-indent"; +import fs from "fs"; + +export function compare(lhs: T, rhs: T): -1 | 0 | 1 { + if (lhs === rhs) { + return 0; + } else if (lhs < rhs) { + return -1; + } else { + return 1; + } +} + +export function concatVersionRanges(versions: string[]): string { + return "^" + versions.join(" || ^"); +} + +export function keysOf>(obj: T): (keyof T)[] { + return Object.keys(obj); +} + +export function modifyManifest( + pkgPath: string, + manifest: PackageManifest +): void { + const content = fs.readFileSync(pkgPath, { encoding: "utf-8" }); + const indent = detectIndent(content).indent || " "; + writePackage(pkgPath, manifest, indent); +} + +export function omitEmptySections(manifest: PackageManifest): PackageManifest { + const sections = ["dependencies", "peerDependencies", "devDependencies"]; + for (const sectionName of sections) { + const section = manifest[sectionName]; + if (typeof section === "object" && Object.keys(section).length === 0) { + delete manifest[sectionName]; + } + } + return manifest; +} diff --git a/packages/align-deps/src/index.ts b/packages/align-deps/src/index.ts new file mode 100644 index 000000000..20b152fbf --- /dev/null +++ b/packages/align-deps/src/index.ts @@ -0,0 +1,20 @@ +export { capabilitiesFor } from "./capabilities"; +export { checkPackageManifest } from "./check"; +export { cli } from "./cli"; +export { updatePackageManifest } from "./manifest"; + +export type { + Args, + CapabilitiesOptions, + CheckOptions, + Command, + DependencyType, + ExcludedPackage, + ManifestProfile, + MetaPackage, + Package, + Profile, + ProfilesInfo, + ProfileVersion, + VigilantOptions, +} from "./types"; diff --git a/packages/align-deps/src/initialize.ts b/packages/align-deps/src/initialize.ts new file mode 100644 index 000000000..384a032bf --- /dev/null +++ b/packages/align-deps/src/initialize.ts @@ -0,0 +1,31 @@ +import { readPackage } from "@rnx-kit/tools-node/package"; +import { capabilitiesFor } from "./capabilities"; +import { modifyManifest } from "./helpers"; +import type { CapabilitiesOptions } from "./types"; + +export function initializeConfig( + packageManifest: string, + options: CapabilitiesOptions +): void { + const manifest = readPackage(packageManifest); + if (manifest["rnx-kit"]?.["capabilities"]) { + return; + } + + const capabilities = capabilitiesFor(manifest, options); + if (!capabilities?.capabilities?.length) { + return; + } + + const updatedManifest = { + ...manifest, + "rnx-kit": { + ...manifest["rnx-kit"], + ...capabilities, + ...(options.customProfilesPath + ? { customProfiles: options.customProfilesPath } + : undefined), + }, + }; + modifyManifest(packageManifest, updatedManifest); +} diff --git a/packages/align-deps/src/manifest.ts b/packages/align-deps/src/manifest.ts new file mode 100644 index 000000000..9ffee8533 --- /dev/null +++ b/packages/align-deps/src/manifest.ts @@ -0,0 +1,131 @@ +import type { Capability, KitType } from "@rnx-kit/config"; +import type { PackageManifest } from "@rnx-kit/tools-node/package"; +import omit from "lodash/omit"; +import { resolveCapabilities } from "./capabilities"; +import { compare, omitEmptySections } from "./helpers"; +import type { DependencyType, Package, Profile } from "./types"; + +export function devOnlyPackages( + packages: Record +): Record { + return Object.keys(packages).reduce>( + (result, name) => { + const versions = packages[name]; + if (versions.some((pkg) => Boolean(pkg.devOnly))) { + result[name] = versions; + } + return result; + }, + {} + ); +} + +export function removeKeys( + obj: Record | undefined, + keys: string[] +): Record | undefined { + if (!obj) { + return obj; + } + + return omit(obj, ...keys); +} + +export function updateDependencies( + dependencies: Record = {}, + packages: Record, + dependencyType: DependencyType +): Record { + const packageNames = Object.keys(packages); + if (packageNames.length === 0) { + return dependencies; + } + + const makeVersionRange = (() => { + switch (dependencyType) { + case "direct": + return (versions: Package[]) => versions[versions.length - 1].version; + case "development": + return (versions: Package[]) => versions[0].version; + case "peer": + return (versions: Package[]) => + versions.map((pkg) => pkg.version).join(" || "); + } + })(); + const shouldBeAdded = (pkg: Package) => + !pkg.devOnly || dependencyType === "development"; + + const entries = packageNames.reduce((result, dependency) => { + const packageRange = packages[dependency]; + if (shouldBeAdded(packageRange[0])) { + result.push([dependency, makeVersionRange(packageRange)]); + } + return result; + }, Object.entries(dependencies)); + return Object.fromEntries(entries.sort(([a], [b]) => compare(a, b))); +} + +/** + * Updates the specified package manifest so that it will satisfy all declared + * capabilities, using the specified set of profiles. + * + * When a kit is of type "library", it expects the consumer to be providing all + * the capabilites. This function will make sure that `peerDependencies` is + * correctly populated, and because `peerDependencies` don't get downloaded (at + * least with some package managers), it will also make sure that appropriate + * changes are made to `devDependencies`. To avoid version conflicts with the + * hosting app, and with other libraries, the packages that were added, will be + * removed from `dependencies` if found. + * + * This function behaves similarly when a kit is of type "app". But because it + * is now a provider of capabilities, it needs to have a direct dependency on + * packages. For the "app" type, packages are instead added to `dependencies`, + * and removed from `peerDependencies` and `devDependencies`. + * + * @param manifest The package manifest that should be updated + * @param capabilities The set of capabilities that the kit requires + * @param profiles The set of profiles that the kit needs to conform to + * @param devProfiles The set of profiles that the kit will develop against + * @param packageType Whether the kit provides a feature or is an app + * @returns A package manifest that satisfies specified capabilities + */ +export function updatePackageManifest( + manifest: PackageManifest, + capabilities: Capability[], + profiles: Profile[], + devProfiles: Profile[], + packageType: KitType +): PackageManifest { + const { dependencies, peerDependencies, devDependencies } = manifest; + const packages = resolveCapabilities(capabilities, profiles); + const names = Object.keys(packages); + + switch (packageType) { + case "app": + return omitEmptySections({ + ...manifest, + dependencies: updateDependencies(dependencies, packages, "direct"), + peerDependencies: removeKeys(peerDependencies, names), + devDependencies: updateDependencies( + removeKeys(devDependencies, names), + devOnlyPackages(packages), + "development" + ), + }); + case "library": + return omitEmptySections({ + ...manifest, + dependencies: removeKeys(dependencies, names), + peerDependencies: updateDependencies( + peerDependencies, + packages, + "peer" + ), + devDependencies: updateDependencies( + devDependencies, + resolveCapabilities(capabilities, devProfiles), + "development" + ), + }); + } +} diff --git a/packages/align-deps/src/presets/banned.ts b/packages/align-deps/src/presets/banned.ts new file mode 100644 index 000000000..8089c1309 --- /dev/null +++ b/packages/align-deps/src/presets/banned.ts @@ -0,0 +1,42 @@ +import type { ExcludedPackage } from "../types"; + +const bannedPackages: ExcludedPackage[] = [ + { + name: "@react-native-community/async-storage", + version: "*", + reason: + "This package was renamed to '@react-native-async-storage/async-storage' in 1.13.0. The new package will be recommended in 0.64.", + }, + { + name: "@react-native-community/masked-view", + version: "*", + reason: + "This package was renamed to '@react-native-masked-view/masked-view' in 0.2.0. Please remove the old package and start using the new one.", + }, + { + name: "@types/react-native", + version: ">=0.71.0-0", + reason: + "Types are included in react-native starting with 0.71.0. '@types/react-native' will be deprecated from 0.72 onwards.", + }, + { + name: "react-native-linear-gradient", + version: "<2.6.0", + reason: + "This package causes significant degradation in app start up time prior to 2.6.0.", + }, + { + name: "react-native-lazy-index", + version: "*", + reason: + "This package was renamed to '@rnx-kit/react-native-lazy-index'. The new package will be recommended in 0.66.", + }, + { + name: "react-native-netinfo", + version: "*", + reason: + "This is an old and unmaintained fork of @react-native-netinfo/netinfo.", + }, +]; + +export default bannedPackages; diff --git a/packages/align-deps/src/presets/microsoft/baseCapabilities.ts b/packages/align-deps/src/presets/microsoft/baseCapabilities.ts new file mode 100644 index 000000000..e66094d11 --- /dev/null +++ b/packages/align-deps/src/presets/microsoft/baseCapabilities.ts @@ -0,0 +1,11 @@ +import type { MetaCapability } from "@rnx-kit/config"; +import type { MetaPackage } from "../../types"; + +const baseCapabilities: Readonly> = { + "core/testing": { + name: "#meta", + capabilities: ["core", "jest", "react-test-renderer"], + }, +}; + +export default baseCapabilities; diff --git a/packages/align-deps/src/presets/microsoft/index.ts b/packages/align-deps/src/presets/microsoft/index.ts new file mode 100644 index 000000000..f9a659a57 --- /dev/null +++ b/packages/align-deps/src/presets/microsoft/index.ts @@ -0,0 +1,29 @@ +import type { ProfileMap } from "../../types"; +import profile_0_61 from "./profile-0.61"; +import profile_0_62 from "./profile-0.62"; +import profile_0_63 from "./profile-0.63"; +import profile_0_64 from "./profile-0.64"; +import profile_0_65 from "./profile-0.65"; +import profile_0_66 from "./profile-0.66"; +import profile_0_67 from "./profile-0.67"; +import profile_0_68 from "./profile-0.68"; +import profile_0_69 from "./profile-0.69"; +import profile_0_70 from "./profile-0.70"; + +// Also export this by name for scripts to work around a bug where this module +// is wrapped twice, i.e. `{ default: { default: preset } }`, when imported as +// ESM. +export const preset: Readonly = { + "0.61": profile_0_61, + "0.62": profile_0_62, + "0.63": profile_0_63, + "0.64": profile_0_64, + "0.65": profile_0_65, + "0.66": profile_0_66, + "0.67": profile_0_67, + "0.68": profile_0_68, + "0.69": profile_0_69, + "0.70": profile_0_70, +}; + +export default preset; diff --git a/packages/align-deps/src/presets/microsoft/profile-0.61.ts b/packages/align-deps/src/presets/microsoft/profile-0.61.ts new file mode 100644 index 000000000..537fbad55 --- /dev/null +++ b/packages/align-deps/src/presets/microsoft/profile-0.61.ts @@ -0,0 +1,190 @@ +import type { Package, Profile } from "../../types"; +import baseCapabilities from "./baseCapabilities"; + +const reactNative: Package = { + name: "react-native", + version: "^0.61.5", + capabilities: ["react"], +}; + +const profile: Profile = { + ...baseCapabilities, + react: { + name: "react", + version: "16.9.0", + }, + "react-dom": { + name: "react-dom", + version: "16.9.0", + capabilities: ["react"], + }, + "react-test-renderer": { + name: "react-test-renderer", + version: "16.9.0", + capabilities: ["react"], + devOnly: true, + }, + + core: reactNative, + "core-android": reactNative, + "core-ios": reactNative, + "core-macos": { + name: "react-native-macos", + version: "^0.61.0", + capabilities: ["react"], + }, + "core-windows": { + name: "react-native-windows", + version: "^0.61.0", + capabilities: ["core"], + }, + + animation: { + name: "react-native-reanimated", + version: "^1.13.3", + }, + "babel-preset-react-native": { + name: "metro-react-native-babel-preset", + version: "^0.56.0", + devOnly: true, + }, + base64: { + name: "react-native-base64", + version: "^0.2.1", + }, + checkbox: { + name: "@react-native-community/checkbox", + version: "^0.5.7", + }, + clipboard: { + name: "@react-native-community/clipboard", + version: "^1.5.1", + }, + "datetime-picker": { + name: "@react-native-community/datetimepicker", + version: "^3.0.9", + }, + filesystem: { + name: "react-native-fs", + version: "^2.16.6", + }, + "floating-action": { + name: "react-native-floating-action", + version: "^1.18.0", + }, + gestures: { + name: "react-native-gesture-handler", + version: "^1.9.0", + }, + hermes: { + name: "hermes-engine", + version: "^0.2.1", + }, + hooks: { + name: "@react-native-community/hooks", + version: "^2.6.0", + }, + html: { + name: "react-native-render-html", + version: "^5.1.0", + }, + jest: { + name: "jest", + version: "^24.8.0", + devOnly: true, + }, + "lazy-index": { + name: "react-native-lazy-index", + version: "^2.1.1", + }, + "masked-view": { + name: "@react-native-masked-view/masked-view", + version: "^0.2.4", + }, + metro: { + name: "metro", + version: "^0.56.0", + devOnly: true, + }, + "metro-config": { + name: "metro-config", + version: "^0.56.0", + devOnly: true, + }, + "metro-core": { + name: "metro-core", + version: "^0.56.0", + devOnly: true, + }, + "metro-react-native-babel-transformer": { + name: "metro-react-native-babel-transformer", + version: "^0.56.0", + devOnly: true, + }, + "metro-resolver": { + name: "metro-resolver", + version: "^0.56.0", + devOnly: true, + }, + "metro-runtime": { + name: "metro-runtime", + version: "^0.56.0", + devOnly: true, + }, + modal: { + name: "react-native-modal", + version: "^11.5.6", + }, + "navigation/native": { + name: "@react-navigation/native", + version: "^5.7.6", + }, + "navigation/stack": { + name: "@react-navigation/stack", + version: "^5.9.3", + capabilities: ["navigation/native"], + }, + netinfo: { + name: "@react-native-community/netinfo", + version: "^5.7.1", + }, + popover: { + name: "react-native-popover-view", + version: "^3.1.1", + }, + "safe-area": { + name: "react-native-safe-area-context", + version: "^3.1.9", + }, + screens: { + name: "react-native-screens", + version: "^2.10.1", + }, + shimmer: { + name: "react-native-shimmer", + version: "^0.5.0", + }, + sqlite: { + name: "react-native-sqlite-storage", + version: "^3.3.11", + }, + storage: { + name: "@react-native-community/async-storage", + version: "^1.12.1", + }, + svg: { + name: "react-native-svg", + version: "^12.1.1", + }, + "test-app": { + name: "react-native-test-app", + version: "^0.11.4", + devOnly: true, + }, + webview: { + name: "react-native-webview", + version: "^11.0.3", + }, +}; + +export default profile; diff --git a/packages/align-deps/src/presets/microsoft/profile-0.62.ts b/packages/align-deps/src/presets/microsoft/profile-0.62.ts new file mode 100644 index 000000000..ff874092d --- /dev/null +++ b/packages/align-deps/src/presets/microsoft/profile-0.62.ts @@ -0,0 +1,92 @@ +import type { Package, Profile } from "../../types"; +import profile61 from "./profile-0.61"; + +const reactNative: Package = { + name: "react-native", + version: "^0.62.3", + capabilities: ["react"], +}; + +const profile: Profile = { + ...profile61, + react: { + name: "react", + version: "16.11.0", + }, + "react-dom": { + name: "react-dom", + version: "16.11.0", + capabilities: ["react"], + }, + "react-test-renderer": { + name: "react-test-renderer", + version: "16.11.0", + capabilities: ["react"], + devOnly: true, + }, + + core: reactNative, + "core-android": reactNative, + "core-ios": reactNative, + "core-macos": { + name: "react-native-macos", + version: "^0.62.0", + capabilities: ["react"], + }, + "core-windows": { + name: "react-native-windows", + version: "^0.62.0", + capabilities: ["core"], + }, + + "babel-preset-react-native": { + name: "metro-react-native-babel-preset", + version: "^0.58.0", + devOnly: true, + }, + hermes: { + name: "hermes-engine", + version: "~0.4.0", + }, + jest: { + name: "jest", + version: "^24.8.0", + devOnly: true, + }, + netinfo: { + name: "@react-native-community/netinfo", + version: "^5.9.10", + }, + metro: { + name: "metro", + version: "^0.58.0", + devOnly: true, + }, + "metro-config": { + name: "metro-config", + version: "^0.58.0", + devOnly: true, + }, + "metro-core": { + name: "metro-core", + version: "^0.58.0", + devOnly: true, + }, + "metro-react-native-babel-transformer": { + name: "metro-react-native-babel-transformer", + version: "^0.58.0", + devOnly: true, + }, + "metro-resolver": { + name: "metro-resolver", + version: "^0.58.0", + devOnly: true, + }, + "metro-runtime": { + name: "metro-runtime", + version: "^0.58.0", + devOnly: true, + }, +}; + +export default profile; diff --git a/packages/align-deps/src/presets/microsoft/profile-0.63.ts b/packages/align-deps/src/presets/microsoft/profile-0.63.ts new file mode 100644 index 000000000..ec2d121b7 --- /dev/null +++ b/packages/align-deps/src/presets/microsoft/profile-0.63.ts @@ -0,0 +1,118 @@ +import type { Package, Profile } from "../../types"; +import profile62 from "./profile-0.62"; + +const reactNative: Package = { + name: "react-native", + version: "^0.63.2", + capabilities: ["react"], +}; + +const profile: Profile = { + ...profile62, + + react: { + name: "react", + version: "16.13.1", + }, + "react-dom": { + name: "react-dom", + version: "16.13.1", + capabilities: ["react"], + }, + "react-test-renderer": { + name: "react-test-renderer", + version: "16.13.1", + capabilities: ["react"], + devOnly: true, + }, + + core: reactNative, + "core-android": reactNative, + "core-ios": reactNative, + "core-macos": { + name: "react-native-macos", + version: "^0.63.0", + capabilities: ["react"], + }, + "core-windows": { + name: "react-native-windows", + version: "^0.63.0", + capabilities: ["core"], + }, + + "babel-preset-react-native": { + name: "metro-react-native-babel-preset", + version: "^0.59.0", + devOnly: true, + }, + "floating-action": { + name: "react-native-floating-action", + version: "^1.21.0", + }, + gestures: { + name: "react-native-gesture-handler", + version: "^1.10.3", + }, + hermes: { + name: "hermes-engine", + version: "~0.5.0", + }, + jest: { + name: "jest", + version: "^24.9.0", + devOnly: true, + }, + metro: { + name: "metro", + version: "^0.59.0", + devOnly: true, + }, + "metro-config": { + name: "metro-config", + version: "^0.59.0", + devOnly: true, + }, + "metro-core": { + name: "metro-core", + version: "^0.59.0", + devOnly: true, + }, + "metro-react-native-babel-transformer": { + name: "metro-react-native-babel-transformer", + version: "^0.59.0", + devOnly: true, + }, + "metro-resolver": { + name: "metro-resolver", + version: "^0.59.0", + devOnly: true, + }, + "metro-runtime": { + name: "metro-runtime", + version: "^0.59.0", + devOnly: true, + }, + "navigation/native": { + name: "@react-navigation/native", + version: "^5.9.4", + }, + "navigation/stack": { + name: "@react-navigation/stack", + version: "^5.14.4", + capabilities: ["navigation/native"], + }, + "safe-area": { + name: "react-native-safe-area-context", + version: "^3.2.0", + }, + screens: { + name: "react-native-screens", + version: "^2.18.1", + }, + webview: { + name: "react-native-webview", + version: "^11.4.2", + }, +}; + +export default profile; diff --git a/packages/align-deps/src/presets/microsoft/profile-0.64.ts b/packages/align-deps/src/presets/microsoft/profile-0.64.ts new file mode 100644 index 000000000..f2c752c52 --- /dev/null +++ b/packages/align-deps/src/presets/microsoft/profile-0.64.ts @@ -0,0 +1,185 @@ +import type { Package, Profile } from "../../types"; +import profile63 from "./profile-0.63"; + +const reactNative: Package = { + name: "react-native", + version: "^0.64.2", + capabilities: ["react"], +}; + +const profile: Profile = { + ...profile63, + react: { + name: "react", + version: "17.0.1", + }, + "react-dom": { + name: "react-dom", + version: "17.0.1", + capabilities: ["react"], + }, + "react-test-renderer": { + name: "react-test-renderer", + version: "17.0.1", + capabilities: ["react"], + devOnly: true, + }, + + core: reactNative, + "core-android": reactNative, + "core-ios": reactNative, + "core-macos": { + name: "react-native-macos", + version: "^0.64.0", + capabilities: ["react"], + }, + "core-windows": { + name: "react-native-windows", + version: "^0.64.0", + capabilities: ["core"], + }, + + animation: { + name: "react-native-reanimated", + version: "^2.1.0", + }, + "babel-preset-react-native": { + name: "metro-react-native-babel-preset", + version: "^0.64.0", + devOnly: true, + }, + base64: { + name: "react-native-base64", + version: "^0.2.1", + }, + checkbox: { + name: "@react-native-community/checkbox", + version: "^0.5.8", + }, + clipboard: { + name: "@react-native-clipboard/clipboard", + version: "^1.8.3", + }, + "datetime-picker": { + name: "@react-native-community/datetimepicker", + version: "^3.4.6", + }, + filesystem: { + name: "react-native-fs", + version: "^2.17.0", + }, + "floating-action": { + name: "react-native-floating-action", + version: "^1.21.0", + }, + gestures: { + name: "react-native-gesture-handler", + version: "^1.10.3", + }, + hermes: { + name: "hermes-engine", + version: "~0.7.0", + }, + hooks: { + name: "@react-native-community/hooks", + version: "^2.6.0", + }, + html: { + name: "react-native-render-html", + version: "^5.1.1", + }, + jest: { + name: "jest", + version: "^26.5.2", + devOnly: true, + }, + "lazy-index": { + name: "react-native-lazy-index", + version: "^2.1.1", + }, + "masked-view": { + name: "@react-native-masked-view/masked-view", + version: "^0.2.4", + }, + metro: { + name: "metro", + version: "^0.64.0", + devOnly: true, + }, + "metro-config": { + name: "metro-config", + version: "^0.64.0", + devOnly: true, + }, + "metro-core": { + name: "metro-core", + version: "^0.64.0", + devOnly: true, + }, + "metro-react-native-babel-transformer": { + name: "metro-react-native-babel-transformer", + version: "^0.64.0", + devOnly: true, + }, + "metro-resolver": { + name: "metro-resolver", + version: "^0.64.0", + devOnly: true, + }, + "metro-runtime": { + name: "metro-runtime", + version: "^0.64.0", + devOnly: true, + }, + modal: { + name: "react-native-modal", + version: "^11.10.0", + }, + "navigation/native": { + name: "@react-navigation/native", + version: "^5.9.8", + }, + "navigation/stack": { + name: "@react-navigation/stack", + version: "^5.14.9", + capabilities: ["navigation/native"], + }, + netinfo: { + name: "@react-native-community/netinfo", + version: "^6.0.2", + }, + popover: { + name: "react-native-popover-view", + version: "^4.0.3", + }, + "safe-area": { + name: "react-native-safe-area-context", + version: "^3.2.0", + }, + screens: { + name: "react-native-screens", + version: "^3.1.1", + }, + shimmer: { + name: "react-native-shimmer", + version: "^0.5.0", + }, + sqlite: { + name: "react-native-sqlite-storage", + version: "^5.0.0", + }, + storage: { + name: "@react-native-async-storage/async-storage", + version: "^1.15.8", + }, + svg: { + name: "react-native-svg", + version: "^12.1.1", + }, + webview: { + name: "react-native-webview", + version: "^11.4.2", + }, +}; + +export default profile; diff --git a/packages/align-deps/src/presets/microsoft/profile-0.65.ts b/packages/align-deps/src/presets/microsoft/profile-0.65.ts new file mode 100644 index 000000000..520994f0f --- /dev/null +++ b/packages/align-deps/src/presets/microsoft/profile-0.65.ts @@ -0,0 +1,132 @@ +import type { Package, Profile } from "../../types"; +import profile_0_64 from "./profile-0.64"; + +const reactNative: Package = { + name: "react-native", + version: "^0.65.0", + capabilities: ["react"], +}; + +const profile: Profile = { + ...profile_0_64, + react: { + name: "react", + version: "17.0.2", + }, + "react-dom": { + name: "react-dom", + version: "17.0.2", + capabilities: ["react"], + }, + "react-test-renderer": { + name: "react-test-renderer", + version: "17.0.2", + capabilities: ["react"], + devOnly: true, + }, + + core: reactNative, + "core-android": reactNative, + "core-ios": reactNative, + "core-macos": { + name: "react-native-macos", + version: "^0.65.0", + capabilities: ["react"], + }, + "core-windows": { + name: "react-native-windows", + version: "^0.65.0", + capabilities: ["core"], + }, + + animation: { + name: "react-native-reanimated", + version: "^2.2.1", + }, + "babel-preset-react-native": { + name: "metro-react-native-babel-preset", + version: "^0.66.0", + devOnly: true, + }, + clipboard: { + name: "@react-native-clipboard/clipboard", + version: "^1.9.0", + }, + "datetime-picker": { + name: "@react-native-community/datetimepicker", + version: "^3.5.2", + }, + filesystem: { + name: "react-native-fs", + version: "^2.18.0", + }, + "floating-action": { + name: "react-native-floating-action", + version: "^1.22.0", + }, + hermes: { + name: "hermes-engine", + version: "~0.8.1", + }, + hooks: { + name: "@react-native-community/hooks", + version: "^2.8.0", + }, + jest: { + name: "jest", + version: "^26.6.3", + devOnly: true, + }, + "masked-view": { + name: "@react-native-masked-view/masked-view", + version: "^0.2.6", + }, + metro: { + name: "metro", + version: "^0.66.0", + devOnly: true, + }, + "metro-config": { + name: "metro-config", + version: "^0.66.0", + devOnly: true, + }, + "metro-core": { + name: "metro-core", + version: "^0.66.0", + devOnly: true, + }, + "metro-react-native-babel-transformer": { + name: "metro-react-native-babel-transformer", + version: "^0.66.0", + devOnly: true, + }, + "metro-resolver": { + name: "metro-resolver", + version: "^0.66.0", + devOnly: true, + }, + "metro-runtime": { + name: "metro-runtime", + version: "^0.66.0", + devOnly: true, + }, + modal: { + name: "react-native-modal", + version: "^13.0.0", + }, + netinfo: { + name: "@react-native-community/netinfo", + version: "^7.0.0", + }, + screens: { + name: "react-native-screens", + version: "^3.7.0", + }, + webview: { + name: "react-native-webview", + version: "^11.13.0", + }, +}; + +export default profile; diff --git a/packages/align-deps/src/presets/microsoft/profile-0.66.ts b/packages/align-deps/src/presets/microsoft/profile-0.66.ts new file mode 100644 index 000000000..4274c11a0 --- /dev/null +++ b/packages/align-deps/src/presets/microsoft/profile-0.66.ts @@ -0,0 +1,122 @@ +import type { Package, Profile } from "../../types"; +import profile_0_65 from "./profile-0.65"; + +const reactNative: Package = { + name: "react-native", + version: "^0.66.0", + capabilities: ["react"], +}; + +const profile: Profile = { + ...profile_0_65, + react: { + name: "react", + version: "17.0.2", + }, + "react-dom": { + name: "react-dom", + version: "17.0.2", + capabilities: ["react"], + }, + "react-test-renderer": { + name: "react-test-renderer", + version: "17.0.2", + capabilities: ["react"], + devOnly: true, + }, + + core: reactNative, + "core-android": reactNative, + "core-ios": reactNative, + "core-macos": { + name: "react-native-macos", + version: "^0.66.0", + capabilities: ["react"], + }, + "core-windows": { + name: "react-native-windows", + version: "^0.66.0", + capabilities: ["core"], + }, + + animation: { + name: "react-native-reanimated", + version: "^2.2.3", + }, + hermes: { + name: "hermes-engine", + version: "~0.9.0", + }, + "babel-preset-react-native": { + name: "metro-react-native-babel-preset", + version: "^0.66.2", + devOnly: true, + }, + html: { + name: "react-native-render-html", + version: "^6.1.0", + }, + jest: { + name: "jest", + version: "^26.6.3", + devOnly: true, + }, + "lazy-index": { + name: "@rnx-kit/react-native-lazy-index", + version: "^2.1.7", + }, + metro: { + name: "metro", + version: "^0.66.2", + devOnly: true, + }, + "metro-config": { + name: "metro-config", + version: "^0.66.2", + devOnly: true, + }, + "metro-core": { + name: "metro-core", + version: "^0.66.2", + devOnly: true, + }, + "metro-react-native-babel-transformer": { + name: "metro-react-native-babel-transformer", + version: "^0.66.2", + devOnly: true, + }, + "metro-resolver": { + name: "metro-resolver", + version: "^0.66.2", + devOnly: true, + }, + "metro-runtime": { + name: "metro-runtime", + version: "^0.66.2", + devOnly: true, + }, + "navigation/native": { + name: "@react-navigation/native", + version: "^6.0.8", + }, + "navigation/stack": { + name: "@react-navigation/stack", + version: "^6.2.0", + capabilities: ["navigation/native"], + }, + screens: { + name: "react-native-screens", + version: "^3.9.0", + }, + storage: { + name: "@react-native-async-storage/async-storage", + version: "^1.15.9", + }, + "test-app": { + name: "react-native-test-app", + version: "^1.0.6", + devOnly: true, + }, +}; + +export default profile; diff --git a/packages/align-deps/src/presets/microsoft/profile-0.67.ts b/packages/align-deps/src/presets/microsoft/profile-0.67.ts new file mode 100644 index 000000000..6a0bae3d5 --- /dev/null +++ b/packages/align-deps/src/presets/microsoft/profile-0.67.ts @@ -0,0 +1,57 @@ +import type { Package, Profile } from "../../types"; +import profile_0_66 from "./profile-0.66"; + +const reactNative: Package = { + name: "react-native", + version: "^0.67.0", + capabilities: ["react"], +}; + +const profile: Profile = { + ...profile_0_66, + react: { + name: "react", + version: "17.0.2", + }, + "react-dom": { + name: "react-dom", + version: "17.0.2", + capabilities: ["react"], + }, + "react-test-renderer": { + name: "react-test-renderer", + version: "17.0.2", + capabilities: ["react"], + devOnly: true, + }, + + core: reactNative, + "core-android": reactNative, + "core-ios": reactNative, + "core-macos": { + name: "react-native-macos", + version: "^0.67.0", + capabilities: ["react"], + }, + "core-windows": { + name: "react-native-windows", + version: "^0.67.0", + capabilities: ["core"], + }, + + animation: { + name: "react-native-reanimated", + version: "^2.2.4", + }, + storage: { + name: "@react-native-async-storage/async-storage", + version: "^1.15.16", + }, + "test-app": { + name: "react-native-test-app", + version: "^1.1.7", + devOnly: true, + }, +}; + +export default profile; diff --git a/packages/align-deps/src/presets/microsoft/profile-0.68.ts b/packages/align-deps/src/presets/microsoft/profile-0.68.ts new file mode 100644 index 000000000..bbb5980f1 --- /dev/null +++ b/packages/align-deps/src/presets/microsoft/profile-0.68.ts @@ -0,0 +1,120 @@ +import type { Package, Profile } from "../../types"; +import profile_0_67 from "./profile-0.67"; + +const reactNative: Package = { + name: "react-native", + version: "^0.68.0", + capabilities: ["react"], +}; + +const profile: Profile = { + ...profile_0_67, + react: { + name: "react", + version: "17.0.2", + }, + "react-dom": { + name: "react-dom", + version: "17.0.2", + capabilities: ["react"], + }, + "react-test-renderer": { + name: "react-test-renderer", + version: "17.0.2", + capabilities: ["react"], + devOnly: true, + }, + + core: reactNative, + "core-android": reactNative, + "core-ios": reactNative, + "core-macos": { + name: "react-native-macos", + version: "^0.68.0", + capabilities: ["react"], + }, + "core-windows": { + name: "react-native-windows", + version: "^0.68.0", + capabilities: ["core"], + }, + + animation: { + name: "react-native-reanimated", + version: "^2.5.0", + }, + "babel-preset-react-native": { + name: "metro-react-native-babel-preset", + version: "^0.67.0", + devOnly: true, + }, + clipboard: { + name: "@react-native-clipboard/clipboard", + version: "^1.10.0", + }, + "datetime-picker": { + name: "@react-native-community/datetimepicker", + version: "^6.0.2", + }, + gestures: { + name: "react-native-gesture-handler", + version: "^2.3.2", + }, + hermes: { + name: "hermes-engine", + version: "~0.11.0", + }, + metro: { + name: "metro", + version: "^0.67.0", + devOnly: true, + }, + "metro-config": { + name: "metro-config", + version: "^0.67.0", + devOnly: true, + }, + "metro-core": { + name: "metro-core", + version: "^0.67.0", + devOnly: true, + }, + "metro-react-native-babel-transformer": { + name: "metro-react-native-babel-transformer", + version: "^0.67.0", + devOnly: true, + }, + "metro-resolver": { + name: "metro-resolver", + version: "^0.67.0", + devOnly: true, + }, + "metro-runtime": { + name: "metro-runtime", + version: "^0.67.0", + devOnly: true, + }, + screens: { + name: "react-native-screens", + version: "^3.13.1", + }, + storage: { + name: "@react-native-async-storage/async-storage", + version: "^1.17.3", + }, + svg: { + name: "react-native-svg", + version: "^12.3.0", + }, + "test-app": { + name: "react-native-test-app", + version: "^1.3.5", + devOnly: true, + }, + webview: { + name: "react-native-webview", + version: "^11.22.6", + }, +}; + +export default profile; diff --git a/packages/align-deps/src/presets/microsoft/profile-0.69.ts b/packages/align-deps/src/presets/microsoft/profile-0.69.ts new file mode 100644 index 000000000..b506612c7 --- /dev/null +++ b/packages/align-deps/src/presets/microsoft/profile-0.69.ts @@ -0,0 +1,123 @@ +import type { Profile, Package } from "../../types"; +import profile_0_68 from "./profile-0.68"; + +const reactNative: Package = { + name: "react-native", + version: "^0.69.0", + capabilities: ["react"], +}; + +const profile: Profile = { + ...profile_0_68, + react: { + name: "react", + version: "18.0.0", + }, + "react-dom": { + name: "react-dom", + version: "^18.0.0", + capabilities: ["react"], + }, + "react-test-renderer": { + name: "react-test-renderer", + version: "18.0.0", + capabilities: ["react"], + devOnly: true, + }, + core: reactNative, + "core-android": reactNative, + "core-ios": reactNative, + "core-macos": { + name: "react-native-macos", + version: "^0.69.0", + capabilities: ["react"], + }, + "core-windows": { + name: "react-native-windows", + version: "^0.69.0", + capabilities: ["core"], + }, + + animation: { + name: "react-native-reanimated", + version: "^2.9.0", + }, + "babel-preset-react-native": { + name: "metro-react-native-babel-preset", + version: "^0.70.3", + devOnly: true, + }, + gestures: { + name: "react-native-gesture-handler", + version: "^2.5.0", + }, + "masked-view": { + name: "@react-native-masked-view/masked-view", + version: "^0.2.7", + }, + metro: { + name: "metro", + version: "^0.70.1", + devOnly: true, + }, + "metro-config": { + name: "metro-config", + version: "^0.70.1", + devOnly: true, + }, + "metro-core": { + name: "metro-core", + version: "^0.70.1", + devOnly: true, + }, + "metro-react-native-babel-transformer": { + name: "metro-react-native-babel-transformer", + version: "^0.70.1", + devOnly: true, + }, + "metro-resolver": { + name: "metro-resolver", + version: "^0.70.1", + devOnly: true, + }, + "metro-runtime": { + name: "metro-runtime", + version: "^0.70.1", + devOnly: true, + }, + netinfo: { + name: "@react-native-community/netinfo", + version: "^8.0.0", + }, + popover: { + name: "react-native-popover-view", + version: "^5.0.0", + }, + "safe-area": { + name: "react-native-safe-area-context", + version: "^4.3.1", + }, + screens: { + name: "react-native-screens", + version: "^3.14.1", + }, + sqlite: { + name: "react-native-sqlite-storage", + version: "^6.0.1", + }, + storage: { + name: "@react-native-async-storage/async-storage", + version: "^1.17.7", + }, + "test-app": { + name: "react-native-test-app", + version: "^1.3.10", + devOnly: true, + }, + webview: { + name: "react-native-webview", + version: "^11.23.0", + }, +}; + +export default profile; diff --git a/packages/align-deps/src/presets/microsoft/profile-0.70.ts b/packages/align-deps/src/presets/microsoft/profile-0.70.ts new file mode 100644 index 000000000..8d479389b --- /dev/null +++ b/packages/align-deps/src/presets/microsoft/profile-0.70.ts @@ -0,0 +1,104 @@ +import type { Profile, Package } from "../../types"; +import profile_0_69 from "./profile-0.69"; + +const reactNative: Package = { + name: "react-native", + version: "^0.70.0", + capabilities: ["react"], +}; + +const profile: Profile = { + ...profile_0_69, + react: { + name: "react", + version: "18.1.0", + }, + "react-dom": { + name: "react-dom", + version: "^18.1.0", + capabilities: ["react"], + }, + "react-test-renderer": { + name: "react-test-renderer", + version: "18.1.0", + capabilities: ["react"], + devOnly: true, + }, + + core: reactNative, + "core-android": reactNative, + "core-ios": reactNative, + "core-macos": { + name: "react-native-macos", + version: "^0.70.0", + capabilities: ["react"], + }, + "core-windows": { + name: "react-native-windows", + version: "^0.70.0", + capabilities: ["core"], + }, + + animation: { + name: "react-native-reanimated", + version: "^2.10.0", + }, + "babel-preset-react-native": { + name: "metro-react-native-babel-preset", + version: "^0.72.1", + devOnly: true, + }, + gestures: { + name: "react-native-gesture-handler", + version: "^2.6.0", + }, + metro: { + name: "metro", + version: "^0.72.1", + devOnly: true, + }, + "metro-config": { + name: "metro-config", + version: "^0.72.1", + devOnly: true, + }, + "metro-core": { + name: "metro-core", + version: "^0.72.1", + devOnly: true, + }, + "metro-react-native-babel-transformer": { + name: "metro-react-native-babel-transformer", + version: "^0.72.1", + devOnly: true, + }, + "metro-resolver": { + name: "metro-resolver", + version: "^0.72.1", + devOnly: true, + }, + "metro-runtime": { + name: "metro-runtime", + version: "^0.72.1", + devOnly: true, + }, + netinfo: { + name: "@react-native-community/netinfo", + version: "^9.0.0", + }, + "safe-area": { + name: "react-native-safe-area-context", + version: "^4.4.1", + }, + storage: { + name: "@react-native-async-storage/async-storage", + version: "^1.17.10", + }, + "test-app": { + name: "react-native-test-app", + version: "^1.6.9", + devOnly: true, + }, +}; + +export default profile; diff --git a/packages/align-deps/src/profiles.ts b/packages/align-deps/src/profiles.ts new file mode 100644 index 000000000..7e6ab3822 --- /dev/null +++ b/packages/align-deps/src/profiles.ts @@ -0,0 +1,194 @@ +import { error } from "@rnx-kit/console"; +import isString from "lodash/isString"; +import semverCoerce from "semver/functions/coerce"; +import semverSatisfies from "semver/functions/satisfies"; +import semverValid from "semver/functions/valid"; +import semverIntersects from "semver/ranges/intersects"; +import semverValidRange from "semver/ranges/valid"; +import { keysOf } from "./helpers"; +import { default as defaultPreset } from "./presets/microsoft"; +import type { + MetaPackage, + Package, + Profile, + ProfileMap, + ProfilesInfo, + ProfileVersion, +} from "./types"; + +type Capabilities = Record; + +function getVersionComparator( + versionOrRange: string +): (profileVersion: ProfileVersion) => boolean { + const includePrerelease = { includePrerelease: true }; + + const version = semverValid(versionOrRange); + if (version) { + return (profileVersion: ProfileVersion) => + semverSatisfies(version, "^" + profileVersion, includePrerelease); + } + + const range = semverValidRange(versionOrRange); + if (range) { + return (profileVersion: ProfileVersion) => + semverIntersects("^" + profileVersion, range, includePrerelease); + } + + throw new Error(`Invalid 'react-native' version range: ${versionOrRange}`); +} + +function isValidProfileMap( + map: unknown +): map is Partial & Capabilities { + // Just make sure we've got a dictionary since custom profiles can contain + // only common dependencies. + return typeof map === "object" && map !== null && map.constructor === Object; +} + +function isValidProfileVersion(v: string): v is ProfileVersion { + return v in defaultPreset; +} + +export function loadCustomProfiles( + customProfilesPath: string | undefined +): Partial { + if (customProfilesPath) { + const customProfiles: unknown = require(customProfilesPath); + if (!isValidProfileMap(customProfiles)) { + const message = `'${customProfilesPath}' doesn't default export profiles`; + error( + [ + `${message}. Please make sure that it exports an object with a shape similar to:`, + "", + " module.exports = {", + ' "0.67": {', + ' "my-capability": {', + ' "name": "my-module",', + ' "version": "1.0.0",', + " },", + " },", + " };", + "", + ].join("\n") + ); + throw new Error(message); + } + + // Root-level capabilities should be prepended to all profiles to allow + // version-specific capabilities to override them. + const commonCapabilities: Capabilities = {}; + const hasCommonCapabilities = Object.keys(customProfiles).reduce( + (hasCommonCapabilities, key) => { + if (isValidProfileVersion(key)) { + return hasCommonCapabilities; + } + + commonCapabilities[key] = customProfiles[key]; + return true; + }, + false + ); + if (hasCommonCapabilities) { + const allVersions = Object.keys(defaultPreset) as ProfileVersion[]; + return allVersions.reduce< + Record> + >((expandedProfiles, version) => { + // Check whether property exists otherwise Node will complain: + // Accessing non-existent property '0.67' of module exports inside circular dependency + const profile = + version in customProfiles ? customProfiles[version] : undefined; + expandedProfiles[version] = profile + ? { + ...commonCapabilities, + ...profile, + } + : commonCapabilities; + return expandedProfiles; + }, {}); + } + + return customProfiles; + } + + return {}; +} + +export function getProfileVersionsFor( + reactVersionRange: string | ProfileVersion[] +): ProfileVersion[] { + if (!isString(reactVersionRange)) { + return reactVersionRange; + } + + const isSatifisedBy = getVersionComparator(reactVersionRange); + const allVersions = keysOf(defaultPreset); + return allVersions.reduce((profiles, version) => { + if (isSatifisedBy(version)) { + profiles.push(version); + } + return profiles; + }, []); +} + +export function getProfilesFor( + reactVersionRange: string | ProfileVersion[], + customProfilesPath: string | undefined +): Profile[] { + const customProfiles = loadCustomProfiles(customProfilesPath); + const profiles = getProfileVersionsFor(reactVersionRange).map((version) => ({ + ...defaultPreset[version], + ...customProfiles[version], + })); + if (profiles.length === 0) { + throw new Error( + `Unsupported 'react-native' version/range: ${reactVersionRange}` + ); + } + + return profiles; +} + +export function parseProfilesString( + versions: string | number, + customProfilesPath?: string | number +): ProfilesInfo { + const profileVersions = versions + .toString() + .split(",") + .map((value) => "^" + semverCoerce(value)); + const targetVersion = profileVersions[0]; + + // Note: `.sort()` mutates the array + const supportedVersions = profileVersions.sort().join(" || "); + + return { + supportedProfiles: getProfilesFor( + supportedVersions, + customProfilesPath?.toString() + ), + supportedVersions, + targetProfile: getProfilesFor( + targetVersion, + customProfilesPath?.toString() + ), + targetVersion, + }; +} + +export function profilesSatisfying( + profiles: ProfileVersion[], + versionOrRange: string +): ProfileVersion[] { + const versions = getProfileVersionsFor(versionOrRange); + return profiles.filter((v) => versions.includes(v)); +} + +export function resolveCustomProfiles( + projectRoot: string, + profilesPath: string | undefined +): string | undefined { + return profilesPath + ? require.resolve(profilesPath, { paths: [projectRoot] }) + : undefined; +} diff --git a/packages/align-deps/src/setVersion.ts b/packages/align-deps/src/setVersion.ts new file mode 100644 index 000000000..5cd8fde17 --- /dev/null +++ b/packages/align-deps/src/setVersion.ts @@ -0,0 +1,90 @@ +import { readPackage } from "@rnx-kit/tools-node/package"; +import isString from "lodash/isString"; +import prompts from "prompts"; +import { checkPackageManifest } from "./check"; +import { concatVersionRanges, keysOf, modifyManifest } from "./helpers"; +import { default as defaultPreset } from "./presets/microsoft"; +import { parseProfilesString } from "./profiles"; +import type { Command, ProfileVersion } from "./types"; + +function profileToChoice(version: ProfileVersion): prompts.Choice { + return { title: version, value: version }; +} + +async function parseInput(versions: string | number): Promise<{ + supportedVersions?: string; + targetVersion?: string; +}> { + // When `--set-version` is without a value, `versions` is an empty string if + // invoked directly. When invoked via `@react-native-community/cli`, + // `versions` is `true` instead. + if (isString(versions) && versions) { + return parseProfilesString(versions); + } + + const { supportedVersions } = await prompts({ + type: "multiselect", + name: "supportedVersions", + message: "Select all supported versions of `react-native`", + choices: keysOf(defaultPreset).map(profileToChoice), + min: 1, + }); + if (!Array.isArray(supportedVersions)) { + return {}; + } + + const targetVersion = + supportedVersions.length === 1 + ? supportedVersions[0] + : ( + await prompts({ + type: "select", + name: "targetVersion", + message: "Select development version of `react-native`", + choices: supportedVersions.map(profileToChoice), + }) + ).targetVersion; + if (!supportedVersions.includes(targetVersion)) { + return {}; + } + + return { + supportedVersions: concatVersionRanges(supportedVersions), + targetVersion, + }; +} + +export async function makeSetVersionCommand( + versions: string | number +): Promise { + const { supportedVersions, targetVersion } = await parseInput(versions); + if (!supportedVersions) { + return undefined; + } + + const checkOnly = { loose: false, write: false }; + const write = { loose: false, write: true }; + + return (manifestPath: string) => { + const checkReturnCode = checkPackageManifest(manifestPath, checkOnly); + if (checkReturnCode !== 0) { + return checkReturnCode; + } + + const manifest = readPackage(manifestPath); + const rnxKitConfig = manifest["rnx-kit"]; + if (!rnxKitConfig) { + return 0; + } + + rnxKitConfig.reactNativeVersion = supportedVersions; + if (rnxKitConfig.kitType === "app") { + delete rnxKitConfig.reactNativeDevVersion; + } else { + rnxKitConfig.reactNativeDevVersion = targetVersion; + } + + modifyManifest(manifestPath, manifest); + return checkPackageManifest(manifestPath, write); + }; +} diff --git a/packages/align-deps/src/types.ts b/packages/align-deps/src/types.ts new file mode 100644 index 000000000..4b655fb6c --- /dev/null +++ b/packages/align-deps/src/types.ts @@ -0,0 +1,93 @@ +import type { Capability, KitType } from "@rnx-kit/config"; +import type { PackageManifest } from "@rnx-kit/tools-node/package"; + +type Options = { + customProfiles?: string; + excludePackages?: string; + loose: boolean; + write: boolean; +}; + +export type Args = Options & { + "custom-profiles"?: string | number; + "exclude-packages"?: string | number; + "set-version"?: string | number; + init?: string; + packages?: (string | number)[]; + vigilant?: string | number; +}; + +export type CheckConfig = { + kitType: KitType; + reactNativeVersion: string; + reactNativeDevVersion: string; + capabilities: Capability[]; + customProfilesPath?: string; + manifest: PackageManifest; +}; + +export type CheckOptions = Options & { + uncheckedReturnCode?: number; + config?: number | CheckConfig; + supportedVersions?: string; + targetVersion?: string; +}; + +export type VigilantOptions = Options & { + versions: string; +}; + +export type CapabilitiesOptions = { + kitType?: KitType; + customProfilesPath?: string; +}; + +export type Command = (manifest: string) => number; + +export type DependencyType = "direct" | "development" | "peer"; + +export type MetaPackage = { + name: "#meta"; + capabilities: Capability[]; + devOnly?: boolean; +}; + +export type Package = { + name: string; + version: string; + capabilities?: Capability[]; + devOnly?: boolean; +}; + +export type ManifestProfile = PackageManifest & { + dependencies: Record; + peerDependencies: Record; + devDependencies: Record; +}; + +export type Profile = Readonly>; + +export type ProfileVersion = + | "0.61" + | "0.62" + | "0.63" + | "0.64" + | "0.65" + | "0.66" + | "0.67" + | "0.68" + | "0.69" + | "0.70"; + +export type ProfileMap = Record; + +export type ProfilesInfo = { + supportedProfiles: Profile[]; + supportedVersions: string; + targetProfile: Profile[]; + targetVersion: string; +}; + +export type ExcludedPackage = Package & { + reason: string; +}; diff --git a/packages/align-deps/src/vigilant.ts b/packages/align-deps/src/vigilant.ts new file mode 100644 index 000000000..0e4a31799 --- /dev/null +++ b/packages/align-deps/src/vigilant.ts @@ -0,0 +1,198 @@ +import { Capability } from "@rnx-kit/config"; +import { error } from "@rnx-kit/console"; +import { PackageManifest, readPackage } from "@rnx-kit/tools-node/package"; +import isString from "lodash/isString"; +import semverSubset from "semver/ranges/subset"; +import { resolveCapabilities } from "./capabilities"; +import { checkPackageManifest, getCheckConfig } from "./check"; +import { keysOf, modifyManifest } from "./helpers"; +import { updateDependencies } from "./manifest"; +import { parseProfilesString } from "./profiles"; +import type { + Command, + ManifestProfile, + ProfilesInfo, + VigilantOptions, +} from "./types"; + +type Change = { + name: string; + from: string; + to: string; + section: string; +}; + +const allSections = [ + "dependencies" as const, + "peerDependencies" as const, + "devDependencies" as const, +]; + +export function removeManagedDependencies( + dependencies: Record, + managed: string[] +): Record { + managed.forEach((name) => delete dependencies[name]); + return dependencies; +} + +/** + * Builds a profile targeting specified versions. + * @param profilesInfo Resolved target and supported profile versions + * @param managedCapabilities Capabilities that are already managed and can be skipped + * @returns A profile containing dependencies to compare against + */ +export function buildManifestProfile( + { supportedProfiles, targetProfile }: ProfilesInfo, + managedCapabilities: Capability[] = [] +): ManifestProfile { + const allCapabilities = keysOf(targetProfile[0]); + const managedDependencies = Object.keys( + resolveCapabilities(managedCapabilities, targetProfile) + ); + + // Use "development" type so we can check for devOnly packages under + // `dependencies` as well. + const directDependencies = removeManagedDependencies( + updateDependencies( + {}, + resolveCapabilities(allCapabilities, targetProfile), + "development" + ), + managedDependencies + ); + + const { name, version } = require("../package.json"); + return { + name, + version, + dependencies: directDependencies, + peerDependencies: removeManagedDependencies( + updateDependencies( + {}, + resolveCapabilities(allCapabilities, supportedProfiles), + "peer" + ), + managedDependencies + ), + devDependencies: directDependencies, + }; +} + +export function buildProfileFromConfig( + config: ReturnType, + defaultProfile: ManifestProfile +): ManifestProfile { + if (typeof config === "number") { + return defaultProfile; + } + + const { + capabilities, + customProfilesPath, + reactNativeDevVersion, + reactNativeVersion, + } = config; + + const supportedVersions = reactNativeVersion.replace(/ [|]{2} /g, ","); + const versions = `${reactNativeDevVersion},${supportedVersions}`; + const profilesInfo = parseProfilesString(versions, customProfilesPath); + + return buildManifestProfile(profilesInfo, capabilities); +} + +export function inspect( + manifest: PackageManifest, + profile: ManifestProfile, + write: boolean +): Change[] { + const isMisalignedDirect = (from: string, to: string) => from !== to; + const isMisalignedPeer = (from: string, to: string) => + from !== to && !semverSubset(to, from, { includePrerelease: true }); + + const changes: Change[] = []; + allSections.forEach((section) => { + const dependencies = manifest[section]; + if (!dependencies) { + return; + } + + const isMisaligned = + section === "peerDependencies" ? isMisalignedPeer : isMisalignedDirect; + const desiredDependencies = profile[section]; + Object.keys(dependencies).forEach((name) => { + if (name in desiredDependencies) { + const from = dependencies[name]; + const to = desiredDependencies[name]; + if (isMisaligned(from, to)) { + changes.push({ name, from, to, section }); + if (write) { + dependencies[name] = to; + } + } + } + }); + }); + return changes; +} + +export function makeVigilantCommand({ + customProfiles, + excludePackages, + loose, + versions, + write, +}: VigilantOptions): Command | undefined { + if (!versions) { + error("A comma-separated list of profile versions must be specified."); + return undefined; + } + + const profilesInfo = parseProfilesString(versions, customProfiles); + + const checkOptions = { + loose, + write, + supportedVersions: profilesInfo.supportedVersions, + targetVersion: profilesInfo.targetVersion, + }; + + const exclusionList = isString(excludePackages) + ? excludePackages.split(",") + : []; + + const inputProfile = buildManifestProfile(profilesInfo); + return (manifestPath: string) => { + const config = getCheckConfig(manifestPath, checkOptions); + try { + checkPackageManifest(manifestPath, { ...checkOptions, config }); + } catch (_) { + // Ignore; retry with a full inspection + } + + const manifest = readPackage(manifestPath); + if (exclusionList.includes(manifest.name)) { + return 0; + } + + const currentProfile = buildProfileFromConfig(config, inputProfile); + const changes = inspect(manifest, currentProfile, write); + if (changes.length > 0) { + if (write) { + modifyManifest(manifestPath, manifest); + } else { + const violations = changes + .map( + ({ name, from, to, section }) => + ` ${name} "${from}" -> "${to}" (${section})` + ) + .join("\n"); + error( + `Found ${changes.length} violation(s) in ${manifest.name}:\n${violations}` + ); + return 1; + } + } + return 0; + }; +} diff --git a/packages/align-deps/test/__fixtures__/awesome-repo-extended/node_modules/conan/package.json b/packages/align-deps/test/__fixtures__/awesome-repo-extended/node_modules/conan/package.json new file mode 100644 index 000000000..88bf75cc7 --- /dev/null +++ b/packages/align-deps/test/__fixtures__/awesome-repo-extended/node_modules/conan/package.json @@ -0,0 +1,14 @@ +{ + "name": "conan", + "version": "1.0.0", + "dependencies": { + "t-800": "1.0.0" + }, + "rnx-kit": { + "reactNativeVersion": "^0.63 || ^0.64", + "capabilities": [ + "core-android", + "core-ios" + ] + } +} diff --git a/packages/align-deps/test/__fixtures__/awesome-repo-extended/node_modules/dutch/package.json b/packages/align-deps/test/__fixtures__/awesome-repo-extended/node_modules/dutch/package.json new file mode 100644 index 000000000..3ff6ec40e --- /dev/null +++ b/packages/align-deps/test/__fixtures__/awesome-repo-extended/node_modules/dutch/package.json @@ -0,0 +1,16 @@ +{ + "name": "dutch", + "version": "1.0.0", + "peerDependencies": { + "react": "17.0.1", + "react-native": "0.64.0" + }, + "rnx-kit": { + "reactNativeVersion": "^0.63 || ^0.64", + "capabilities": [ + "core-android", + "core-ios", + "netinfo" + ] + } +} diff --git a/packages/align-deps/test/__fixtures__/awesome-repo-extended/node_modules/john/package.json b/packages/align-deps/test/__fixtures__/awesome-repo-extended/node_modules/john/package.json new file mode 100644 index 000000000..2560a9602 --- /dev/null +++ b/packages/align-deps/test/__fixtures__/awesome-repo-extended/node_modules/john/package.json @@ -0,0 +1,15 @@ +{ + "name": "john", + "version": "1.0.0", + "dependencies": { + "dutch": "1.0.0" + }, + "rnx-kit": { + "reactNativeVersion": "^0.63 || ^0.64", + "capabilities": [ + "core-android", + "core-ios", + "storage" + ] + } +} diff --git a/packages/align-deps/test/__fixtures__/awesome-repo-extended/node_modules/quaid/package.json b/packages/align-deps/test/__fixtures__/awesome-repo-extended/node_modules/quaid/package.json new file mode 100644 index 000000000..7b57d84ed --- /dev/null +++ b/packages/align-deps/test/__fixtures__/awesome-repo-extended/node_modules/quaid/package.json @@ -0,0 +1,16 @@ +{ + "name": "quaid", + "version": "1.0.0", + "peerDependencies": { + "react": "17.0.1", + "react-native": "0.64.0" + }, + "rnx-kit": { + "reactNativeVersion": "^0.63 || ^0.64", + "capabilities": [ + "core-android", + "core-ios", + "webview" + ] + } +} diff --git a/packages/align-deps/test/__fixtures__/awesome-repo-extended/node_modules/react-native/package.json b/packages/align-deps/test/__fixtures__/awesome-repo-extended/node_modules/react-native/package.json new file mode 100644 index 000000000..8d4fc8c2b --- /dev/null +++ b/packages/align-deps/test/__fixtures__/awesome-repo-extended/node_modules/react-native/package.json @@ -0,0 +1,7 @@ +{ + "name": "react-native", + "version": "0.64.0", + "dependencies": { + "react": "17.0.1" + } +} diff --git a/packages/align-deps/test/__fixtures__/awesome-repo-extended/node_modules/react/package.json b/packages/align-deps/test/__fixtures__/awesome-repo-extended/node_modules/react/package.json new file mode 100644 index 000000000..e8ba83664 --- /dev/null +++ b/packages/align-deps/test/__fixtures__/awesome-repo-extended/node_modules/react/package.json @@ -0,0 +1,4 @@ +{ + "name": "react", + "version": "17.0.1" +} diff --git a/packages/align-deps/test/__fixtures__/awesome-repo-extended/node_modules/t-800/package.json b/packages/align-deps/test/__fixtures__/awesome-repo-extended/node_modules/t-800/package.json new file mode 100644 index 000000000..38055faab --- /dev/null +++ b/packages/align-deps/test/__fixtures__/awesome-repo-extended/node_modules/t-800/package.json @@ -0,0 +1,18 @@ +{ + "name": "t-800", + "version": "1.0.0", + "dependencies": { + "john": "1.0.0" + }, + "rnx-kit": { + "reactNativeVersion": "^0.63 || ^0.64", + "capabilities": [ + "core-android", + "core-ios", + "animation", + "cyberdyne", + "skynet", + "test-app" + ] + } +} diff --git a/packages/align-deps/test/__fixtures__/awesome-repo-extended/package.json b/packages/align-deps/test/__fixtures__/awesome-repo-extended/package.json new file mode 100644 index 000000000..c01cb6918 --- /dev/null +++ b/packages/align-deps/test/__fixtures__/awesome-repo-extended/package.json @@ -0,0 +1,19 @@ +{ + "name": "awesome-repo-extended", + "version": "1.0.0", + "dependencies": { + "conan": "1.0.0", + "quaid": "1.0.0", + "react": "17.0.1", + "react-native": "0.64.0" + }, + "rnx-kit": { + "reactNativeVersion": "^0.63 || ^0.64", + "kitType": "app", + "capabilities": [ + "core-android", + "hermes", + "lazy-index" + ] + } +} diff --git a/packages/align-deps/test/__fixtures__/awesome-repo/node_modules/conan/package.json b/packages/align-deps/test/__fixtures__/awesome-repo/node_modules/conan/package.json new file mode 100644 index 000000000..88bf75cc7 --- /dev/null +++ b/packages/align-deps/test/__fixtures__/awesome-repo/node_modules/conan/package.json @@ -0,0 +1,14 @@ +{ + "name": "conan", + "version": "1.0.0", + "dependencies": { + "t-800": "1.0.0" + }, + "rnx-kit": { + "reactNativeVersion": "^0.63 || ^0.64", + "capabilities": [ + "core-android", + "core-ios" + ] + } +} diff --git a/packages/align-deps/test/__fixtures__/awesome-repo/node_modules/dutch/package.json b/packages/align-deps/test/__fixtures__/awesome-repo/node_modules/dutch/package.json new file mode 100644 index 000000000..3ff6ec40e --- /dev/null +++ b/packages/align-deps/test/__fixtures__/awesome-repo/node_modules/dutch/package.json @@ -0,0 +1,16 @@ +{ + "name": "dutch", + "version": "1.0.0", + "peerDependencies": { + "react": "17.0.1", + "react-native": "0.64.0" + }, + "rnx-kit": { + "reactNativeVersion": "^0.63 || ^0.64", + "capabilities": [ + "core-android", + "core-ios", + "netinfo" + ] + } +} diff --git a/packages/align-deps/test/__fixtures__/awesome-repo/node_modules/john/package.json b/packages/align-deps/test/__fixtures__/awesome-repo/node_modules/john/package.json new file mode 100644 index 000000000..2560a9602 --- /dev/null +++ b/packages/align-deps/test/__fixtures__/awesome-repo/node_modules/john/package.json @@ -0,0 +1,15 @@ +{ + "name": "john", + "version": "1.0.0", + "dependencies": { + "dutch": "1.0.0" + }, + "rnx-kit": { + "reactNativeVersion": "^0.63 || ^0.64", + "capabilities": [ + "core-android", + "core-ios", + "storage" + ] + } +} diff --git a/packages/align-deps/test/__fixtures__/awesome-repo/node_modules/quaid/package.json b/packages/align-deps/test/__fixtures__/awesome-repo/node_modules/quaid/package.json new file mode 100644 index 000000000..7b57d84ed --- /dev/null +++ b/packages/align-deps/test/__fixtures__/awesome-repo/node_modules/quaid/package.json @@ -0,0 +1,16 @@ +{ + "name": "quaid", + "version": "1.0.0", + "peerDependencies": { + "react": "17.0.1", + "react-native": "0.64.0" + }, + "rnx-kit": { + "reactNativeVersion": "^0.63 || ^0.64", + "capabilities": [ + "core-android", + "core-ios", + "webview" + ] + } +} diff --git a/packages/align-deps/test/__fixtures__/awesome-repo/node_modules/react-native/package.json b/packages/align-deps/test/__fixtures__/awesome-repo/node_modules/react-native/package.json new file mode 100644 index 000000000..8d4fc8c2b --- /dev/null +++ b/packages/align-deps/test/__fixtures__/awesome-repo/node_modules/react-native/package.json @@ -0,0 +1,7 @@ +{ + "name": "react-native", + "version": "0.64.0", + "dependencies": { + "react": "17.0.1" + } +} diff --git a/packages/align-deps/test/__fixtures__/awesome-repo/node_modules/react/package.json b/packages/align-deps/test/__fixtures__/awesome-repo/node_modules/react/package.json new file mode 100644 index 000000000..e8ba83664 --- /dev/null +++ b/packages/align-deps/test/__fixtures__/awesome-repo/node_modules/react/package.json @@ -0,0 +1,4 @@ +{ + "name": "react", + "version": "17.0.1" +} diff --git a/packages/align-deps/test/__fixtures__/awesome-repo/node_modules/t-800/package.json b/packages/align-deps/test/__fixtures__/awesome-repo/node_modules/t-800/package.json new file mode 100644 index 000000000..dc1d1e888 --- /dev/null +++ b/packages/align-deps/test/__fixtures__/awesome-repo/node_modules/t-800/package.json @@ -0,0 +1,16 @@ +{ + "name": "t-800", + "version": "1.0.0", + "dependencies": { + "john": "1.0.0" + }, + "rnx-kit": { + "reactNativeVersion": "^0.63 || ^0.64", + "capabilities": [ + "core-android", + "core-ios", + "animation", + "test-app" + ] + } +} diff --git a/packages/align-deps/test/__fixtures__/awesome-repo/package.json b/packages/align-deps/test/__fixtures__/awesome-repo/package.json new file mode 100644 index 000000000..1bea7cc75 --- /dev/null +++ b/packages/align-deps/test/__fixtures__/awesome-repo/package.json @@ -0,0 +1,19 @@ +{ + "name": "awesome-repo", + "version": "1.0.0", + "dependencies": { + "conan": "1.0.0", + "quaid": "1.0.0", + "react": "17.0.1", + "react-native": "0.64.0" + }, + "rnx-kit": { + "reactNativeVersion": "^0.63 || ^0.64", + "kitType": "app", + "capabilities": [ + "core-android", + "hermes", + "lazy-index" + ] + } +} diff --git a/packages/align-deps/test/__fixtures__/config-custom-profiles-only/packageSpecificProfiles.js b/packages/align-deps/test/__fixtures__/config-custom-profiles-only/packageSpecificProfiles.js new file mode 100644 index 000000000..fd5d0635d --- /dev/null +++ b/packages/align-deps/test/__fixtures__/config-custom-profiles-only/packageSpecificProfiles.js @@ -0,0 +1,22 @@ +module.exports = { + 0.64: { + core: { + name: "react-native", + version: "0.64.3", + }, + react: { + name: "react", + version: "17.0.2", + }, + }, + 0.65: { + core: { + name: "react-native", + version: "0.65.2", + }, + react: { + name: "react", + version: "17.0.2", + }, + }, +}; diff --git a/packages/align-deps/test/__fixtures__/custom-profiles/local-profiles.js b/packages/align-deps/test/__fixtures__/custom-profiles/local-profiles.js new file mode 100644 index 000000000..e69de29bb diff --git a/packages/align-deps/test/__fixtures__/custom-profiles/node_modules/custom-profiles-package/index.js b/packages/align-deps/test/__fixtures__/custom-profiles/node_modules/custom-profiles-package/index.js new file mode 100644 index 000000000..e69de29bb diff --git a/packages/align-deps/test/__fixtures__/custom-profiles/node_modules/custom-profiles-package/package.json b/packages/align-deps/test/__fixtures__/custom-profiles/node_modules/custom-profiles-package/package.json new file mode 100644 index 000000000..b9d31bf36 --- /dev/null +++ b/packages/align-deps/test/__fixtures__/custom-profiles/node_modules/custom-profiles-package/package.json @@ -0,0 +1,5 @@ +{ + "name": "custom-profiles-package", + "version": "1.0.0", + "main": "index.js" +} diff --git a/packages/align-deps/test/__fixtures__/custom-profiles/root-level-profiles.js b/packages/align-deps/test/__fixtures__/custom-profiles/root-level-profiles.js new file mode 100644 index 000000000..a1fcec004 --- /dev/null +++ b/packages/align-deps/test/__fixtures__/custom-profiles/root-level-profiles.js @@ -0,0 +1,26 @@ +module.exports = { + format: { + name: "prettier", + version: "^2.5.1", + devOnly: true, + }, + 0.66: { + test: { + name: "jest", + version: "26.0", + devOnly: true, + }, + }, + 0.67: { + format: { + name: "prettier", + version: "3.0", + devOnly: true, + }, + test: { + name: "jest", + version: "27.0", + devOnly: true, + }, + }, +}; diff --git a/packages/align-deps/test/__fixtures__/custom-profiles/valid-profiles.js b/packages/align-deps/test/__fixtures__/custom-profiles/valid-profiles.js new file mode 100644 index 000000000..6a15a7ed9 --- /dev/null +++ b/packages/align-deps/test/__fixtures__/custom-profiles/valid-profiles.js @@ -0,0 +1,23 @@ +module.exports = { + 0.65: { + format: { + name: "prettier", + version: "^2.5.1", + devOnly: true, + }, + }, + 0.66: { + format: { + name: "prettier", + version: "^2.5.1", + devOnly: true, + }, + }, + 0.67: { + format: { + name: "prettier", + version: "^2.5.1", + devOnly: true, + }, + }, +}; diff --git a/packages/align-deps/test/__fixtures__/no-profile-satisfying-deps/node_modules/conan/package.json b/packages/align-deps/test/__fixtures__/no-profile-satisfying-deps/node_modules/conan/package.json new file mode 100644 index 000000000..a6d98f4c1 --- /dev/null +++ b/packages/align-deps/test/__fixtures__/no-profile-satisfying-deps/node_modules/conan/package.json @@ -0,0 +1,17 @@ +{ + "name": "conan", + "version": "1.0.0", + "peerDependencies": { + "react-native": "^0.63.2 || ^0.64.2" + }, + "devDependencies": { + "react-native": "^0.63.2" + }, + "rnx-kit": { + "reactNativeVersion": "^0.63 || ^0.64", + "capabilities": [ + "core-android", + "core-ios" + ] + } +} diff --git a/packages/align-deps/test/__fixtures__/no-profile-satisfying-deps/node_modules/react-native/package.json b/packages/align-deps/test/__fixtures__/no-profile-satisfying-deps/node_modules/react-native/package.json new file mode 100644 index 000000000..3bef893a7 --- /dev/null +++ b/packages/align-deps/test/__fixtures__/no-profile-satisfying-deps/node_modules/react-native/package.json @@ -0,0 +1,7 @@ +{ + "name": "react-native", + "version": "0.64.2", + "dependencies": { + "react": "17.0.1" + } +} diff --git a/packages/align-deps/test/__fixtures__/no-profile-satisfying-deps/node_modules/react/package.json b/packages/align-deps/test/__fixtures__/no-profile-satisfying-deps/node_modules/react/package.json new file mode 100644 index 000000000..e8ba83664 --- /dev/null +++ b/packages/align-deps/test/__fixtures__/no-profile-satisfying-deps/node_modules/react/package.json @@ -0,0 +1,4 @@ +{ + "name": "react", + "version": "17.0.1" +} diff --git a/packages/align-deps/test/__fixtures__/no-profile-satisfying-deps/node_modules/t-800/package.json b/packages/align-deps/test/__fixtures__/no-profile-satisfying-deps/node_modules/t-800/package.json new file mode 100644 index 000000000..01d042190 --- /dev/null +++ b/packages/align-deps/test/__fixtures__/no-profile-satisfying-deps/node_modules/t-800/package.json @@ -0,0 +1,17 @@ +{ + "name": "t-800", + "version": "1.0.0", + "peerDependencies": { + "react-native": "^0.63.2" + }, + "devDependencies": { + "react-native": "^0.63.2" + }, + "rnx-kit": { + "reactNativeVersion": "^0.63", + "capabilities": [ + "core-android", + "core-ios" + ] + } +} diff --git a/packages/align-deps/test/__fixtures__/no-profile-satisfying-deps/package.json b/packages/align-deps/test/__fixtures__/no-profile-satisfying-deps/package.json new file mode 100644 index 000000000..8810a65de --- /dev/null +++ b/packages/align-deps/test/__fixtures__/no-profile-satisfying-deps/package.json @@ -0,0 +1,18 @@ +{ + "name": "no-profile-satisfying-deps", + "version": "1.0.0", + "dependencies": { + "conan": "1.0.0", + "react": "17.0.1", + "react-native": "^0.64.2", + "t-800": "1.0.0" + }, + "rnx-kit": { + "reactNativeVersion": "^0.64", + "kitType": "app", + "capabilities": [ + "core-android", + "core-ios" + ] + } +} diff --git a/packages/align-deps/test/__mocks__/@rnx-kit/config.js b/packages/align-deps/test/__mocks__/@rnx-kit/config.js new file mode 100644 index 000000000..25cac233b --- /dev/null +++ b/packages/align-deps/test/__mocks__/@rnx-kit/config.js @@ -0,0 +1,13 @@ +const rnxKitConfig = jest.createMockFromModule("@rnx-kit/config"); +const actualKitConfig = jest.requireActual("@rnx-kit/config"); + +let kitConfig = ""; + +rnxKitConfig.__setMockConfig = (config) => { + kitConfig = config; +}; + +rnxKitConfig.getKitCapabilities = actualKitConfig.getKitCapabilities; +rnxKitConfig.getKitConfig = () => kitConfig; + +module.exports = rnxKitConfig; diff --git a/packages/align-deps/test/__mocks__/chalk.js b/packages/align-deps/test/__mocks__/chalk.js new file mode 100644 index 000000000..c07fae9cb --- /dev/null +++ b/packages/align-deps/test/__mocks__/chalk.js @@ -0,0 +1,16 @@ +const chalk = jest.createMockFromModule("chalk"); + +function passthrough(s) { + return s; +} + +chalk.bold = passthrough; +chalk.cyan = passthrough; +chalk.cyan.bold = passthrough; +chalk.dim = passthrough; +chalk.green = passthrough; +chalk.red = passthrough; +chalk.red.bold = passthrough; +chalk.yellow = passthrough; + +module.exports = chalk; diff --git a/packages/align-deps/test/__mocks__/fs.js b/packages/align-deps/test/__mocks__/fs.js new file mode 100644 index 000000000..72c7bde33 --- /dev/null +++ b/packages/align-deps/test/__mocks__/fs.js @@ -0,0 +1,19 @@ +const fs = jest.createMockFromModule("fs"); +const actualFs = jest.requireActual("fs"); + +let data = ""; + +fs.__setMockContent = (content, space = 2) => { + data = JSON.stringify(content, undefined, space) + "\n"; +}; + +fs.__setMockFileWriter = (writer) => { + fs.writeFileSync = writer; +}; + +fs.lstatSync = (...args) => actualFs.lstatSync(...args); +fs.readFileSync = (...args) => data || actualFs.readFileSync(...args); +fs.statSync = actualFs.statSync; // used by cosmiconfig +fs.writeFileSync = undefined; + +module.exports = fs; diff --git a/packages/align-deps/test/__snapshots__/check.app.test.ts.snap b/packages/align-deps/test/__snapshots__/check.app.test.ts.snap new file mode 100644 index 000000000..6ff1cdcb1 --- /dev/null +++ b/packages/align-deps/test/__snapshots__/check.app.test.ts.snap @@ -0,0 +1,31 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`checkPackageManifest({ kitType: 'app' }) adds required dependencies 1`] = ` +"{ + \\"name\\": \\"awesome-repo\\", + \\"version\\": \\"1.0.0\\", + \\"dependencies\\": { + \\"@react-native-async-storage/async-storage\\": \\"^1.15.8\\", + \\"@react-native-community/async-storage\\": \\"^1.12.1\\", + \\"@react-native-community/netinfo\\": \\"^6.0.2\\", + \\"conan\\": \\"1.0.0\\", + \\"hermes-engine\\": \\"~0.7.0\\", + \\"quaid\\": \\"1.0.0\\", + \\"react\\": \\"17.0.1\\", + \\"react-native\\": \\"^0.64.2\\", + \\"react-native-lazy-index\\": \\"^2.1.1\\", + \\"react-native-reanimated\\": \\"^2.1.0\\", + \\"react-native-webview\\": \\"^11.4.2\\" + }, + \\"rnx-kit\\": { + \\"reactNativeVersion\\": \\"^0.63 || ^0.64\\", + \\"kitType\\": \\"app\\", + \\"capabilities\\": [ + \\"core-android\\", + \\"hermes\\", + \\"lazy-index\\" + ] + } +} +" +`; diff --git a/packages/align-deps/test/__snapshots__/check.test.ts.snap b/packages/align-deps/test/__snapshots__/check.test.ts.snap new file mode 100644 index 000000000..fc88acb81 --- /dev/null +++ b/packages/align-deps/test/__snapshots__/check.test.ts.snap @@ -0,0 +1,37 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`checkPackageManifest({ kitType: 'library' }) preserves indentation in 'package.json' 1`] = ` +"{ + \\"name\\": \\"@rnx-kit/align-deps\\", + \\"version\\": \\"0.0.1\\", + \\"peerDependencies\\": { + \\"react\\": \\"17.0.1\\", + \\"react-native\\": \\"^0.64.2\\" + }, + \\"devDependencies\\": { + \\"react\\": \\"17.0.1\\", + \\"react-native\\": \\"^0.64.2\\" + } +} +" +`; + +exports[`checkPackageManifest({ kitType: 'library' }) prints warnings when detecting bad packages (with version range) 1`] = ` +Array [ + Array [ + "warn", + "Known bad packages are found in '@rnx-kit/align-deps': + react-native-linear-gradient@<2.6.0: This package causes significant degradation in app start up time prior to 2.6.0.", + ], +] +`; + +exports[`checkPackageManifest({ kitType: 'library' }) prints warnings when detecting bad packages 1`] = ` +Array [ + Array [ + "warn", + "Known bad packages are found in '@rnx-kit/align-deps': + react-native-linear-gradient@<2.6.0: This package causes significant degradation in app start up time prior to 2.6.0.", + ], +] +`; diff --git a/packages/align-deps/test/__snapshots__/profiles.test.ts.snap b/packages/align-deps/test/__snapshots__/profiles.test.ts.snap new file mode 100644 index 000000000..f1575a48f --- /dev/null +++ b/packages/align-deps/test/__snapshots__/profiles.test.ts.snap @@ -0,0 +1,112 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`loadCustomProfiles() loads valid custom profiles 1`] = ` +Object { + "0.65": Object { + "format": Object { + "devOnly": true, + "name": "prettier", + "version": "^2.5.1", + }, + }, + "0.66": Object { + "format": Object { + "devOnly": true, + "name": "prettier", + "version": "^2.5.1", + }, + }, + "0.67": Object { + "format": Object { + "devOnly": true, + "name": "prettier", + "version": "^2.5.1", + }, + }, +} +`; + +exports[`loadCustomProfiles() prepends root-level capabilities to all profiles 1`] = ` +Object { + "0.61": Object { + "format": Object { + "devOnly": true, + "name": "prettier", + "version": "^2.5.1", + }, + }, + "0.62": Object { + "format": Object { + "devOnly": true, + "name": "prettier", + "version": "^2.5.1", + }, + }, + "0.63": Object { + "format": Object { + "devOnly": true, + "name": "prettier", + "version": "^2.5.1", + }, + }, + "0.64": Object { + "format": Object { + "devOnly": true, + "name": "prettier", + "version": "^2.5.1", + }, + }, + "0.65": Object { + "format": Object { + "devOnly": true, + "name": "prettier", + "version": "^2.5.1", + }, + }, + "0.66": Object { + "format": Object { + "devOnly": true, + "name": "prettier", + "version": "^2.5.1", + }, + "test": Object { + "devOnly": true, + "name": "jest", + "version": "26.0", + }, + }, + "0.67": Object { + "format": Object { + "devOnly": true, + "name": "prettier", + "version": "3.0", + }, + "test": Object { + "devOnly": true, + "name": "jest", + "version": "27.0", + }, + }, + "0.68": Object { + "format": Object { + "devOnly": true, + "name": "prettier", + "version": "^2.5.1", + }, + }, + "0.69": Object { + "format": Object { + "devOnly": true, + "name": "prettier", + "version": "^2.5.1", + }, + }, + "0.70": Object { + "format": Object { + "devOnly": true, + "name": "prettier", + "version": "^2.5.1", + }, + }, +} +`; diff --git a/packages/align-deps/test/__snapshots__/vigilant.test.ts.snap b/packages/align-deps/test/__snapshots__/vigilant.test.ts.snap new file mode 100644 index 000000000..86886677f --- /dev/null +++ b/packages/align-deps/test/__snapshots__/vigilant.test.ts.snap @@ -0,0 +1,249 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`buildManifestProfile() builds a package manifest for a single profile version 1`] = ` +Object { + "dependencies": Object { + "@react-native-async-storage/async-storage": "^1.15.8", + "@react-native-clipboard/clipboard": "^1.8.3", + "@react-native-community/checkbox": "^0.5.8", + "@react-native-community/datetimepicker": "^3.4.6", + "@react-native-community/hooks": "^2.6.0", + "@react-native-community/netinfo": "^6.0.2", + "@react-native-masked-view/masked-view": "^0.2.4", + "@react-navigation/native": "^5.9.8", + "@react-navigation/stack": "^5.14.9", + "hermes-engine": "~0.7.0", + "jest": "^26.5.2", + "metro": "^0.64.0", + "metro-config": "^0.64.0", + "metro-core": "^0.64.0", + "metro-react-native-babel-preset": "^0.64.0", + "metro-react-native-babel-transformer": "^0.64.0", + "metro-resolver": "^0.64.0", + "metro-runtime": "^0.64.0", + "react": "17.0.1", + "react-dom": "17.0.1", + "react-native": "^0.64.2", + "react-native-base64": "^0.2.1", + "react-native-floating-action": "^1.21.0", + "react-native-fs": "^2.17.0", + "react-native-gesture-handler": "^1.10.3", + "react-native-lazy-index": "^2.1.1", + "react-native-macos": "^0.64.0", + "react-native-modal": "^11.10.0", + "react-native-popover-view": "^4.0.3", + "react-native-reanimated": "^2.1.0", + "react-native-render-html": "^5.1.1", + "react-native-safe-area-context": "^3.2.0", + "react-native-screens": "^3.1.1", + "react-native-shimmer": "^0.5.0", + "react-native-sqlite-storage": "^5.0.0", + "react-native-svg": "^12.1.1", + "react-native-test-app": "^0.11.4", + "react-native-webview": "^11.4.2", + "react-native-windows": "^0.64.0", + "react-test-renderer": "17.0.1", + }, + "devDependencies": Object { + "@react-native-async-storage/async-storage": "^1.15.8", + "@react-native-clipboard/clipboard": "^1.8.3", + "@react-native-community/checkbox": "^0.5.8", + "@react-native-community/datetimepicker": "^3.4.6", + "@react-native-community/hooks": "^2.6.0", + "@react-native-community/netinfo": "^6.0.2", + "@react-native-masked-view/masked-view": "^0.2.4", + "@react-navigation/native": "^5.9.8", + "@react-navigation/stack": "^5.14.9", + "hermes-engine": "~0.7.0", + "jest": "^26.5.2", + "metro": "^0.64.0", + "metro-config": "^0.64.0", + "metro-core": "^0.64.0", + "metro-react-native-babel-preset": "^0.64.0", + "metro-react-native-babel-transformer": "^0.64.0", + "metro-resolver": "^0.64.0", + "metro-runtime": "^0.64.0", + "react": "17.0.1", + "react-dom": "17.0.1", + "react-native": "^0.64.2", + "react-native-base64": "^0.2.1", + "react-native-floating-action": "^1.21.0", + "react-native-fs": "^2.17.0", + "react-native-gesture-handler": "^1.10.3", + "react-native-lazy-index": "^2.1.1", + "react-native-macos": "^0.64.0", + "react-native-modal": "^11.10.0", + "react-native-popover-view": "^4.0.3", + "react-native-reanimated": "^2.1.0", + "react-native-render-html": "^5.1.1", + "react-native-safe-area-context": "^3.2.0", + "react-native-screens": "^3.1.1", + "react-native-shimmer": "^0.5.0", + "react-native-sqlite-storage": "^5.0.0", + "react-native-svg": "^12.1.1", + "react-native-test-app": "^0.11.4", + "react-native-webview": "^11.4.2", + "react-native-windows": "^0.64.0", + "react-test-renderer": "17.0.1", + }, + "name": "@rnx-kit/align-deps", + "peerDependencies": Object { + "@react-native-async-storage/async-storage": "^1.15.8", + "@react-native-clipboard/clipboard": "^1.8.3", + "@react-native-community/checkbox": "^0.5.8", + "@react-native-community/datetimepicker": "^3.4.6", + "@react-native-community/hooks": "^2.6.0", + "@react-native-community/netinfo": "^6.0.2", + "@react-native-masked-view/masked-view": "^0.2.4", + "@react-navigation/native": "^5.9.8", + "@react-navigation/stack": "^5.14.9", + "hermes-engine": "~0.7.0", + "react": "17.0.1", + "react-dom": "17.0.1", + "react-native": "^0.64.2", + "react-native-base64": "^0.2.1", + "react-native-floating-action": "^1.21.0", + "react-native-fs": "^2.17.0", + "react-native-gesture-handler": "^1.10.3", + "react-native-lazy-index": "^2.1.1", + "react-native-macos": "^0.64.0", + "react-native-modal": "^11.10.0", + "react-native-popover-view": "^4.0.3", + "react-native-reanimated": "^2.1.0", + "react-native-render-html": "^5.1.1", + "react-native-safe-area-context": "^3.2.0", + "react-native-screens": "^3.1.1", + "react-native-shimmer": "^0.5.0", + "react-native-sqlite-storage": "^5.0.0", + "react-native-svg": "^12.1.1", + "react-native-webview": "^11.4.2", + "react-native-windows": "^0.64.0", + }, + "version": "1.0.0-test", +} +`; + +exports[`buildManifestProfile() builds a package manifest for multiple profile versions 1`] = ` +Object { + "dependencies": Object { + "@react-native-async-storage/async-storage": "^1.15.8", + "@react-native-clipboard/clipboard": "^1.8.3", + "@react-native-community/checkbox": "^0.5.8", + "@react-native-community/datetimepicker": "^3.4.6", + "@react-native-community/hooks": "^2.6.0", + "@react-native-community/netinfo": "^6.0.2", + "@react-native-masked-view/masked-view": "^0.2.4", + "@react-navigation/native": "^5.9.8", + "@react-navigation/stack": "^5.14.9", + "hermes-engine": "~0.7.0", + "jest": "^26.5.2", + "metro": "^0.64.0", + "metro-config": "^0.64.0", + "metro-core": "^0.64.0", + "metro-react-native-babel-preset": "^0.64.0", + "metro-react-native-babel-transformer": "^0.64.0", + "metro-resolver": "^0.64.0", + "metro-runtime": "^0.64.0", + "react": "17.0.1", + "react-dom": "17.0.1", + "react-native": "^0.64.2", + "react-native-base64": "^0.2.1", + "react-native-floating-action": "^1.21.0", + "react-native-fs": "^2.17.0", + "react-native-gesture-handler": "^1.10.3", + "react-native-lazy-index": "^2.1.1", + "react-native-macos": "^0.64.0", + "react-native-modal": "^11.10.0", + "react-native-popover-view": "^4.0.3", + "react-native-reanimated": "^2.1.0", + "react-native-render-html": "^5.1.1", + "react-native-safe-area-context": "^3.2.0", + "react-native-screens": "^3.1.1", + "react-native-shimmer": "^0.5.0", + "react-native-sqlite-storage": "^5.0.0", + "react-native-svg": "^12.1.1", + "react-native-test-app": "^0.11.4", + "react-native-webview": "^11.4.2", + "react-native-windows": "^0.64.0", + "react-test-renderer": "17.0.1", + }, + "devDependencies": Object { + "@react-native-async-storage/async-storage": "^1.15.8", + "@react-native-clipboard/clipboard": "^1.8.3", + "@react-native-community/checkbox": "^0.5.8", + "@react-native-community/datetimepicker": "^3.4.6", + "@react-native-community/hooks": "^2.6.0", + "@react-native-community/netinfo": "^6.0.2", + "@react-native-masked-view/masked-view": "^0.2.4", + "@react-navigation/native": "^5.9.8", + "@react-navigation/stack": "^5.14.9", + "hermes-engine": "~0.7.0", + "jest": "^26.5.2", + "metro": "^0.64.0", + "metro-config": "^0.64.0", + "metro-core": "^0.64.0", + "metro-react-native-babel-preset": "^0.64.0", + "metro-react-native-babel-transformer": "^0.64.0", + "metro-resolver": "^0.64.0", + "metro-runtime": "^0.64.0", + "react": "17.0.1", + "react-dom": "17.0.1", + "react-native": "^0.64.2", + "react-native-base64": "^0.2.1", + "react-native-floating-action": "^1.21.0", + "react-native-fs": "^2.17.0", + "react-native-gesture-handler": "^1.10.3", + "react-native-lazy-index": "^2.1.1", + "react-native-macos": "^0.64.0", + "react-native-modal": "^11.10.0", + "react-native-popover-view": "^4.0.3", + "react-native-reanimated": "^2.1.0", + "react-native-render-html": "^5.1.1", + "react-native-safe-area-context": "^3.2.0", + "react-native-screens": "^3.1.1", + "react-native-shimmer": "^0.5.0", + "react-native-sqlite-storage": "^5.0.0", + "react-native-svg": "^12.1.1", + "react-native-test-app": "^0.11.4", + "react-native-webview": "^11.4.2", + "react-native-windows": "^0.64.0", + "react-test-renderer": "17.0.1", + }, + "name": "@rnx-kit/align-deps", + "peerDependencies": Object { + "@react-native-async-storage/async-storage": "^1.15.8", + "@react-native-clipboard/clipboard": "^1.8.3", + "@react-native-community/async-storage": "^1.12.1", + "@react-native-community/checkbox": "^0.5.7 || ^0.5.8", + "@react-native-community/clipboard": "^1.5.1", + "@react-native-community/datetimepicker": "^3.0.9 || ^3.4.6", + "@react-native-community/hooks": "^2.6.0", + "@react-native-community/netinfo": "^5.9.10 || ^6.0.2", + "@react-native-masked-view/masked-view": "^0.2.4", + "@react-navigation/native": "^5.9.4 || ^5.9.8", + "@react-navigation/stack": "^5.14.4 || ^5.14.9", + "hermes-engine": "~0.5.0 || ~0.7.0", + "react": "16.13.1 || 17.0.1", + "react-dom": "16.13.1 || 17.0.1", + "react-native": "^0.63.2 || ^0.64.2", + "react-native-base64": "^0.2.1", + "react-native-floating-action": "^1.21.0", + "react-native-fs": "^2.16.6 || ^2.17.0", + "react-native-gesture-handler": "^1.10.3", + "react-native-lazy-index": "^2.1.1", + "react-native-macos": "^0.63.0 || ^0.64.0", + "react-native-modal": "^11.5.6 || ^11.10.0", + "react-native-popover-view": "^3.1.1 || ^4.0.3", + "react-native-reanimated": "^1.13.3 || ^2.1.0", + "react-native-render-html": "^5.1.0 || ^5.1.1", + "react-native-safe-area-context": "^3.2.0", + "react-native-screens": "^2.18.1 || ^3.1.1", + "react-native-shimmer": "^0.5.0", + "react-native-sqlite-storage": "^3.3.11 || ^5.0.0", + "react-native-svg": "^12.1.1", + "react-native-webview": "^11.4.2", + "react-native-windows": "^0.63.0 || ^0.64.0", + }, + "version": "1.0.0-test", +} +`; diff --git a/packages/align-deps/test/capabilities.test.ts b/packages/align-deps/test/capabilities.test.ts new file mode 100644 index 000000000..7b976f4af --- /dev/null +++ b/packages/align-deps/test/capabilities.test.ts @@ -0,0 +1,276 @@ +import type { Capability } from "@rnx-kit/config"; +import { capabilitiesFor, resolveCapabilities } from "../src/capabilities"; +import profile_0_62 from "../src/presets/microsoft/profile-0.62"; +import profile_0_63 from "../src/presets/microsoft/profile-0.63"; +import profile_0_64 from "../src/presets/microsoft/profile-0.64"; +import { getProfilesFor } from "../src/profiles"; +import { pickPackage } from "./helpers"; + +describe("capabilitiesFor()", () => { + test("returns `undefined` when react-native is not a dependency", () => { + expect( + capabilitiesFor({ name: "@rnx-kit/align-deps", version: "1.0.0" }) + ).toBeUndefined(); + expect( + capabilitiesFor({ + name: "@rnx-kit/align-deps", + version: "1.0.0", + dependencies: { + react: "^17.0.1", + }, + }) + ).toBeUndefined(); + }); + + test("returns capabilities when react-native is under dependencies", () => { + const manifest = { + name: "@rnx-kit/align-deps", + version: "1.0.0", + dependencies: { + "react-native": "^0.64.1", + }, + }; + expect(capabilitiesFor(manifest)).toEqual({ + reactNativeVersion: "^0.64", + reactNativeDevVersion: "0.64.0", + kitType: "library", + capabilities: ["core", "core-android", "core-ios"], + }); + }); + + test("returns capabilities when react-native is under peerDependencies", () => { + const manifest = { + name: "@rnx-kit/align-deps", + version: "1.0.0", + peerDependencies: { + "react-native": "^0.64.1", + }, + }; + expect(capabilitiesFor(manifest)).toEqual({ + reactNativeVersion: "^0.64", + reactNativeDevVersion: "0.64.0", + kitType: "library", + capabilities: ["core", "core-android", "core-ios"], + }); + }); + + test("returns capabilities when react-native is under devDependencies", () => { + const manifest = { + name: "@rnx-kit/align-deps", + version: "1.0.0", + devDependencies: { + "react-native": "^0.64.1", + }, + }; + expect(capabilitiesFor(manifest)).toEqual({ + reactNativeVersion: "^0.64", + reactNativeDevVersion: "^0.64.1", + kitType: "library", + capabilities: ["core", "core-android", "core-ios"], + }); + }); + + test("returns kit config with app type instead of dev version", () => { + const manifest = { + name: "@rnx-kit/align-deps", + version: "1.0.0", + peerDependencies: { + "react-native": "^0.64.1", + }, + }; + expect(capabilitiesFor(manifest, { kitType: "app" })).toEqual({ + reactNativeVersion: "^0.64", + kitType: "app", + capabilities: ["core", "core-android", "core-ios"], + }); + }); + + test("ignores packages that are not managed by align-deps", () => { + const manifest = { + name: "@rnx-kit/align-deps", + version: "1.0.0", + peerDependencies: { + react: "17.0.1", + "react-native": "^0.64.1", + }, + devDependencies: { + "@rnx-kit/babel-preset-metro-react-native": "*", + "@rnx-kit/cli": "*", + }, + }; + expect(capabilitiesFor(manifest, { kitType: "app" })).toEqual({ + reactNativeVersion: "^0.64", + kitType: "app", + capabilities: ["core", "core-android", "core-ios", "react"], + }); + }); +}); + +describe("resolveCapabilities()", () => { + const consoleWarnSpy = jest.spyOn(global.console, "warn"); + + beforeEach(() => { + consoleWarnSpy.mockReset(); + }); + + afterAll(() => { + jest.clearAllMocks(); + }); + + test("dedupes packages", () => { + const packages = resolveCapabilities( + ["core", "core", "test-app"], + [profile_0_64] + ); + + const { name } = profile_0_64["core"]; + const { name: reactName } = profile_0_64["react"]; + const { name: testAppName } = profile_0_64["test-app"]; + expect(packages).toEqual({ + [name]: [profile_0_64["core"]], + [reactName]: [profile_0_64["react"]], + [testAppName]: [profile_0_64["test-app"]], + }); + + expect(consoleWarnSpy).not.toBeCalled(); + }); + + test("dedupes package versions", () => { + const packages = resolveCapabilities( + ["webview"], + [profile_0_62, profile_0_63, profile_0_64] + ); + + const { name } = profile_0_64["webview"]; + expect(packages).toEqual({ + [name]: [profile_0_62["webview"], profile_0_64["webview"]], + }); + + expect(consoleWarnSpy).not.toBeCalled(); + }); + + test("ignores missing/unknown capabilities", () => { + const packages = resolveCapabilities( + ["skynet" as Capability, "svg"], + [profile_0_62, profile_0_63, profile_0_64] + ); + + const { name } = profile_0_64["svg"]; + expect(packages).toEqual({ [name]: [profile_0_64["svg"]] }); + expect(consoleWarnSpy).toBeCalledTimes(1); + }); + + test("resolves custom capabilities", () => { + const skynet = { name: "skynet", version: "1.0.0" }; + jest.mock( + "mock-custom-profiles-module", + () => ({ "0.62": { [skynet.name]: skynet } }), + { virtual: true } + ); + + const profiles = getProfilesFor( + "^0.62 || ^0.63 || ^0.64", + "mock-custom-profiles-module" + ); + + const packages = resolveCapabilities( + ["skynet" as Capability, "svg"], + profiles + ); + + const { name } = profile_0_64["svg"]; + expect(packages).toEqual({ + [name]: [profile_0_64["svg"]], + [skynet.name]: [skynet], + }); + }); + + test("resolves capabilities required by capabilities", () => { + const packages = resolveCapabilities( + ["core-windows"], + [profile_0_63, profile_0_64] + ); + + expect(packages).toEqual({ + react: [ + pickPackage(profile_0_63, "react"), + pickPackage(profile_0_64, "react"), + ], + "react-native": [ + pickPackage(profile_0_63, "core"), + pickPackage(profile_0_64, "core"), + ], + "react-native-windows": [ + pickPackage(profile_0_63, "core-windows"), + pickPackage(profile_0_64, "core-windows"), + ], + }); + + expect(consoleWarnSpy).not.toBeCalled(); + }); + + test("resolves meta packages", () => { + jest.mock( + "mock-meta-package", + () => ({ + "0.64": { + "core/all": { + name: "#meta", + capabilities: [ + "core-android", + "core-ios", + "core-macos", + "core-windows", + ], + }, + }, + }), + { virtual: true } + ); + + const packages = resolveCapabilities( + ["core/all" as Capability], + getProfilesFor("^0.64", "mock-meta-package") + ); + + expect(packages).toEqual({ + react: [pickPackage(profile_0_64, "react")], + "react-native": [pickPackage(profile_0_64, "core")], + "react-native-macos": [pickPackage(profile_0_64, "core-macos")], + "react-native-windows": [pickPackage(profile_0_64, "core-windows")], + }); + }); + + test("resolves meta packages with loops", () => { + jest.mock( + "mock-meta-package-loop", + () => ({ + "0.64": { + connor: { + name: "#meta", + capabilities: ["core", "reese"], + }, + reese: { + name: "#meta", + capabilities: ["t-800"], + }, + "t-800": { + name: "#meta", + capabilities: ["connor"], + }, + }, + }), + { virtual: true } + ); + + const packages = resolveCapabilities( + ["reese" as Capability], + getProfilesFor("^0.64", "mock-meta-package-loop") + ); + + expect(packages).toEqual({ + react: [pickPackage(profile_0_64, "react")], + "react-native": [pickPackage(profile_0_64, "core")], + }); + }); +}); diff --git a/packages/align-deps/test/check.app.test.ts b/packages/align-deps/test/check.app.test.ts new file mode 100644 index 000000000..97007b835 --- /dev/null +++ b/packages/align-deps/test/check.app.test.ts @@ -0,0 +1,40 @@ +import path from "path"; +import { checkPackageManifest } from "../src/check"; + +jest.mock("fs"); +jest.unmock("@rnx-kit/config"); + +function fixturePath(name: string) { + return path.join(process.cwd(), "test", "__fixtures__", name); +} + +describe("checkPackageManifest({ kitType: 'app' })", () => { + const fs = require("fs"); + const consoleWarnSpy = jest.spyOn(global.console, "warn"); + + beforeEach(() => { + consoleWarnSpy.mockReset(); + }); + + afterAll(() => { + jest.clearAllMocks(); + }); + + test("adds required dependencies", () => { + const manifestPath = path.join(fixturePath("awesome-repo"), "package.json"); + + let destination = ""; + let updatedManifest = ""; + fs.__setMockFileWriter((dest, content) => { + destination = dest; + updatedManifest = content; + }); + + expect( + checkPackageManifest(manifestPath, { loose: false, write: true }) + ).toBe(0); + expect(consoleWarnSpy).not.toBeCalled(); + expect(destination).toBe(manifestPath); + expect(updatedManifest).toMatchSnapshot(); + }); +}); diff --git a/packages/align-deps/test/check.test.ts b/packages/align-deps/test/check.test.ts new file mode 100644 index 000000000..c4a18d762 --- /dev/null +++ b/packages/align-deps/test/check.test.ts @@ -0,0 +1,359 @@ +import semverCoerce from "semver/functions/coerce"; +import { checkPackageManifest, getCheckConfig } from "../src/check"; +import profile_0_62 from "../src/presets/microsoft/profile-0.62"; +import profile_0_63 from "../src/presets/microsoft/profile-0.63"; +import profile_0_64 from "../src/presets/microsoft/profile-0.64"; +import { packageVersion } from "./helpers"; + +jest.mock("fs"); + +describe("checkPackageManifest({ kitType: 'library' })", () => { + const rnxKitConfig = require("@rnx-kit/config"); + const fs = require("fs"); + + const consoleErrorSpy = jest.spyOn(global.console, "error"); + const consoleLogSpy = jest.spyOn(global.console, "log"); + const consoleWarnSpy = jest.spyOn(global.console, "warn"); + + const defaultOptions = { loose: false, write: false }; + + const mockManifest = { + name: "@rnx-kit/align-deps", + version: "0.0.1", + }; + + const react_v62_v63_v64 = [ + packageVersion(profile_0_62, "react"), + packageVersion(profile_0_63, "react"), + packageVersion(profile_0_64, "react"), + ].join(" || "); + + const v62_v63_v64 = [ + packageVersion(profile_0_62, "core"), + packageVersion(profile_0_63, "core"), + packageVersion(profile_0_64, "core"), + ].join(" || "); + + beforeEach(() => { + consoleErrorSpy.mockReset(); + consoleLogSpy.mockReset(); + consoleWarnSpy.mockReset(); + fs.__setMockContent({}); + rnxKitConfig.__setMockConfig(); + }); + + afterAll(() => { + jest.clearAllMocks(); + }); + + test("returns error code when reading invalid manifests", () => { + expect(checkPackageManifest("package.json", defaultOptions)).not.toBe(0); + expect(consoleErrorSpy).toBeCalledTimes(1); + }); + + test("returns early if 'rnx-kit' is missing from the manifest", () => { + fs.__setMockContent({ + ...mockManifest, + dependencies: { "react-native-linear-gradient": "0.0.0" }, + }); + + const options = { ...defaultOptions, uncheckedReturnCode: -1 }; + expect(checkPackageManifest("package.json", options)).toBe(-1); + expect(consoleWarnSpy).toBeCalled(); + }); + + test("prints warnings when detecting bad packages", () => { + fs.__setMockContent({ + ...mockManifest, + dependencies: { "react-native-linear-gradient": "0.0.0" }, + peerDependencies: { + "react-native": profile_0_64["core"], + }, + devDependencies: { + "react-native": profile_0_64["core"], + }, + }); + rnxKitConfig.__setMockConfig({ reactNativeVersion: "0.64.0" }); + + expect(checkPackageManifest("package.json", defaultOptions)).toBe(0); + expect(consoleErrorSpy).not.toBeCalled(); + expect(consoleWarnSpy.mock.calls).toMatchSnapshot(); + }); + + test("prints warnings when detecting bad packages (with version range)", () => { + fs.__setMockContent({ + ...mockManifest, + dependencies: { "react-native-linear-gradient": "0.0.0" }, + }); + rnxKitConfig.__setMockConfig({ reactNativeVersion: "^0.63.0 || ^0.64.0" }); + + expect(checkPackageManifest("package.json", defaultOptions)).toBe(0); + expect(consoleErrorSpy).not.toBeCalled(); + expect(consoleWarnSpy.mock.calls).toMatchSnapshot(); + }); + + test("returns early if no capabilities are defined", () => { + fs.__setMockContent(mockManifest); + rnxKitConfig.__setMockConfig({ reactNativeVersion: "0.64.0" }); + + expect(checkPackageManifest("package.json", defaultOptions)).toBe(0); + expect(consoleErrorSpy).not.toBeCalled(); + expect(consoleWarnSpy).not.toBeCalled(); + }); + + test("returns if no changes are needed", () => { + fs.__setMockContent({ + ...mockManifest, + peerDependencies: { + react: packageVersion(profile_0_64, "react"), + "react-native": packageVersion(profile_0_64, "core"), + }, + devDependencies: { + react: packageVersion(profile_0_64, "react"), + "react-native": packageVersion(profile_0_64, "core"), + }, + }); + rnxKitConfig.__setMockConfig({ + reactNativeVersion: "0.64.0", + capabilities: ["core-ios"], + }); + + expect(checkPackageManifest("package.json", defaultOptions)).toBe(0); + expect(consoleErrorSpy).not.toBeCalled(); + expect(consoleLogSpy).not.toBeCalled(); + expect(consoleWarnSpy).not.toBeCalled(); + }); + + test("returns if no changes are needed (write: true)", () => { + let didWriteToPath = false; + + fs.__setMockContent({ + ...mockManifest, + peerDependencies: { + react: packageVersion(profile_0_64, "react"), + "react-native": packageVersion(profile_0_64, "core"), + }, + devDependencies: { + react: packageVersion(profile_0_64, "react"), + "react-native": packageVersion(profile_0_64, "core"), + }, + }); + fs.__setMockFileWriter((p, _content) => { + didWriteToPath = p; + }); + rnxKitConfig.__setMockConfig({ + reactNativeVersion: "0.64.0", + capabilities: ["core-ios"], + }); + + expect( + checkPackageManifest("package.json", { loose: false, write: true }) + ).toBe(0); + expect(didWriteToPath).toBe(false); + expect(consoleErrorSpy).not.toBeCalled(); + expect(consoleLogSpy).not.toBeCalled(); + expect(consoleWarnSpy).not.toBeCalled(); + }); + + test("returns error code if changes are needed", () => { + fs.__setMockContent(mockManifest); + rnxKitConfig.__setMockConfig({ + reactNativeVersion: "0.64.0", + capabilities: ["core-ios"], + }); + + expect(checkPackageManifest("package.json", defaultOptions)).not.toBe(0); + expect(consoleErrorSpy).toBeCalledTimes(1); + expect(consoleWarnSpy).not.toBeCalled(); + expect(consoleLogSpy).toBeCalledTimes(2); + }); + + test("writes changes back to 'package.json'", () => { + let didWriteToPath = false; + + fs.__setMockContent(mockManifest); + fs.__setMockFileWriter((p, _content) => { + didWriteToPath = p; + }); + rnxKitConfig.__setMockConfig({ + reactNativeVersion: "0.64.0", + capabilities: ["core-ios"], + }); + + expect( + checkPackageManifest("package.json", { loose: false, write: true }) + ).toBe(0); + expect(didWriteToPath).toBe("package.json"); + expect(consoleErrorSpy).not.toBeCalled(); + expect(consoleWarnSpy).not.toBeCalled(); + expect(consoleLogSpy).not.toBeCalled(); + }); + + test("preserves indentation in 'package.json'", () => { + let output = ""; + + fs.__setMockContent(mockManifest, "\t"); + fs.__setMockFileWriter((_, content) => { + output = content; + }); + rnxKitConfig.__setMockConfig({ + reactNativeVersion: "0.64.0", + capabilities: ["core-ios"], + }); + + expect( + checkPackageManifest("package.json", { loose: false, write: true }) + ).toBe(0); + expect(output).toMatchSnapshot(); + expect(consoleErrorSpy).not.toBeCalled(); + expect(consoleWarnSpy).not.toBeCalled(); + expect(consoleLogSpy).not.toBeCalled(); + }); + + test("uses minimum supported version as development version", () => { + fs.__setMockContent({ + ...mockManifest, + peerDependencies: { + react: react_v62_v63_v64, + "react-native": v62_v63_v64, + }, + devDependencies: { + react: packageVersion(profile_0_62, "react"), + "react-native": packageVersion(profile_0_62, "core"), + }, + }); + rnxKitConfig.__setMockConfig({ + reactNativeVersion: "^0.62 || ^0.63 || ^0.64", + capabilities: ["core-ios"], + }); + + expect(checkPackageManifest("package.json", defaultOptions)).toBe(0); + expect(consoleErrorSpy).not.toBeCalled(); + expect(consoleLogSpy).not.toBeCalled(); + expect(consoleWarnSpy).not.toBeCalled(); + }); + + test("uses declared development version", () => { + fs.__setMockContent({ + ...mockManifest, + peerDependencies: { + react: react_v62_v63_v64, + "react-native": v62_v63_v64, + }, + devDependencies: { + react: packageVersion(profile_0_63, "react"), + "react-native": packageVersion(profile_0_63, "core"), + }, + }); + rnxKitConfig.__setMockConfig({ + reactNativeVersion: "^0.62 || ^0.63 || ^0.64", + reactNativeDevVersion: "0.63.4", + capabilities: ["core-ios"], + }); + + expect(checkPackageManifest("package.json", defaultOptions)).toBe(0); + expect(consoleErrorSpy).not.toBeCalled(); + expect(consoleLogSpy).not.toBeCalled(); + expect(consoleWarnSpy).not.toBeCalled(); + }); + + test("handles development version ranges", () => { + fs.__setMockContent({ + ...mockManifest, + peerDependencies: { + react: react_v62_v63_v64, + "react-native": v62_v63_v64, + }, + devDependencies: { + react: packageVersion(profile_0_63, "react"), + "react-native": packageVersion(profile_0_63, "core"), + }, + }); + rnxKitConfig.__setMockConfig({ + reactNativeVersion: "^0.62 || ^0.63 || ^0.64", + reactNativeDevVersion: "^0.63.4", + capabilities: ["core-ios"], + }); + + expect(checkPackageManifest("package.json", defaultOptions)).toBe(0); + expect(consoleErrorSpy).not.toBeCalled(); + expect(consoleLogSpy).not.toBeCalled(); + expect(consoleWarnSpy).not.toBeCalled(); + }); +}); + +describe("getCheckConfig", () => { + const rnxKitConfig = require("@rnx-kit/config"); + const fs = require("fs"); + + const mockManifest = { + name: "@rnx-kit/align-deps", + version: "0.0.1", + }; + + const defaultOptions = { loose: false, write: false }; + + beforeEach(() => { + fs.__setMockContent(mockManifest); + rnxKitConfig.__setMockConfig(); + }); + + afterAll(() => { + jest.clearAllMocks(); + }); + + test("returns early if the package is unconfigured", () => { + expect(getCheckConfig("package.json", defaultOptions)).toBe(0); + }); + + test("returns default values given react-native version", () => { + const reactNativeVersion = "^0.64"; + rnxKitConfig.__setMockConfig({ reactNativeVersion }); + + expect(getCheckConfig("package.json", defaultOptions)).toEqual({ + capabilities: [], + kitType: "library", + manifest: mockManifest, + reactNativeVersion, + reactNativeDevVersion: semverCoerce(reactNativeVersion).version, + }); + }); + + test("uses react-native version provided by vigilant flag if unspecified", () => { + const reactNativeVersion = "^0.64"; + rnxKitConfig.__setMockConfig({ customProfiles: "" }); + + expect( + getCheckConfig("package.json", { + ...defaultOptions, + supportedVersions: reactNativeVersion, + targetVersion: reactNativeVersion, + }) + ).toEqual({ + capabilities: [], + kitType: "library", + manifest: mockManifest, + reactNativeVersion, + reactNativeDevVersion: reactNativeVersion, + }); + }); + + test("does not overwrite existing config with the version provided by vigilant flag", () => { + const reactNativeVersion = "^0.64"; + rnxKitConfig.__setMockConfig({ reactNativeVersion }); + + expect( + getCheckConfig("package.json", { + ...defaultOptions, + supportedVersions: "1000.0", + targetVersion: "1000.0", + }) + ).toEqual({ + capabilities: [], + kitType: "library", + manifest: mockManifest, + reactNativeVersion, + reactNativeDevVersion: semverCoerce(reactNativeVersion).version, + }); + }); +}); diff --git a/packages/align-deps/test/dependencies.test.ts b/packages/align-deps/test/dependencies.test.ts new file mode 100644 index 000000000..508fa662b --- /dev/null +++ b/packages/align-deps/test/dependencies.test.ts @@ -0,0 +1,239 @@ +import { PackageManifest, readPackage } from "@rnx-kit/tools-node/package"; +import path from "path"; +import { getRequirements, visitDependencies } from "../src/dependencies"; + +jest.unmock("@rnx-kit/config"); + +function fixturePath(name: string) { + return path.join(process.cwd(), "test", "__fixtures__", name); +} + +function useFixture(name: string): [string, PackageManifest] { + const fixture = fixturePath(name); + return [fixture, readPackage(fixture)]; +} + +describe("visitDependencies()", () => { + const consoleWarnSpy = jest.spyOn(global.console, "warn"); + const currentWorkingDir = process.cwd(); + + beforeEach(() => { + consoleWarnSpy.mockReset(); + }); + + afterEach(() => { + process.chdir(currentWorkingDir); + }); + + afterAll(() => { + jest.clearAllMocks(); + }); + + test("returns if there are no direct dependencies", () => { + const visited = new Set(); + + visitDependencies( + { name: "@rnx-kit/align-deps", version: "1.0.0" }, + process.cwd(), + () => 0, + visited + ); + expect(visited.size).toBe(0); + + const dependencies = { "react-native": "1000.0.0" }; + + visitDependencies( + { + name: "@rnx-kit/align-deps", + version: "1.0.0", + peerDependencies: dependencies, + devDependencies: dependencies, + }, + process.cwd(), + () => 0, + visited + ); + expect(visited.size).toBe(0); + + expect(consoleWarnSpy).not.toBeCalled(); + }); + + test("traverse transitive dependencies", () => { + const fixture = fixturePath("awesome-repo"); + process.chdir(fixture); + + const visited: string[] = []; + visitDependencies( + require(path.join(fixture, "package.json")), + process.cwd(), + (module) => { + visited.push(module); + } + ); + + expect(visited.sort()).toEqual([ + "conan", + "dutch", + "john", + "quaid", + "react", + "react-native", + "t-800", + ]); + + expect(consoleWarnSpy).not.toBeCalled(); + }); + + test("skips unresolved packages", () => { + const visited: string[] = []; + visitDependencies( + { + name: "@rnx-kit/align-deps", + version: "1.0.0", + dependencies: { + "this-does-not-exist": "1.0.0", + }, + }, + process.cwd(), + (module) => visited.push(module) + ); + + expect(visited.length).toBe(0); + expect(consoleWarnSpy).toBeCalledTimes(1); + }); +}); + +describe("getRequirements()", () => { + const consoleErrorSpy = jest.spyOn(global.console, "error"); + const consoleWarnSpy = jest.spyOn(global.console, "warn"); + const defaultOptions = { loose: false }; + + afterEach(() => { + consoleErrorSpy.mockReset(); + consoleWarnSpy.mockReset(); + }); + + test("gets requirements from all dependencies", () => { + const [fixture, manifest] = useFixture("awesome-repo"); + const { reactNativeVersion, capabilities } = getRequirements( + "^0.63 || ^0.64", + "app", + manifest, + fixture, + undefined, + defaultOptions + ); + + expect(reactNativeVersion).toBe("^0.63 || ^0.64"); + + expect(capabilities.sort()).toEqual([ + "animation", + "netinfo", + "storage", + "webview", + ]); + + expect(consoleErrorSpy).not.toBeCalled(); + expect(consoleWarnSpy).not.toBeCalled(); + }); + + test("gets requirements from all dependencies with custom profiles", () => { + const cyberdyne = { name: "cyberdyne", version: "1.0.0", devOnly: true }; + const skynet = { name: "skynet", version: "1.0.0" }; + jest.mock( + "awesome-align-deps-profiles", + () => ({ + "0.63": { + [cyberdyne.name]: cyberdyne, + [skynet.name]: skynet, + }, + "0.64": { + [cyberdyne.name]: cyberdyne, + [skynet.name]: skynet, + }, + }), + { virtual: true } + ); + + const [fixture, manifest] = useFixture("awesome-repo-extended"); + const { reactNativeVersion, capabilities } = getRequirements( + "^0.63 || ^0.64", + "app", + manifest, + fixture, + "awesome-align-deps-profiles", + defaultOptions + ); + + expect(reactNativeVersion).toBe("^0.63 || ^0.64"); + + expect(capabilities.sort()).toEqual([ + "animation", + "netinfo", + "skynet", + "storage", + "webview", + ]); + + expect(consoleErrorSpy).not.toBeCalled(); + expect(consoleWarnSpy).not.toBeCalled(); + }); + + test("throws if no profiles can satisfy required React Native version", () => { + expect(() => + getRequirements( + "0.60.6", + "app", + { + name: "@rnx-kit/align-deps", + version: "1.0.0", + }, + "", + undefined, + defaultOptions + ) + ).toThrow(); + + expect(consoleErrorSpy).not.toBeCalled(); + expect(consoleWarnSpy).not.toBeCalled(); + }); + + test("throws if no profiles can satisfy requirement of dependencies", () => { + const [fixture, manifest] = useFixture("no-profile-satisfying-deps"); + expect(() => + getRequirements( + "^0.64", + "app", + manifest, + fixture, + undefined, + defaultOptions + ) + ).toThrowError("No React Native profile could satisfy all dependencies"); + + expect(consoleErrorSpy).toBeCalledWith( + "error", + expect.stringContaining( + "No React Native profile could satisfy all dependencies" + ) + ); + expect(consoleWarnSpy).not.toBeCalled(); + }); + + test("does not throw if no profiles can satisfy requirement of dependencies in loose mode", () => { + const [fixture, manifest] = useFixture("no-profile-satisfying-deps"); + expect(() => + getRequirements("^0.64", "app", manifest, fixture, undefined, { + loose: true, + }) + ).not.toThrow(); + + expect(consoleErrorSpy).not.toBeCalled(); + expect(consoleWarnSpy).toBeCalledWith( + "warn", + expect.stringContaining( + "No React Native profile could satisfy all dependencies" + ) + ); + }); +}); diff --git a/packages/align-deps/test/findBadPackages.test.ts b/packages/align-deps/test/findBadPackages.test.ts new file mode 100644 index 000000000..d027c691f --- /dev/null +++ b/packages/align-deps/test/findBadPackages.test.ts @@ -0,0 +1,115 @@ +import { findBadPackages } from "../src/findBadPackages"; + +describe("findBadPackages()", () => { + const dependenciesWithOneBadPackage = { + "react-native": "0.0.0", + "react-native-linear-gradient": "0.0.0", + }; + + const dependenciesWithMoreBadPackages = { + "react-native": "0.0.0", + "react-native-linear-gradient": "0.0.0", + "react-native-netinfo": "0.0.0", + }; + + test("finds bad packages in all dependencies", () => { + expect( + findBadPackages({ + name: "Test", + version: "0.0.1", + }) + ).toBeUndefined(); + + expect( + findBadPackages({ + name: "Test", + version: "0.0.1", + dependencies: dependenciesWithOneBadPackage, + })?.length + ).toBe(1); + + expect( + findBadPackages({ + name: "Test", + version: "0.0.1", + peerDependencies: dependenciesWithOneBadPackage, + })?.length + ).toBe(1); + + expect( + findBadPackages({ + name: "Test", + version: "0.0.1", + devDependencies: dependenciesWithOneBadPackage, + })?.length + ).toBe(1); + }); + + test("dedupes bad packages", () => { + expect( + findBadPackages({ + name: "Test", + version: "0.0.1", + dependencies: dependenciesWithOneBadPackage, + peerDependencies: dependenciesWithOneBadPackage, + })?.length + ).toBe(1); + + expect( + findBadPackages({ + name: "Test", + version: "0.0.1", + dependencies: dependenciesWithOneBadPackage, + devDependencies: dependenciesWithOneBadPackage, + })?.length + ).toBe(1); + + expect( + findBadPackages({ + name: "Test", + version: "0.0.1", + peerDependencies: dependenciesWithOneBadPackage, + devDependencies: dependenciesWithOneBadPackage, + })?.length + ).toBe(1); + + expect( + findBadPackages({ + name: "Test", + version: "0.0.1", + dependencies: dependenciesWithOneBadPackage, + peerDependencies: dependenciesWithOneBadPackage, + devDependencies: dependenciesWithOneBadPackage, + })?.length + ).toBe(1); + }); + + test("finds all bad packages", () => { + expect( + findBadPackages({ + name: "Test", + version: "0.0.1", + dependencies: dependenciesWithMoreBadPackages, + })?.length + ).toBe(2); + + expect( + findBadPackages({ + name: "Test", + version: "0.0.1", + dependencies: dependenciesWithOneBadPackage, + peerDependencies: dependenciesWithMoreBadPackages, + })?.length + ).toBe(2); + + expect( + findBadPackages({ + name: "Test", + version: "0.0.1", + dependencies: dependenciesWithOneBadPackage, + peerDependencies: dependenciesWithMoreBadPackages, + devDependencies: dependenciesWithMoreBadPackages, + })?.length + ).toBe(2); + }); +}); diff --git a/packages/align-deps/test/helpers.test.ts b/packages/align-deps/test/helpers.test.ts new file mode 100644 index 000000000..1136897e3 --- /dev/null +++ b/packages/align-deps/test/helpers.test.ts @@ -0,0 +1,17 @@ +import { compare } from "../src/helpers"; + +describe("compare", () => { + test("compares values", () => { + expect(compare(0, 0)).toBe(0); + expect(compare(0, 1)).toBe(-1); + expect(compare(1, 0)).toBe(1); + + expect(compare("dutch", "dutch")).toBe(0); + expect(compare("dutch", "quaid")).toBe(-1); + expect(compare("quaid", "dutch")).toBe(1); + expect(compare("dutch", "dutchess")).toBe(-1); + expect(compare("dutchess", "dutch")).toBe(1); + + expect(compare("hyphen-before-lowbar", "hyphen_before_lowbar")).toBe(-1); + }); +}); diff --git a/packages/align-deps/test/helpers.ts b/packages/align-deps/test/helpers.ts new file mode 100644 index 000000000..cbecacf00 --- /dev/null +++ b/packages/align-deps/test/helpers.ts @@ -0,0 +1,20 @@ +import type { Capability } from "@rnx-kit/config"; +import type { Profile, Package } from "../src/types"; + +export function pickPackage(profile: Profile, capability: string): Package { + const pkg = profile[capability]; + if (!pkg) { + throw new Error(`Could not resolve '${capability}'`); + } else if (!("version" in pkg)) { + throw new Error(`'${capability}' is a meta package`); + } + + return pkg; +} + +export function packageVersion( + profile: Profile, + capability: Capability +): string { + return pickPackage(profile, capability).version; +} diff --git a/packages/align-deps/test/initialize.test.ts b/packages/align-deps/test/initialize.test.ts new file mode 100644 index 000000000..e1c8bd26d --- /dev/null +++ b/packages/align-deps/test/initialize.test.ts @@ -0,0 +1,169 @@ +import { initializeConfig } from "../src/initialize"; + +jest.mock("fs"); + +describe("initializeConfig()", () => { + const fs = require("fs"); + + const bundle = { + entryPath: "src/index.ts", + distPath: "dist", + assetsPath: "dist", + bundlePrefix: "main", + targets: ["ios", "android", "macos", "windows"], + platforms: { + android: { + assetsPath: "dist/res", + }, + }, + }; + + const mockManifest = { + dependencies: { + "react-native": "^0.64.1", + }, + peerDependencies: { + "@react-native-community/netinfo": "^5.9.10", + "react-native-webview": "^10.10.2", + }, + "rnx-kit": { + bundle, + }, + }; + + const mockCapabilities = [ + "core", + "core-android", + "core-ios", + "netinfo", + "webview", + ]; + + beforeEach(() => { + const unset = () => { + throw new Error("unset"); + }; + fs.__setMockContent(unset); + fs.__setMockFileWriter(unset); + }); + + test("returns early if capabilities are declared", () => { + fs.__setMockContent({ "rnx-kit": { capabilities: [] } }); + + let didWrite = false; + fs.__setMockFileWriter(() => { + didWrite = true; + }); + + initializeConfig("package.json", {}); + expect(didWrite).toBe(false); + }); + + test("returns early if no capabilities are found", () => { + fs.__setMockContent({ name: "@rnx-kit/align-deps", version: "1.0.0-test" }); + + let didWrite = false; + fs.__setMockFileWriter(() => { + didWrite = true; + }); + + initializeConfig("package.json", {}); + expect(didWrite).toBe(false); + }); + + test("keeps existing config", () => { + fs.__setMockContent({ + dependencies: { + "react-native": "^0.64.1", + }, + "rnx-kit": { + platformBundle: false, + bundle, + }, + }); + + let content = {}; + fs.__setMockFileWriter((_: string, data: string) => { + content = JSON.parse(data); + }); + + initializeConfig("package.json", {}); + + const kitConfig = content["rnx-kit"]; + if (!kitConfig) { + fail(); + } + + expect(kitConfig["platformBundle"]).toBe(false); + expect(kitConfig["bundle"]).toEqual(bundle); + }); + + test('adds config with type "app"', () => { + fs.__setMockContent(mockManifest); + + let content = {}; + fs.__setMockFileWriter((_: string, data: string) => { + content = JSON.parse(data); + }); + + initializeConfig("package.json", { kitType: "app" }); + + const kitConfig = content["rnx-kit"]; + if (!kitConfig) { + fail(); + } + + expect(kitConfig["bundle"]).toEqual(bundle); + expect(kitConfig["reactNativeVersion"]).toEqual("^0.64"); + expect(kitConfig["reactNativeDevVersion"]).toBeUndefined(); + expect(kitConfig["kitType"]).toEqual("app"); + expect(kitConfig["capabilities"]).toEqual(mockCapabilities); + expect(kitConfig["customProfiles"]).toBeUndefined(); + }); + + test('adds config with type "library"', () => { + fs.__setMockContent(mockManifest); + + let content = {}; + fs.__setMockFileWriter((_: string, data: string) => { + content = JSON.parse(data); + }); + + initializeConfig("package.json", { kitType: "library" }); + + const kitConfig = content["rnx-kit"]; + if (!kitConfig) { + fail(); + } + + expect(kitConfig["bundle"]).toEqual(bundle); + expect(kitConfig["reactNativeVersion"]).toEqual("^0.64"); + expect(kitConfig["reactNativeDevVersion"]).toEqual("0.64.0"); + expect(kitConfig["kitType"]).toEqual("library"); + expect(kitConfig["capabilities"]).toEqual(mockCapabilities); + expect(kitConfig["customProfiles"]).toBeUndefined(); + }); + + test("adds config with custom profiles", () => { + fs.__setMockContent(mockManifest); + + let content = {}; + fs.__setMockFileWriter((_: string, data: string) => { + content = JSON.parse(data); + }); + + initializeConfig("package.json", { + kitType: "library", + customProfilesPath: "@rnx-kit/scripts/rnx-dep-check.js", + }); + + const kitConfig = content["rnx-kit"]; + if (!kitConfig) { + fail(); + } + + expect(kitConfig["customProfiles"]).toEqual( + "@rnx-kit/scripts/rnx-dep-check.js" + ); + }); +}); diff --git a/packages/align-deps/test/manifest.test.ts b/packages/align-deps/test/manifest.test.ts new file mode 100644 index 000000000..36c7adcdc --- /dev/null +++ b/packages/align-deps/test/manifest.test.ts @@ -0,0 +1,312 @@ +import { + removeKeys, + updateDependencies, + updatePackageManifest, +} from "../src/manifest"; +import profile_0_63 from "../src/presets/microsoft/profile-0.63"; +import profile_0_64 from "../src/presets/microsoft/profile-0.64"; +import type { Package } from "../src/types"; +import { packageVersion, pickPackage } from "./helpers"; + +const mockDependencies = { + typescript: "0.0.0", + react: "0.0.0", + "react-native-test-app": "0.0.0", + "react-native": "0.0.0", +}; + +describe("removeKeys()", () => { + test("returns a new copy of object with specified keys removed", () => { + const original = { x: "1", y: "2", z: "3" }; + const originalKeys = Object.keys(original); + const modified = removeKeys(original, ["x", "z"]); + expect(modified).not.toBe(original); + expect(Object.keys(original)).toEqual(originalKeys); + expect(modified).toEqual({ y: original.y }); + }); + + test("returns a new copy of object even if no keys are removed", () => { + const original = { x: "1", y: "2", z: "3" }; + const originalKeys = Object.keys(original); + const modified = removeKeys(original, ["a", "b"]); + expect(modified).not.toBe(original); + expect(Object.keys(original)).toEqual(originalKeys); + expect(modified).toEqual(original); + }); + + test("handles undefined objects", () => { + expect(removeKeys(undefined, ["x", "y"])).toBeUndefined(); + }); +}); + +describe("updateDependencies()", () => { + const resolvedPackages: Record = { + react: [ + pickPackage(profile_0_63, "react"), + pickPackage(profile_0_64, "react"), + ], + "react-native": [ + pickPackage(profile_0_63, "core"), + pickPackage(profile_0_64, "core"), + ], + "react-native-macos": [ + pickPackage(profile_0_63, "core-macos"), + pickPackage(profile_0_64, "core-macos"), + ], + "react-native-test-app": [pickPackage(profile_0_64, "test-app")], + "react-native-windows": [ + pickPackage(profile_0_63, "core-windows"), + pickPackage(profile_0_64, "core-windows"), + ], + }; + + test("bumps dependencies to maximum supported version", () => { + const updated = updateDependencies( + mockDependencies, + resolvedPackages, + "direct" + ); + expect(updated).toEqual({ + react: packageVersion(profile_0_64, "react"), + "react-native": packageVersion(profile_0_64, "core"), + "react-native-macos": packageVersion(profile_0_64, "core-macos"), + "react-native-test-app": "0.0.0", + "react-native-windows": packageVersion(profile_0_64, "core-windows"), + typescript: "0.0.0", + }); + }); + + test("bumps dependencies to minimum supported version", () => { + const updated = updateDependencies( + mockDependencies, + resolvedPackages, + "development" + ); + expect(updated).toEqual({ + react: packageVersion(profile_0_63, "react"), + "react-native": packageVersion(profile_0_63, "core"), + "react-native-macos": packageVersion(profile_0_63, "core-macos"), + "react-native-test-app": packageVersion(profile_0_63, "test-app"), + "react-native-windows": packageVersion(profile_0_63, "core-windows"), + typescript: "0.0.0", + }); + }); + + test("bumps dependencies to widest possible version range", () => { + const updated = updateDependencies( + mockDependencies, + resolvedPackages, + "peer" + ); + expect(updated).toEqual({ + react: `${packageVersion(profile_0_63, "react")} || ${packageVersion( + profile_0_64, + "react" + )}`, + "react-native": `${packageVersion( + profile_0_63, + "core" + )} || ${packageVersion(profile_0_64, "core")}`, + "react-native-macos": `${packageVersion( + profile_0_63, + "core-macos" + )} || ${packageVersion(profile_0_64, "core-macos")}`, + "react-native-test-app": "0.0.0", + "react-native-windows": `${packageVersion( + profile_0_63, + "core-windows" + )} || ${packageVersion(profile_0_64, "core-windows")}`, + typescript: "0.0.0", + }); + }); + + test("sorts keys", () => { + const updated = updateDependencies( + mockDependencies, + resolvedPackages, + "development" + ); + const updatedKeys = Object.keys(updated); + const originalKeys = Object.keys(mockDependencies); + expect(updatedKeys).not.toEqual(originalKeys); + expect(updatedKeys.sort()).not.toEqual(originalKeys); + }); + + test("sets undefined dependencies", () => { + expect( + updateDependencies(undefined, resolvedPackages, "development") + ).toEqual({ + react: packageVersion(profile_0_63, "react"), + "react-native": packageVersion(profile_0_63, "core"), + "react-native-macos": packageVersion(profile_0_63, "core-macos"), + "react-native-test-app": packageVersion(profile_0_63, "test-app"), + "react-native-windows": packageVersion(profile_0_63, "core-windows"), + }); + }); +}); + +describe("updatePackageManifest()", () => { + test("sets direct dependencies for apps", () => { + const { dependencies, devDependencies, peerDependencies } = + updatePackageManifest( + { + name: "Test", + version: "0.0.1", + dependencies: mockDependencies, + peerDependencies: {}, + devDependencies: {}, + }, + ["core-android", "core-ios"], + [profile_0_63, profile_0_64], + [profile_0_64], + "app" + ); + expect(dependencies).toEqual({ + ...mockDependencies, + react: packageVersion(profile_0_64, "react"), + "react-native": packageVersion(profile_0_64, "core"), + }); + expect(peerDependencies).toBeUndefined(); + expect(devDependencies).toBeUndefined(); + }); + + test("removes dependencies from devDependencies for apps", () => { + const { dependencies, devDependencies, peerDependencies } = + updatePackageManifest( + { + name: "Test", + version: "0.0.1", + dependencies: {}, + peerDependencies: {}, + devDependencies: mockDependencies, + }, + ["core-android", "core-ios", "react"], + [profile_0_63, profile_0_64], + [profile_0_64], + "app" + ); + expect(dependencies).toEqual({ + react: packageVersion(profile_0_64, "react"), + "react-native": packageVersion(profile_0_64, "core"), + }); + expect(peerDependencies).toBeUndefined(); + expect(devDependencies).toEqual({ + "react-native-test-app": "0.0.0", + typescript: "0.0.0", + }); + }); + + test("removes dependencies from peerDependencies for apps", () => { + const { dependencies, devDependencies, peerDependencies } = + updatePackageManifest( + { + name: "Test", + version: "0.0.1", + dependencies: {}, + peerDependencies: mockDependencies, + devDependencies: {}, + }, + ["core-android", "core-ios", "react"], + [profile_0_63, profile_0_64], + [profile_0_64], + "app" + ); + expect(dependencies).toEqual({ + react: packageVersion(profile_0_64, "react"), + "react-native": packageVersion(profile_0_64, "core"), + }); + expect(peerDependencies).toEqual({ + "react-native-test-app": "0.0.0", + typescript: "0.0.0", + }); + expect(devDependencies).toBeUndefined(); + }); + + test("sets dev/peer dependencies for libraries", () => { + const { dependencies, devDependencies, peerDependencies } = + updatePackageManifest( + { + name: "Test", + version: "0.0.1", + dependencies: { "@rnx-kit/align-deps": "^1.0.0" }, + peerDependencies: mockDependencies, + }, + ["core-android", "core-ios"], + [profile_0_63, profile_0_64], + [profile_0_64], + "library" + ); + expect(dependencies).toEqual({ "@rnx-kit/align-deps": "^1.0.0" }); + expect(peerDependencies).toEqual({ + ...mockDependencies, + react: [ + packageVersion(profile_0_63, "react"), + packageVersion(profile_0_64, "react"), + ].join(" || "), + "react-native": [ + packageVersion(profile_0_63, "core"), + packageVersion(profile_0_64, "core"), + ].join(" || "), + }); + expect(devDependencies).toEqual({ + react: packageVersion(profile_0_64, "react"), + "react-native": packageVersion(profile_0_64, "core"), + }); + }); + + test("removes dependencies from direct dependencies for libraries", () => { + const { dependencies, devDependencies, peerDependencies } = + updatePackageManifest( + { + name: "Test", + version: "0.0.1", + dependencies: { + "@rnx-kit/align-deps": "^1.0.0", + "react-native": "0.0.0", + }, + peerDependencies: mockDependencies, + devDependencies: {}, + }, + ["core-android", "core-ios"], + [profile_0_64], + [profile_0_64], + "library" + ); + expect(dependencies).toEqual({ "@rnx-kit/align-deps": "^1.0.0" }); + expect(peerDependencies).toEqual({ + ...mockDependencies, + react: packageVersion(profile_0_64, "react"), + "react-native": packageVersion(profile_0_64, "core"), + }); + expect(devDependencies).toEqual({ + react: packageVersion(profile_0_64, "react"), + "react-native": packageVersion(profile_0_64, "core"), + }); + }); + + test("always sets dev-only dependencies", () => { + const { dependencies, devDependencies, peerDependencies } = + updatePackageManifest( + { + name: "Test", + version: "0.0.1", + dependencies: mockDependencies, + peerDependencies: {}, + devDependencies: {}, + }, + ["core-android", "core-ios", "test-app"], + [profile_0_63, profile_0_64], + [profile_0_64], + "app" + ); + expect(dependencies).toEqual({ + ...mockDependencies, + react: packageVersion(profile_0_64, "react"), + "react-native": packageVersion(profile_0_64, "core"), + }); + expect(peerDependencies).toBeUndefined(); + expect(devDependencies).toEqual({ + "react-native-test-app": packageVersion(profile_0_64, "test-app"), + }); + }); +}); diff --git a/packages/align-deps/test/profiles.test.ts b/packages/align-deps/test/profiles.test.ts new file mode 100644 index 000000000..03a493fc1 --- /dev/null +++ b/packages/align-deps/test/profiles.test.ts @@ -0,0 +1,265 @@ +import * as path from "path"; +import semver from "semver"; +import preset from "../src/presets/microsoft"; +import profile_0_62 from "../src/presets/microsoft/profile-0.62"; +import profile_0_63 from "../src/presets/microsoft/profile-0.63"; +import profile_0_64 from "../src/presets/microsoft/profile-0.64"; +import { + getProfilesFor, + getProfileVersionsFor, + loadCustomProfiles, + profilesSatisfying, + resolveCustomProfiles, +} from "../src/profiles"; +import { ProfileVersion } from "../src/types"; + +describe("Microsoft preset", () => { + test("matches react-native versions", () => { + const includePrerelease = { + includePrerelease: true, + }; + Object.entries(preset).forEach(([version, capabilities]) => { + const versionRange = "^" + version; + Object.entries(capabilities).forEach(([capability, pkg]) => { + if (capability === "core") { + expect( + "version" in pkg && + semver.subset(pkg.version, versionRange, includePrerelease) + ).toBe(true); + } + }); + }); + }); +}); + +describe("getProfileVersionsFor()", () => { + test("returns profile versions for specific version", () => { + expect(getProfileVersionsFor("0.61.5")).toEqual(["0.61"]); + expect(getProfileVersionsFor("0.62.2")).toEqual(["0.62"]); + expect(getProfileVersionsFor("0.63.4")).toEqual(["0.63"]); + expect(getProfileVersionsFor("0.64.0")).toEqual(["0.64"]); + }); + + test("returns profile for one version range", () => { + expect(getProfileVersionsFor("^0.62.2")).toEqual(["0.62"]); + expect(getProfileVersionsFor("^0.63.4")).toEqual(["0.63"]); + expect(getProfileVersionsFor("^0.64.0")).toEqual(["0.64"]); + }); + + test("returns profiles for bigger version ranges", () => { + const profiles = getProfileVersionsFor(">=0.66.4"); + expect(profiles).toEqual(["0.66", "0.67", "0.68", "0.69", "0.70"]); + }); + + test("returns profiles for multiple version ranges", () => { + const profiles = getProfileVersionsFor("^0.63.0 || ^0.64.0"); + expect(profiles).toEqual(["0.63", "0.64"]); + }); + + test("returns an empty array when an unsupported version is provided", () => { + expect(getProfileVersionsFor("^0.60.6")).toEqual([]); + }); + + test("throws when an invalid version is provided", () => { + expect(() => getProfileVersionsFor("invalid")).toThrowError( + "Invalid 'react-native' version" + ); + }); +}); + +describe("getProfilesFor()", () => { + const consoleErrorSpy = jest.spyOn(global.console, "error"); + + beforeEach(() => { + consoleErrorSpy.mockReset(); + }); + + afterAll(() => { + jest.clearAllMocks(); + }); + + test("returns profile for specific version", () => { + const profiles = getProfilesFor("0.64.0", undefined); + expect(profiles).toEqual([profile_0_64]); + expect(consoleErrorSpy).not.toBeCalled(); + }); + + test("returns profile for one version range", () => { + expect(getProfilesFor("^0.62.2", undefined)).toEqual([profile_0_62]); + expect(getProfilesFor("^0.63.4", undefined)).toEqual([profile_0_63]); + expect(getProfilesFor("^0.64.0", undefined)).toEqual([profile_0_64]); + }); + + test("returns profiles for bigger version ranges", () => { + const profiles = getProfilesFor(">=0.62.2", undefined); + expect(profiles.slice(0, 3)).toEqual([ + profile_0_62, + profile_0_63, + profile_0_64, + ]); + expect(consoleErrorSpy).not.toBeCalled(); + }); + + test("returns profiles for multiple version ranges", () => { + const profiles = getProfilesFor("^0.63.0 || ^0.64.0", undefined); + expect(profiles).toEqual([profile_0_63, profile_0_64]); + expect(consoleErrorSpy).not.toBeCalled(); + }); + + test("throws when an unsupported version is provided", () => { + expect(() => getProfilesFor("^0.60.6", undefined)).toThrowError( + "Unsupported 'react-native' version" + ); + expect(consoleErrorSpy).not.toBeCalled(); + }); + + test("throws when an invalid version is provided", () => { + expect(() => getProfilesFor("invalid", undefined)).toThrowError( + "Invalid 'react-native' version" + ); + expect(consoleErrorSpy).not.toBeCalled(); + }); + + test("throws if custom profiles module does not exist", () => { + expect(() => + getProfilesFor("^0.59", "non-existent-profiles-module") + ).toThrow(`Cannot find module 'non-existent-profiles-module'`); + }); + + test("throws if custom profiles module does not default export an object", () => { + jest.mock("bad-profiles-module", () => null, { virtual: true }); + + expect(() => getProfilesFor("^0.59", "bad-profiles-module")).toThrow( + "'bad-profiles-module' doesn't default export profiles" + ); + + expect(consoleErrorSpy).toBeCalledTimes(1); + }); + + test("appends custom profiles", () => { + const skynet = { name: "skynet", version: "1.0.0" }; + jest.mock( + "good-profiles-module", + () => ({ "0.62": { [skynet.name]: skynet } }), + { virtual: true } + ); + + const [profile_0_62, profile_0_63] = getProfilesFor( + "^0.62 || ^0.63", + "good-profiles-module" + ); + + expect(skynet.name in profile_0_62).toBe(true); + expect(skynet.name in profile_0_63).toBe(false); + + expect(consoleErrorSpy).not.toBeCalled(); + }); +}); + +describe("loadCustomProfiles()", () => { + test("returns any empty object if no custom profiles are specified", () => { + expect(loadCustomProfiles(undefined)).toEqual({}); + }); + + test("throws if custom profiles are not the right shape", () => { + expect(() => + loadCustomProfiles( + path.join( + __dirname, + "__fixtures__", + "custom-profiles", + "local-profiles.js" + ) + ) + ).toThrow("doesn't default export profiles"); + }); + + test("loads valid custom profiles", () => { + expect( + loadCustomProfiles( + path.join( + __dirname, + "__fixtures__", + "custom-profiles", + "valid-profiles.js" + ) + ) + ).toMatchSnapshot(); + }); + + test("prepends root-level capabilities to all profiles", () => { + expect( + loadCustomProfiles( + path.join( + __dirname, + "__fixtures__", + "custom-profiles", + "root-level-profiles.js" + ) + ) + ).toMatchSnapshot(); + }); +}); + +describe("profilesSatisfying()", () => { + const profileVersions: ProfileVersion[] = ["0.61", "0.62", "0.63", "0.64"]; + + test("returns an empty array when no profiles can satisfy requirements", () => { + expect(profilesSatisfying([], "^0.63.0 || ^0.64.0")).toEqual([]); + expect(profilesSatisfying(["0.61"], "^0.63.0 || ^0.64.0")).toEqual([]); + }); + + test("returns all profiles satisfying version", () => { + expect(profilesSatisfying(profileVersions, "0.63.2")).toEqual(["0.63"]); + expect(profilesSatisfying(profileVersions, "0.64.0-rc1")).toEqual(["0.64"]); + expect(profilesSatisfying(profileVersions, "0.64.0")).toEqual(["0.64"]); + }); + + test("returns all profiles satisfying version range", () => { + expect( + profilesSatisfying(profileVersions, "^0.63.0-0 || ^0.64.0-0") + ).toEqual(["0.63", "0.64"]); + expect(profilesSatisfying(profileVersions, "^0.63.0 || ^0.64.0")).toEqual([ + "0.63", + "0.64", + ]); + }); +}); + +describe("resolveCustomProfiles()", () => { + const projectRoot = `${__dirname}/__fixtures__/custom-profiles`; + + test("handles undefined and empty strings", () => { + expect(resolveCustomProfiles(projectRoot, undefined)).toBeUndefined(); + expect(resolveCustomProfiles(projectRoot, "")).toBeUndefined(); + }); + + test("throws if the module cannot be resolved", () => { + expect(() => + resolveCustomProfiles(projectRoot, "non-existent-custom-profiles") + ).toThrow("Cannot resolve module"); + }); + + test("returns absolute path for module ids", () => { + const profilesPkg = "custom-profiles-package"; + expect(resolveCustomProfiles(projectRoot, profilesPkg)).toEqual( + expect.stringContaining( + path.join( + "__fixtures__", + "custom-profiles", + "node_modules", + profilesPkg, + "index.js" + ) + ) + ); + }); + + test("returns absolute path for relative paths", () => { + expect(resolveCustomProfiles(projectRoot, "./local-profiles.js")).toEqual( + expect.stringContaining( + path.join("__fixtures__", "custom-profiles", "local-profiles.js") + ) + ); + }); +}); diff --git a/packages/align-deps/test/setVersion.test.ts b/packages/align-deps/test/setVersion.test.ts new file mode 100644 index 000000000..3d9e1c1d9 --- /dev/null +++ b/packages/align-deps/test/setVersion.test.ts @@ -0,0 +1,204 @@ +import type { PackageManifest } from "@rnx-kit/tools-node/package"; +import prompts from "prompts"; +import { makeSetVersionCommand } from "../src/setVersion"; + +jest.mock("fs"); + +type Result = { + didWrite: boolean; + manifest: Record; +}; + +describe("makeSetVersionCommand()", () => { + const rnxKitConfig = require("@rnx-kit/config"); + const fs = require("fs"); + + function setupMocks(manifest: PackageManifest): Result { + fs.__setMockContent(manifest); + + const result: Result = { didWrite: false, manifest: {} }; + fs.__setMockFileWriter((_: string, content: string) => { + const updatedManifest = JSON.parse(content); + fs.__setMockContent(updatedManifest); + rnxKitConfig.__setMockConfig(updatedManifest["rnx-kit"]); + + result.didWrite = true; + result.manifest = updatedManifest; + }); + + return result; + } + + const mockManifest = { + name: "@rnx-kit/align-deps", + version: "1.0.0-test", + dependencies: { + react: "16.13.1", + "react-native": "^0.63.2", + }, + devDependencies: {}, + "rnx-kit": { + reactNativeVersion: "^0.63", + kitType: "app", + capabilities: ["core"], + }, + }; + + afterEach(() => { + fs.__setMockContent({}); + rnxKitConfig.__setMockConfig(); + jest.clearAllMocks(); + }); + + test("rejects unsupported versions `react-native`", async () => { + expect(makeSetVersionCommand("0.59")).rejects.toEqual( + expect.objectContaining({ + message: expect.stringContaining( + "Unsupported 'react-native' version/range:" + ), + }) + ); + }); + + test("updates dependencies", async () => { + const result = setupMocks({ + ...mockManifest, + "rnx-kit": { + ...mockManifest["rnx-kit"], + kitType: "library", + reactNativeDevVersion: "^0.63.0", + }, + }); + + const command = await makeSetVersionCommand("0.64,0.63"); + expect(typeof command).toBe("function"); + expect(command("package.json")).toBe(0); + expect(result.manifest).toEqual({ + ...mockManifest, + dependencies: undefined, + devDependencies: { + react: "17.0.1", + "react-native": "^0.64.2", + }, + peerDependencies: { + react: "16.13.1 || 17.0.1", + "react-native": "^0.63.2 || ^0.64.2", + }, + "rnx-kit": { + ...mockManifest["rnx-kit"], + kitType: "library", + reactNativeVersion: "^0.63.0 || ^0.64.0", + reactNativeDevVersion: "^0.64.0", + }, + }); + }); + + test("removes `reactNativeDevVersion` if `kitType` is `app`", async () => { + const result = setupMocks({ + ...mockManifest, + "rnx-kit": { + ...mockManifest["rnx-kit"], + reactNativeDevVersion: "^0.63.0", + }, + }); + + const command = await makeSetVersionCommand("0.64,0.63"); + expect(typeof command).toBe("function"); + expect(command("package.json")).toBe(0); + expect(result.manifest).toEqual({ + ...mockManifest, + dependencies: { + react: "17.0.1", + "react-native": "^0.64.2", + }, + devDependencies: undefined, + "rnx-kit": { + ...mockManifest["rnx-kit"], + reactNativeVersion: "^0.63.0 || ^0.64.0", + }, + }); + }); + + test("prompts the user if no version is specified", async () => { + const result = setupMocks(mockManifest); + + prompts.inject([["0.63", "0.64"], "0.64"]); + + const command = await makeSetVersionCommand(""); + expect(typeof command).toBe("function"); + expect(command("package.json")).toBe(0); + expect(result.manifest).toEqual({ + ...mockManifest, + dependencies: { + react: "17.0.1", + "react-native": "^0.64.2", + }, + devDependencies: undefined, + "rnx-kit": { + ...mockManifest["rnx-kit"], + reactNativeVersion: "^0.63 || ^0.64", + }, + }); + }); + + test("skips the second prompt if only one version is supported", async () => { + const result = setupMocks(mockManifest); + + prompts.inject([["0.64"]]); + + const command = await makeSetVersionCommand(""); + expect(typeof command).toBe("function"); + expect(command("package.json")).toBe(0); + expect(result.manifest).toEqual({ + ...mockManifest, + dependencies: { + react: "17.0.1", + "react-native": "^0.64.2", + }, + devDependencies: undefined, + "rnx-kit": { + ...mockManifest["rnx-kit"], + reactNativeVersion: "^0.64", + }, + }); + }); + + test('skips "dirty" packages', async () => { + rnxKitConfig.__setMockConfig(mockManifest["rnx-kit"]); + const result = setupMocks({ + ...mockManifest, + dependencies: { + "react-native": "^0.62.3", + }, + }); + + prompts.inject([["0.64"]]); + + const command = await makeSetVersionCommand(""); + expect(typeof command).toBe("function"); + expect(command("package.json")).not.toBe(0); + expect(result.didWrite).toBe(false); + }); + + test("skips unconfigured packages", async () => { + const result = setupMocks({ + ...mockManifest, + "rnx-kit": undefined, + } as PackageManifest); + + prompts.inject([["0.64"]]); + + const command = await makeSetVersionCommand(""); + expect(typeof command).toBe("function"); + expect(command("package.json")).toBe(0); + expect(result.didWrite).toBe(false); + }); + + test("exits if the user cancels during prompts", async () => { + prompts.inject([undefined]); + expect(await makeSetVersionCommand("")).toBeUndefined(); + + prompts.inject([["0.63", "0.64"], undefined]); + expect(await makeSetVersionCommand("")).toBeUndefined(); + }); +}); diff --git a/packages/align-deps/test/vigilant.test.ts b/packages/align-deps/test/vigilant.test.ts new file mode 100644 index 000000000..f7c73b501 --- /dev/null +++ b/packages/align-deps/test/vigilant.test.ts @@ -0,0 +1,477 @@ +import { parseProfilesString } from "../src/profiles"; +import { + buildManifestProfile, + buildProfileFromConfig, + inspect, + makeVigilantCommand, +} from "../src/vigilant"; + +jest.mock("fs"); + +describe("buildManifestProfile()", () => { + const testVersion = "1.0.0-test"; + + test("builds a package manifest for a single profile version", () => { + const profiles = parseProfilesString("0.64", undefined); + const profile = buildManifestProfile(profiles); + profile.version = testVersion; + expect(profile).toMatchSnapshot(); + }); + + test("builds a package manifest for multiple profile versions", () => { + const profiles = parseProfilesString("0.64,0.63", undefined); + const profile = buildManifestProfile(profiles); + profile.version = testVersion; + expect(profile).toMatchSnapshot(); + }); + + test("includes devOnly packages under `dependencies`", () => { + const profiles = parseProfilesString("0.64", undefined); + const { dependencies, devDependencies, peerDependencies } = + buildManifestProfile(profiles); + + expect("react-native-test-app" in dependencies).toBe(true); + expect("react-native-test-app" in peerDependencies).toBe(false); + expect("react-native-test-app" in devDependencies).toBe(true); + }); + + test("includes custom profiles", () => { + const skynet = { name: "skynet", version: "1.0.0" }; + jest.mock( + "vigilant-custom-profiles", + () => ({ "0.64": { [skynet.name]: skynet } }), + { virtual: true } + ); + + const profiles = parseProfilesString("0.64", "vigilant-custom-profiles"); + const { dependencies, devDependencies, peerDependencies } = + buildManifestProfile(profiles); + + expect(skynet.name in dependencies).toBe(true); + expect(skynet.name in peerDependencies).toBe(true); + expect(skynet.name in devDependencies).toBe(true); + }); + + test("throws when no profiles match the requested versions", () => { + expect(() => parseProfilesString("0.59", undefined)).toThrow(); + expect(() => parseProfilesString("0.59,0.64", undefined)).toThrow(); + }); +}); + +describe("buildProfileFromConfig()", () => { + const profiles = parseProfilesString("0.64", undefined); + const defaultProfile = buildManifestProfile(profiles); + + test("returns default profile if there is no config", () => { + expect(buildProfileFromConfig(0, defaultProfile)).toBe(defaultProfile); + }); + + test("filters out managed capabilities", () => { + const dependencies = [ + "dependencies", + "peerDependencies", + "devDependencies", + ] as const; + const config = { + kitType: "library" as const, + reactNativeVersion: "0.64", + reactNativeDevVersion: "0.64", + capabilities: [], + manifest: { + name: "@rnx-kit/align-deps", + version: "1.0.0", + dependencies: { + "react-native": "^0.64.0", + }, + }, + }; + const profile = buildProfileFromConfig(config, defaultProfile); + dependencies.forEach((section) => { + expect(Object.keys(profile[section])).toContain("react"); + expect(Object.keys(profile[section])).toContain("react-native"); + }); + + const withCapabilities = buildProfileFromConfig( + { + ...config, + capabilities: ["core-android", "core-ios"], + }, + defaultProfile + ); + + dependencies.forEach((section) => { + expect(Object.keys(withCapabilities[section])).not.toContain("react"); + expect(Object.keys(withCapabilities[section])).not.toContain( + "react-native" + ); + }); + }); +}); + +describe("inspect()", () => { + const mockManifestProfile = { + name: "@rnx-kit/align-deps", + version: "1.0.0", + dependencies: { + "react-native": "^0.63.2", + }, + peerDependencies: { + "react-native": "^0.63 || ^0.64", + }, + devDependencies: { + "react-native": "^0.63.2", + }, + }; + + test("handles empty dependencies", () => { + const manifest = { + name: "@rnx-kit/align-deps", + version: "1.0.0", + }; + expect(inspect(manifest, mockManifestProfile, false)).toEqual([]); + expect(inspect(manifest, mockManifestProfile, true)).toEqual([]); + }); + + test("ignores unmanaged dependencies", () => { + const dependencies = { + "@babel/core": "^7.0.0", + "react-native": "0.63.2", + }; + const manifest = { + name: "@rnx-kit/align-deps", + version: "1.0.0", + dependencies, + }; + + const expectedChanges = [ + { + name: "react-native", + from: manifest.dependencies["react-native"], + to: mockManifestProfile.dependencies["react-native"], + section: "dependencies", + }, + ]; + + expect(inspect(manifest, mockManifestProfile, false)).toEqual( + expectedChanges + ); + expect(manifest.dependencies).toEqual(dependencies); + }); + + test("inspects all dependency types", () => { + const manifest = { + name: "@rnx-kit/align-deps", + version: "1.0.0", + dependencies: { + "@babel/core": "^7.0.0", + }, + peerDependencies: { + "react-native": "0.63.2", + }, + devDependencies: { + "react-native": "0.63.2", + }, + }; + + const expectedChanges = [ + { + name: "react-native", + from: manifest.peerDependencies["react-native"], + to: mockManifestProfile.peerDependencies["react-native"], + section: "peerDependencies", + }, + { + name: "react-native", + from: manifest.devDependencies["react-native"], + to: mockManifestProfile.devDependencies["react-native"], + section: "devDependencies", + }, + ]; + + expect(inspect(manifest, mockManifestProfile, false)).toEqual( + expectedChanges + ); + }); + + test("modifies the manifest when `write: true`", () => { + const dependencies = { + "@babel/core": "^7.0.0", + "react-native": "0.63.2", + }; + const manifest = { + name: "@rnx-kit/align-deps", + version: "1.0.0", + dependencies: { ...dependencies }, + }; + + const expectedChanges = [ + { + name: "react-native", + from: manifest.dependencies["react-native"], + to: mockManifestProfile.dependencies["react-native"], + section: "dependencies", + }, + ]; + + expect(inspect(manifest, mockManifestProfile, true)).toEqual( + expectedChanges + ); + expect(manifest.dependencies).not.toEqual(dependencies); + }); + + test("does not rewrite peerDependencies if superset", () => { + const manifest = { + name: "@rnx-kit/align-deps", + version: "1.0.0", + dependencies: { + react: "^16.8.1", + }, + peerDependencies: { + metro: "*", + react: ">=16.8.0 <18.0.0", + "react-native": ">=0.64", + }, + devDependencies: {}, + }; + const profile = { + name: "@rnx-kit/align-deps", + version: "1.0.0", + dependencies: { + react: "~17.0.1", + }, + peerDependencies: { + metro: "^0.66.2", + react: "~17.0.1", + "react-native": "^0.66.0-0", + }, + devDependencies: {}, + }; + const expectedChanges = [ + { + name: "react", + from: manifest.dependencies["react"], + to: profile.dependencies["react"], + section: "dependencies", + }, + ]; + + expect(inspect(manifest, profile, false)).toEqual(expectedChanges); + }); +}); + +describe("makeVigilantCommand()", () => { + const rnxKitConfig = require("@rnx-kit/config"); + const fs = require("fs"); + + const consoleErrorSpy = jest.spyOn(global.console, "error"); + + beforeEach(() => { + consoleErrorSpy.mockReset(); + fs.__setMockContent({}); + fs.__setMockFileWriter(() => { + throw new Error("mock for fs.writeFileSync is not set"); + }); + }); + + afterAll(() => { + jest.clearAllMocks(); + }); + + test("returns no command if no versions are specified", () => { + expect( + makeVigilantCommand({ versions: "", loose: false, write: false }) + ).toBeUndefined(); + }); + + test("returns exit code 0 when there are no violations", () => { + fs.__setMockContent({ + name: "@rnx-kit/align-deps", + version: "1.0.0", + dependencies: { + "react-native": "^0.63.2", + }, + }); + + let didWrite = false; + fs.__setMockFileWriter(() => { + didWrite = true; + }); + + const result = makeVigilantCommand({ + versions: "0.63", + loose: false, + write: false, + })("package.json"); + expect(result).toBe(0); + expect(didWrite).toBe(false); + expect(consoleErrorSpy).not.toBeCalled(); + }); + + test("returns non-zero exit code when there are violations", () => { + fs.__setMockContent({ + name: "@rnx-kit/align-deps", + version: "1.0.0", + dependencies: { + "react-native": "0.63.2", + }, + }); + + let didWrite = false; + fs.__setMockFileWriter(() => { + didWrite = true; + }); + + const result = makeVigilantCommand({ + versions: "0.63", + loose: false, + write: false, + })("package.json"); + expect(result).not.toBe(0); + expect(didWrite).toBe(false); + expect(consoleErrorSpy).toBeCalledTimes(1); + }); + + test("returns exit code 0 when writing", () => { + fs.__setMockContent({ + name: "@rnx-kit/align-deps", + version: "1.0.0", + dependencies: { + "react-native": "0.63.2", + }, + }); + + let didWrite = false; + fs.__setMockFileWriter(() => { + didWrite = true; + }); + + const result = makeVigilantCommand({ + versions: "0.63", + loose: false, + write: true, + })("package.json"); + expect(result).toBe(0); + expect(didWrite).toBe(true); + expect(consoleErrorSpy).not.toBeCalled(); + }); + + test("excludes specified packages", () => { + fs.__setMockContent({ + name: "@rnx-kit/align-deps", + version: "1.0.0", + dependencies: { + "react-native": "0.59.10", + }, + }); + + let didWrite = false; + fs.__setMockFileWriter(() => { + didWrite = true; + }); + + const result = makeVigilantCommand({ + versions: "0.63", + write: false, + excludePackages: "@rnx-kit/align-deps", + loose: false, + })("package.json"); + expect(result).toBe(0); + expect(didWrite).toBe(false); + expect(consoleErrorSpy).not.toBeCalled(); + }); + + test("uses package-specific custom profiles", () => { + const fixture = `${__dirname}/__fixtures__/config-custom-profiles-only`; + const kitConfig = { + customProfiles: `${fixture}/packageSpecificProfiles.js`, + }; + const inputManifest = { + name: "@rnx-kit/align-deps", + version: "1.0.0", + peerDependencies: { + react: "17.0.1", + "react-native": "0.64.0", + }, + devDependencies: { + react: "17.0.1", + "react-native": "0.64.0", + }, + "rnx-kit": kitConfig, + }; + + rnxKitConfig.__setMockConfig(kitConfig); + fs.__setMockContent(inputManifest); + + let manifest = undefined; + fs.__setMockFileWriter((_, content) => { + manifest = JSON.parse(content); + }); + + const result = makeVigilantCommand({ + versions: "0.64,0.65", + loose: false, + write: true, + })("package.json"); + expect(result).toBe(0); + expect(consoleErrorSpy).not.toBeCalled(); + expect(manifest).toEqual({ + ...inputManifest, + devDependencies: { + react: "17.0.2", + "react-native": "0.64.3", + }, + peerDependencies: { + react: "17.0.2", + "react-native": "0.64.3 || 0.65.2 || ^0.64.2 || ^0.65.0", + }, + }); + }); + + test("prefers package-specific React Native versions", () => { + const fixture = `${__dirname}/__fixtures__/config-custom-profiles-only`; + const kitConfig = { + reactNativeVersion: "0.64", + customProfiles: `${fixture}/packageSpecificProfiles.js`, + }; + const inputManifest = { + name: "@rnx-kit/align-deps", + version: "1.0.0", + peerDependencies: { + react: "17.0.1", + "react-native": "0.64.0", + }, + devDependencies: { + react: "17.0.1", + "react-native": "0.64.0", + }, + "rnx-kit": kitConfig, + }; + + rnxKitConfig.__setMockConfig(kitConfig); + fs.__setMockContent(inputManifest); + + let manifest = undefined; + fs.__setMockFileWriter((_, content) => { + manifest = JSON.parse(content); + }); + + const result = makeVigilantCommand({ + versions: "0.64,0.65", + loose: false, + write: true, + })("package.json"); + expect(result).toBe(0); + expect(consoleErrorSpy).not.toBeCalled(); + expect(manifest).toEqual({ + ...inputManifest, + devDependencies: { + react: "17.0.2", + "react-native": "0.64.3", + }, + peerDependencies: { + react: "17.0.2", + "react-native": "0.64.3 || ^0.64.2", + }, + }); + }); +}); diff --git a/packages/align-deps/tsconfig.json b/packages/align-deps/tsconfig.json new file mode 100644 index 000000000..d93518184 --- /dev/null +++ b/packages/align-deps/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "@rnx-kit/scripts/tsconfig-shared.json", + "compilerOptions": { + "alwaysStrict": false + }, + "include": ["src"] +}