From c9c8236d18afc2f2a2f163b78d9e6c62ce98b806 Mon Sep 17 00:00:00 2001 From: Tommy Nguyen <4123478+tido64@users.noreply.github.com> Date: Fri, 21 Oct 2022 09:33:09 +0200 Subject: [PATCH] refactor(align-deps): migrate set-version command (#1948) --- .changeset/mean-bats-tie.md | 2 + .../align-deps/scripts/update-profile.mjs | 2 +- packages/align-deps/scripts/update-readme.js | 4 +- packages/align-deps/src/cli.ts | 22 +- packages/align-deps/src/commands/check.ts | 18 +- .../align-deps/src/commands/initialize.ts | 2 +- .../align-deps/src/commands/setVersion.ts | 168 +++++++++ packages/align-deps/src/commands/vigilant.ts | 8 +- .../align-deps/src/compatibility/config.ts | 20 +- packages/align-deps/src/config.ts | 8 +- packages/align-deps/src/dependencies.ts | 2 +- packages/align-deps/src/errors.ts | 13 +- packages/align-deps/src/helpers.ts | 39 +- packages/align-deps/src/index.ts | 4 +- packages/align-deps/src/preset.ts | 49 ++- .../src/presets/microsoft/react-native.ts | 4 +- packages/align-deps/src/profiles.ts | 177 --------- packages/align-deps/src/setVersion.ts | 95 ----- packages/align-deps/src/types.ts | 54 +-- .../test/__snapshots__/profiles.test.ts.snap | 112 ------ .../test/__snapshots__/vigilant.test.ts.snap | 2 - packages/align-deps/test/capabilities.test.ts | 40 +- packages/align-deps/test/helpers.test.ts | 30 +- packages/align-deps/test/preset.test.ts | 24 +- packages/align-deps/test/profiles.test.ts | 197 ---------- packages/align-deps/test/setVersion.test.ts | 343 ++++++++++++++---- packages/align-deps/test/vigilant.test.ts | 18 +- 27 files changed, 655 insertions(+), 802 deletions(-) create mode 100644 .changeset/mean-bats-tie.md create mode 100644 packages/align-deps/src/commands/setVersion.ts delete mode 100644 packages/align-deps/src/profiles.ts delete mode 100644 packages/align-deps/src/setVersion.ts delete mode 100644 packages/align-deps/test/__snapshots__/profiles.test.ts.snap delete mode 100644 packages/align-deps/test/profiles.test.ts diff --git a/.changeset/mean-bats-tie.md b/.changeset/mean-bats-tie.md new file mode 100644 index 000000000..a845151cc --- /dev/null +++ b/.changeset/mean-bats-tie.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/packages/align-deps/scripts/update-profile.mjs b/packages/align-deps/scripts/update-profile.mjs index 4ad6ce4bd..cc6e93c50 100755 --- a/packages/align-deps/scripts/update-profile.mjs +++ b/packages/align-deps/scripts/update-profile.mjs @@ -286,7 +286,7 @@ async function main({ force, }) { const { preset } = await import(`../lib/presets/${presetName}.js`); - const allVersions = /** @type {import("../src/types").ProfileVersion[]} */ ( + const allVersions = /** @type {string[]} */ ( Object.keys(preset) .sort((lhs, rhs) => semverCompare(semverCoerce(lhs), semverCoerce(rhs))) .reverse() diff --git a/packages/align-deps/scripts/update-readme.js b/packages/align-deps/scripts/update-readme.js index 3a5a16ea3..2ce1b7001 100755 --- a/packages/align-deps/scripts/update-readme.js +++ b/packages/align-deps/scripts/update-readme.js @@ -40,9 +40,7 @@ function sortCoreFirst(lhs, rhs) { return lhs < rhs ? -1 : 1; } -const allVersions = /** @type {import("../src/types").ProfileVersion[]} */ ( - Object.keys(preset).reverse() -); +const allVersions = /** @type {string[]} */ (Object.keys(preset).reverse()); const allCapabilities = /** @type {import("@rnx-kit/config").Capability[]} */ ( Object.keys(preset[allVersions[0]]).sort(sortCoreFirst) ); diff --git a/packages/align-deps/src/cli.ts b/packages/align-deps/src/cli.ts index aca0b38fd..2368954ea 100644 --- a/packages/align-deps/src/cli.ts +++ b/packages/align-deps/src/cli.ts @@ -7,11 +7,13 @@ import { findWorkspacePackages, findWorkspaceRoot, } from "@rnx-kit/tools-workspaces"; +import isString from "lodash/isString"; import * as path from "path"; import { makeCheckCommand } from "./commands/check"; import { makeInitializeCommand } from "./commands/initialize"; +import { makeSetVersionCommand } from "./commands/setVersion"; import { defaultConfig } from "./config"; -import { printError } from "./errors"; +import { printError, printInfo } from "./errors"; import type { Args, Command } from "./types"; async function getManifests( @@ -85,6 +87,7 @@ async function makeCommand(args: Args): Promise { loose, presets, requirements, + "set-version": setVersion, write, } = args; @@ -100,6 +103,13 @@ async function makeCommand(args: Args): Promise { return makeInitializeCommand(init, options); } + // 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, options); + } + return makeCheckCommand(options); } @@ -141,6 +151,10 @@ export async function cli({ packages, ...args }: Args): Promise { }, 0); process.exitCode = errors; + + if (errors > 0) { + printInfo(); + } } if (require.main === module) { @@ -178,6 +192,12 @@ if (require.main === module) { type: "string", requiresArg: true, }, + "set-version": { + description: + "Sets `react-native` requirements 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.70,0.69`", + type: "string", + conflicts: ["init", "requirements"], + }, write: { default: false, description: "Writes changes to the specified 'package.json'.", diff --git a/packages/align-deps/src/commands/check.ts b/packages/align-deps/src/commands/check.ts index ca6ab41e0..c9cf6593d 100644 --- a/packages/align-deps/src/commands/check.ts +++ b/packages/align-deps/src/commands/check.ts @@ -4,21 +4,17 @@ import chalk from "chalk"; import { diffLinesUnified } from "jest-diff"; import * as path from "path"; import { migrateConfig } from "../compatibility/config"; -import { getConfig } from "../config"; -import { modifyManifest } from "../helpers"; +import { loadConfig } from "../config"; +import { isError, modifyManifest } from "../helpers"; import { updatePackageManifest } from "../manifest"; import { resolve } from "../preset"; import type { Command, ErrorCode, Options } from "../types"; import { checkPackageManifestUnconfigured } from "./vigilant"; -function isError(config: ReturnType): config is ErrorCode { - return typeof config === "string"; -} - export function checkPackageManifest( manifestPath: string, options: Options, - inputConfig = getConfig(manifestPath) + inputConfig = loadConfig(manifestPath) ): ErrorCode { if (isError(inputConfig)) { return inputConfig; @@ -43,10 +39,10 @@ export function checkPackageManifest( ); } else { info( - "Aligning your library's dependencies according to the following profiles:" + "Aligning your library's dependencies according to the following profiles:\n" + + `\t- Development: ${Object.keys(devPreset).join(", ")}\n` + + `\t- Production: ${Object.keys(prodPreset).join(", ")}` ); - info("\t- Development:", Object.keys(devPreset).join(", ")); - info("\t- Production:", Object.keys(prodPreset).join(", ")); } const updatedManifest = updatePackageManifest( @@ -90,7 +86,7 @@ export function makeCheckCommand(options: Options): Command { } return (manifest: string) => { - const inputConfig = getConfig(manifest); + const inputConfig = loadConfig(manifest); const config = isError(inputConfig) ? inputConfig : migrateConfig(inputConfig); diff --git a/packages/align-deps/src/commands/initialize.ts b/packages/align-deps/src/commands/initialize.ts index 4a0f80950..bc9da2178 100644 --- a/packages/align-deps/src/commands/initialize.ts +++ b/packages/align-deps/src/commands/initialize.ts @@ -61,7 +61,7 @@ export function initializeConfig( const requirements = [ `react-native@${dropPatchFromVersion(targetReactNativeVersion)}`, ]; - const preset = filterPreset(requirements, mergePresets(presets, projectRoot)); + const preset = filterPreset(mergePresets(presets, projectRoot), requirements); return { ...manifest, diff --git a/packages/align-deps/src/commands/setVersion.ts b/packages/align-deps/src/commands/setVersion.ts new file mode 100644 index 000000000..7278a9f49 --- /dev/null +++ b/packages/align-deps/src/commands/setVersion.ts @@ -0,0 +1,168 @@ +import type { PackageManifest } from "@rnx-kit/tools-node/package"; +import isString from "lodash/isString"; +import prompts from "prompts"; +import semverCoerce from "semver/functions/coerce"; +import { transformConfig } from "../compatibility/config"; +import { defaultConfig, loadConfig } from "../config"; +import { isError, keysOf, modifyManifest } from "../helpers"; +import defaultPreset from "../presets/microsoft/react-native"; +import type { + AlignDepsConfig, + Command, + LegacyCheckConfig, + Options, +} from "../types"; +import { checkPackageManifest } from "./check"; + +function parseVersions(versions: string): string[] { + return versions.split(",").map((v) => { + const coerced = semverCoerce(v.trim()); + if (!coerced) { + throw new Error(`'${v}' is not a valid version number`); + } + + const parsedVersion = `${coerced.major}.${coerced.minor}`; + if (!(parsedVersion in defaultPreset)) { + throw new Error( + `'${parsedVersion}' is not a supported react-native version` + ); + } + + return parsedVersion; + }); +} + +function toChoice(version: string): prompts.Choice { + return { title: version, value: version }; +} + +async function parseInput(versions: string | number): Promise<{ + supportedVersions: string[]; + targetVersion: string; +} | null> { + // 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) { + const supportedVersions = parseVersions(versions); + const targetVersion = supportedVersions[0]; + return { supportedVersions: supportedVersions.sort(), targetVersion }; + } + + const { supportedVersions } = await prompts({ + type: "multiselect", + name: "supportedVersions", + message: "Select all supported versions of `react-native`", + choices: keysOf(defaultPreset).map(toChoice), + min: 1, + }); + if (!Array.isArray(supportedVersions)) { + return null; + } + + const targetVersion = + supportedVersions.length === 1 + ? supportedVersions[0] + : ( + await prompts({ + type: "select", + name: "targetVersion", + message: "Select development version of `react-native`", + choices: supportedVersions.map(toChoice), + }) + ).targetVersion; + if (!supportedVersions.includes(targetVersion)) { + return null; + } + + return { supportedVersions, targetVersion }; +} + +function setRequirement(requirements: string[], versionRange: string): void { + const prefix = "react-native@"; + const index = requirements.findIndex((r: string) => r.startsWith(prefix)); + if (index >= 0) { + requirements[index] = prefix + versionRange; + } +} + +function updateRequirements( + { requirements }: AlignDepsConfig["alignDeps"], + prodVersion: string, + devVersion = prodVersion +): void { + if (Array.isArray(requirements)) { + setRequirement(requirements, prodVersion); + } else { + setRequirement(requirements.production, prodVersion); + setRequirement(requirements.development, devVersion); + } +} + +function setVersion( + config: AlignDepsConfig | LegacyCheckConfig, + targetVersion: string, + supportedVersions: string[] +): PackageManifest { + const { kitType, manifest } = config; + const alignDeps = + "alignDeps" in config + ? config.alignDeps + : transformConfig(config).alignDeps; + + if (kitType === "app") { + updateRequirements(alignDeps, targetVersion); + } else { + updateRequirements( + alignDeps, + supportedVersions.join(" || "), + targetVersion + ); + } + + manifest["rnx-kit"] = { + ...manifest["rnx-kit"], + kitType, + alignDeps: { + ...alignDeps, + presets: + // The default presets were added with `loadConfig`. We need to remove + // it here to not add new fields to the config. + alignDeps.presets === defaultConfig.presets + ? undefined + : alignDeps.presets, + }, + }; + + return config.manifest; +} + +export async function makeSetVersionCommand( + versions: string | number, + options: Options +): Promise { + const input = await parseInput(versions); + if (!input) { + return undefined; + } + + const { supportedVersions, targetVersion } = input; + const checkOnly = { ...options, loose: false, write: false }; + const write = { ...options, loose: false, write: true }; + + return (manifestPath: string) => { + const config = loadConfig(manifestPath); + if (isError(config)) { + return config; + } + + const checkResult = checkPackageManifest(manifestPath, checkOnly, config); + if (checkResult !== "success") { + return checkResult; + } + + const result = setVersion(config, targetVersion, supportedVersions); + modifyManifest(manifestPath, result); + return checkPackageManifest(manifestPath, write); + }; +} diff --git a/packages/align-deps/src/commands/vigilant.ts b/packages/align-deps/src/commands/vigilant.ts index b503e29b9..9afc7447e 100644 --- a/packages/align-deps/src/commands/vigilant.ts +++ b/packages/align-deps/src/commands/vigilant.ts @@ -64,14 +64,14 @@ export function buildManifestProfile( const [targetPreset, supportPreset] = (() => { const { requirements } = alignDeps; if (Array.isArray(requirements)) { - const preset = filterPreset(requirements, mergedPresets); + const preset = filterPreset(mergedPresets, requirements); return [preset, preset]; } - const prodPreset = filterPreset(requirements.production, mergedPresets); + const prodPreset = filterPreset(mergedPresets, requirements.production); return kitType === "app" ? [prodPreset, prodPreset] - : [filterPreset(requirements.development, mergedPresets), prodPreset]; + : [filterPreset(mergedPresets, requirements.development), prodPreset]; })(); const unmanagedCapabilities = getAllCapabilities(targetPreset).filter( @@ -93,8 +93,6 @@ export function buildManifestProfile( ); return { - name: "@rnx-kit/align-deps/vigilant-preset", - version: "1.0.0", dependencies: directDependencies, peerDependencies, devDependencies: directDependencies, diff --git a/packages/align-deps/src/compatibility/config.ts b/packages/align-deps/src/compatibility/config.ts index 258140fd7..462b98ba9 100644 --- a/packages/align-deps/src/compatibility/config.ts +++ b/packages/align-deps/src/compatibility/config.ts @@ -1,7 +1,8 @@ import type { KitConfig } from "@rnx-kit/config"; import { warn } from "@rnx-kit/console"; +import { defaultConfig } from "../config"; import { dropPatchFromVersion } from "../helpers"; -import type { AlignDepsConfig, CheckConfig } from "../types"; +import type { AlignDepsConfig, LegacyCheckConfig } from "../types"; function oldConfigKeys(config: KitConfig): (keyof KitConfig)[] { const oldKeys = [ @@ -24,25 +25,22 @@ function oldConfigKeys(config: KitConfig): (keyof KitConfig)[] { */ export function transformConfig({ capabilities, - customProfilesPath, + customProfiles, kitType, manifest, reactNativeDevVersion, reactNativeVersion, -}: CheckConfig): AlignDepsConfig { +}: LegacyCheckConfig): AlignDepsConfig { const devVersion = dropPatchFromVersion( reactNativeDevVersion || reactNativeVersion ); - const prodVersion = dropPatchFromVersion( - reactNativeVersion || reactNativeDevVersion - ); + const prodVersion = dropPatchFromVersion(reactNativeVersion); return { kitType, alignDeps: { - presets: [ - "microsoft/react-native", - ...(customProfilesPath ? [customProfilesPath] : []), - ], + presets: customProfiles + ? [...defaultConfig.presets, customProfiles] + : defaultConfig.presets, requirements: kitType === "app" ? [`react-native@${reactNativeVersion}`] @@ -57,7 +55,7 @@ export function transformConfig({ } export function migrateConfig( - config: AlignDepsConfig | CheckConfig + config: AlignDepsConfig | LegacyCheckConfig ): AlignDepsConfig { if ("alignDeps" in config) { const oldKeys = oldConfigKeys(config); diff --git a/packages/align-deps/src/config.ts b/packages/align-deps/src/config.ts index a37a1d2c2..67278518f 100644 --- a/packages/align-deps/src/config.ts +++ b/packages/align-deps/src/config.ts @@ -4,9 +4,9 @@ import { error, warn } from "@rnx-kit/console"; import { isPackageManifest, readPackage } from "@rnx-kit/tools-node/package"; import * as path from "path"; import { findBadPackages } from "./findBadPackages"; -import type { AlignDepsConfig, CheckConfig, ErrorCode } from "./types"; +import type { AlignDepsConfig, LegacyCheckConfig, ErrorCode } from "./types"; -type ConfigResult = AlignDepsConfig | CheckConfig | ErrorCode; +type ConfigResult = AlignDepsConfig | LegacyCheckConfig | ErrorCode; export const defaultConfig: AlignDepsConfig["alignDeps"] = { presets: ["microsoft/react-native"], @@ -36,7 +36,7 @@ export function containsValidRequirements( return false; } -export function getConfig(manifestPath: string): ConfigResult { +export function loadConfig(manifestPath: string): ConfigResult { const manifest = readPackage(manifestPath); if (!isPackageManifest(manifest)) { return "invalid-manifest"; @@ -98,5 +98,5 @@ export function getConfig(manifestPath: string): ConfigResult { capabilities, customProfiles, manifest, - } as CheckConfig; + }; } diff --git a/packages/align-deps/src/dependencies.ts b/packages/align-deps/src/dependencies.ts index 1d48b2652..0ed3048f1 100644 --- a/packages/align-deps/src/dependencies.ts +++ b/packages/align-deps/src/dependencies.ts @@ -107,7 +107,7 @@ export function gatherRequirements( } } - const filteredPreset = filterPreset(requirements, preset); + const filteredPreset = filterPreset(preset, requirements); const filteredNames = Object.keys(filteredPreset); if (filteredNames.length !== trace[trace.length - 1].profiles.length) { trace.push({ diff --git a/packages/align-deps/src/errors.ts b/packages/align-deps/src/errors.ts index 60cada8ef..8fe31fb46 100644 --- a/packages/align-deps/src/errors.ts +++ b/packages/align-deps/src/errors.ts @@ -3,11 +3,6 @@ import chalk from "chalk"; import * as path from "path"; import type { ErrorCode } from "./types"; -function printURL(): void { - const url = chalk.bold("https://aka.ms/align-deps"); - info(`Visit ${url} for more information about align-deps.`); -} - export function printError(manifestPath: string, code: ErrorCode): void { const currentPackageJson = path.relative(process.cwd(), manifestPath); @@ -17,7 +12,6 @@ export function printError(manifestPath: string, code: ErrorCode): void { case "invalid-configuration": error(`${currentPackageJson}: align-deps was not properly configured`); - printURL(); break; case "invalid-manifest": @@ -36,14 +30,17 @@ export function printError(manifestPath: string, code: ErrorCode): void { case "not-configured": error(`${currentPackageJson}: align-deps was not configured`); - printURL(); break; case "unsatisfied": error( `${currentPackageJson}: Changes are needed to satisfy all requirements. Re-run with '--write' to apply them.` ); - printURL(); break; } } + +export function printInfo(): void { + const url = chalk.bold("https://aka.ms/align-deps"); + info(`Visit ${url} for more information about align-deps.`); +} diff --git a/packages/align-deps/src/helpers.ts b/packages/align-deps/src/helpers.ts index ffa02315c..7cb57f3ea 100644 --- a/packages/align-deps/src/helpers.ts +++ b/packages/align-deps/src/helpers.ts @@ -2,7 +2,8 @@ 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"; -import semverCoerce from "semver/functions/coerce"; +import semverValidRange from "semver/ranges/valid"; +import type { ErrorCode } from "./types"; export function compare(lhs: T, rhs: T): -1 | 0 | 1 { if (lhs === rhs) { @@ -14,23 +15,41 @@ export function compare(lhs: T, rhs: T): -1 | 0 | 1 { } } -export function concatVersionRanges(versions: string[]): string { - return "^" + versions.join(" || ^"); -} - export function dropPatchFromVersion(version: string): string { return version .split("||") - .map((v) => { - const coerced = semverCoerce(v); - if (!coerced) { - throw new Error(`Invalid version number: ${v}`); + .map((input) => { + const versionRange = input.trim(); + if (!semverValidRange(versionRange)) { + throw new Error(`Invalid version number: ${versionRange}`); + } + + if (!versionRange) { + return "*"; } - return `${coerced.major}.${coerced.minor}`; + + return versionRange + .split(" ") + .map((v) => { + if (v === "*" || v === "-") { + // No need to manipulate `*` or hyphen ranges, e.g. `1.0 - 2.0` + return v; + } + + const [major, minor = "0"] = v.split("."); + return major === "^0" || major === "~0" + ? `0.${minor}` + : `${major}.${minor}`; + }) + .join(" "); }) .join(" || "); } +export function isError(config: T | ErrorCode): config is ErrorCode { + return typeof config === "string"; +} + export function keysOf>(obj: T): (keyof T)[] { return Object.keys(obj); } diff --git a/packages/align-deps/src/index.ts b/packages/align-deps/src/index.ts index 6df465616..42fc99070 100644 --- a/packages/align-deps/src/index.ts +++ b/packages/align-deps/src/index.ts @@ -11,7 +11,6 @@ export { checkPackageManifestUnconfigured } from "./commands/vigilant"; export { updatePackageManifest } from "./manifest"; export type { Args, - CapabilitiesOptions, Command, DependencyType, ExcludedPackage, @@ -19,7 +18,6 @@ export type { MetaPackage, Options, Package, + Preset, Profile, - ProfilesInfo, - ProfileVersion, } from "./types"; diff --git a/packages/align-deps/src/preset.ts b/packages/align-deps/src/preset.ts index 2bd9539ff..dea781a9d 100644 --- a/packages/align-deps/src/preset.ts +++ b/packages/align-deps/src/preset.ts @@ -49,15 +49,25 @@ function ensurePreset(preset: Preset, requirements: string[]): void { } } -function loadPreset(preset: string, projectRoot: string): Preset { +function loadPreset( + preset: string, + projectRoot: string, + resolve = require.resolve +): Preset { try { return require("./presets/" + preset).default; } catch (_) { - return require(require.resolve(preset, { paths: [projectRoot] })); + return require(resolve(preset, { paths: [projectRoot] })); } } -export function filterPreset(requirements: string[], preset: Preset): Preset { +/** + * Filters out any profiles that do not satisfy the specified requirements. + * @param preset The preset to filter + * @param requirements The requirements that a profile must satisfy + * @returns Preset with only profiles that satisfy the requirements + */ +export function filterPreset(preset: Preset, requirements: string[]): Preset { const filteredPreset: Preset = {}; const reqs = compileRequirements(requirements); for (const [profileName, profile] of Object.entries(preset)) { @@ -74,10 +84,26 @@ export function filterPreset(requirements: string[], preset: Preset): Preset { return filteredPreset; } -export function mergePresets(presets: string[], projectRoot: string): Preset { +/** + * Loads and merges specified presets. + * + * The order of presets is significant. The profiles from each preset are merged + * when the names overlap. If there are overlaps within the profiles, i.e. when + * multiple profiles declare the same capability, the last profile wins. This + * allows users to both extend and override profiles as needed. + * + * @param presets The presets to load and merge + * @param projectRoot The project root from which presets should be resolved + * @returns Merged preset + */ +export function mergePresets( + presets: string[], + projectRoot: string, + resolve = require.resolve +): Preset { const mergedPreset: Preset = {}; for (const presetName of presets) { - const preset = loadPreset(presetName, projectRoot); + const preset = loadPreset(presetName, projectRoot, resolve); for (const [profileName, profile] of Object.entries(preset)) { mergedPreset[profileName] = { ...mergedPreset[profileName], @@ -89,6 +115,15 @@ export function mergePresets(presets: string[], projectRoot: string): Preset { return mergedPreset; } +/** + * Loads specified presets and filters them according to the requirements. The + * list of capabilities are also gathered from transitive dependencies if + * `kitType` is `app`. + * @param config User input config + * @param projectRoot Root of the project we're currently scanniing + * @param options + * @returns The resolved presets and capabilities + */ export function resolve( { kitType, alignDeps, manifest }: AlignDepsConfig, projectRoot: string, @@ -100,7 +135,7 @@ export function resolve( ? requirements : requirements.production; const mergedPreset = mergePresets(presets, projectRoot); - const initialProdPreset = filterPreset(prodRequirements, mergedPreset); + const initialProdPreset = filterPreset(mergedPreset, prodRequirements); ensurePreset(initialProdPreset, prodRequirements); const devPreset = (() => { @@ -111,7 +146,7 @@ export function resolve( return initialProdPreset; } else { const devRequirements = requirements.development; - const devPreset = filterPreset(devRequirements, mergedPreset); + const devPreset = filterPreset(mergedPreset, devRequirements); ensurePreset(devPreset, devRequirements); return devPreset; } diff --git a/packages/align-deps/src/presets/microsoft/react-native.ts b/packages/align-deps/src/presets/microsoft/react-native.ts index 0588680d6..a7e94fc0a 100644 --- a/packages/align-deps/src/presets/microsoft/react-native.ts +++ b/packages/align-deps/src/presets/microsoft/react-native.ts @@ -1,4 +1,4 @@ -import type { ProfileMap } from "../../types"; +import type { Preset } from "../../types"; import profile_0_61 from "./react-native/profile-0.61"; import profile_0_62 from "./react-native/profile-0.62"; import profile_0_63 from "./react-native/profile-0.63"; @@ -13,7 +13,7 @@ import profile_0_70 from "./react-native/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 = { +export const preset: Readonly = { "0.61": profile_0_61, "0.62": profile_0_62, "0.63": profile_0_63, diff --git a/packages/align-deps/src/profiles.ts b/packages/align-deps/src/profiles.ts deleted file mode 100644 index 5dc52bd89..000000000 --- a/packages/align-deps/src/profiles.ts +++ /dev/null @@ -1,177 +0,0 @@ -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/react-native"; -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, - }; -} diff --git a/packages/align-deps/src/setVersion.ts b/packages/align-deps/src/setVersion.ts deleted file mode 100644 index 9d2682692..000000000 --- a/packages/align-deps/src/setVersion.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { readPackage } from "@rnx-kit/tools-node/package"; -import isString from "lodash/isString"; -import prompts from "prompts"; -import { checkPackageManifest } from "./commands/check"; -import { defaultConfig } from "./config"; -import { concatVersionRanges, keysOf, modifyManifest } from "./helpers"; -import { default as defaultPreset } from "./presets/microsoft/react-native"; -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 = { - presets: defaultConfig.presets, - loose: false, - write: false, - }; - const write = { presets: defaultConfig.presets, loose: false, write: true }; - - return (manifestPath: string) => { - const checkReturnCode = checkPackageManifest(manifestPath, checkOnly); - if (checkReturnCode !== "success") { - return checkReturnCode; - } - - const manifest = readPackage(manifestPath); - const rnxKitConfig = manifest["rnx-kit"]; - if (!rnxKitConfig) { - return "not-configured"; - } - - 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 index e02a8f219..54d83f3ee 100644 --- a/packages/align-deps/src/types.ts +++ b/packages/align-deps/src/types.ts @@ -24,14 +24,7 @@ export type Args = Pick & { requirements?: string | number; }; -export type CheckConfig = { - kitType: KitType; - reactNativeVersion: string; - reactNativeDevVersion: string; - capabilities: Capability[]; - customProfilesPath?: string; - manifest: PackageManifest; -}; +export type DependencyType = "direct" | "development" | "peer"; export type ErrorCode = | "success" @@ -41,14 +34,12 @@ export type ErrorCode = | "not-configured" | "unsatisfied"; -export type CapabilitiesOptions = { - kitType?: KitType; - customProfilesPath?: string; -}; - export type Command = (manifest: string) => ErrorCode; -export type DependencyType = "direct" | "development" | "peer"; +export type ManifestProfile = Pick< + Required, + "dependencies" | "devDependencies" | "peerDependencies" +>; export type MetaPackage = { name: "#meta"; @@ -63,36 +54,19 @@ export type Package = { 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 Preset = Record; -export type ProfileMap = Record; - -export type ProfilesInfo = { - supportedProfiles: Profile[]; - supportedVersions: string; - targetProfile: Profile[]; - targetVersion: string; -}; export type ExcludedPackage = Package & { reason: string; }; + +export type LegacyCheckConfig = { + kitType: KitType; + reactNativeVersion: string; + reactNativeDevVersion?: string; + capabilities: Capability[]; + customProfiles?: string; + manifest: PackageManifest; +}; diff --git a/packages/align-deps/test/__snapshots__/profiles.test.ts.snap b/packages/align-deps/test/__snapshots__/profiles.test.ts.snap deleted file mode 100644 index f1575a48f..000000000 --- a/packages/align-deps/test/__snapshots__/profiles.test.ts.snap +++ /dev/null @@ -1,112 +0,0 @@ -// 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 index 65219e9a8..5c6393eac 100644 --- a/packages/align-deps/test/__snapshots__/vigilant.test.ts.snap +++ b/packages/align-deps/test/__snapshots__/vigilant.test.ts.snap @@ -86,7 +86,6 @@ Object { "react-native-windows": "^0.70.0", "react-test-renderer": "18.1.0", }, - "name": "@rnx-kit/align-deps/vigilant-preset", "peerDependencies": Object { "@react-native-async-storage/async-storage": "^1.17.10", "@react-native-clipboard/clipboard": "^1.10.0", @@ -209,7 +208,6 @@ Object { "react-native-windows": "^0.70.0", "react-test-renderer": "18.1.0", }, - "name": "@rnx-kit/align-deps/vigilant-preset", "peerDependencies": Object { "@react-native-async-storage/async-storage": "^1.17.7 || ^1.17.10", "@react-native-clipboard/clipboard": "^1.10.0", diff --git a/packages/align-deps/test/capabilities.test.ts b/packages/align-deps/test/capabilities.test.ts index 3cb13b80f..5570d44f1 100644 --- a/packages/align-deps/test/capabilities.test.ts +++ b/packages/align-deps/test/capabilities.test.ts @@ -1,12 +1,16 @@ import type { Capability } from "@rnx-kit/config"; import { capabilitiesFor, resolveCapabilities } from "../src/capabilities"; +import { filterPreset, mergePresets } from "../src/preset"; import defaultPreset from "../src/presets/microsoft/react-native"; import profile_0_62 from "../src/presets/microsoft/react-native/profile-0.62"; import profile_0_63 from "../src/presets/microsoft/react-native/profile-0.63"; import profile_0_64 from "../src/presets/microsoft/react-native/profile-0.64"; -import { getProfilesFor } from "../src/profiles"; import { pickPackage } from "./helpers"; +function makeMockResolver(module: string): RequireResolve { + return (() => module) as unknown as RequireResolve; +} + describe("capabilitiesFor()", () => { test("returns an empty array when there are no dependencies", () => { expect( @@ -152,14 +156,18 @@ describe("resolveCapabilities()", () => { { virtual: true } ); - const profiles = getProfilesFor( - "^0.62 || ^0.63 || ^0.64", - "mock-custom-profiles-module" + const preset = filterPreset( + mergePresets( + ["microsoft/react-native", "mock-custom-profiles-module"], + process.cwd(), + makeMockResolver("mock-custom-profiles-module") + ), + ["react-native@0.62 || 0.63 || 0.64"] ); const packages = resolveCapabilities( ["skynet" as Capability, "svg"], - profiles + Object.values(preset) ); const { name } = profile_0_64["svg"]; @@ -212,9 +220,18 @@ describe("resolveCapabilities()", () => { { virtual: true } ); + const preset = filterPreset( + mergePresets( + ["microsoft/react-native", "mock-meta-package"], + process.cwd(), + makeMockResolver("mock-meta-package") + ), + ["react-native@0.64"] + ); + const packages = resolveCapabilities( ["core/all" as Capability], - getProfilesFor("^0.64", "mock-meta-package") + Object.values(preset) ); expect(packages).toEqual({ @@ -247,9 +264,18 @@ describe("resolveCapabilities()", () => { { virtual: true } ); + const preset = filterPreset( + mergePresets( + ["microsoft/react-native", "mock-meta-package-loop"], + process.cwd(), + makeMockResolver("mock-meta-package-loop") + ), + ["react-native@0.64"] + ); + const packages = resolveCapabilities( ["reese" as Capability], - getProfilesFor("^0.64", "mock-meta-package-loop") + Object.values(preset) ); expect(packages).toEqual({ diff --git a/packages/align-deps/test/helpers.test.ts b/packages/align-deps/test/helpers.test.ts index 1136897e3..8eec5a625 100644 --- a/packages/align-deps/test/helpers.test.ts +++ b/packages/align-deps/test/helpers.test.ts @@ -1,6 +1,6 @@ -import { compare } from "../src/helpers"; +import { compare, dropPatchFromVersion } from "../src/helpers"; -describe("compare", () => { +describe("compare()", () => { test("compares values", () => { expect(compare(0, 0)).toBe(0); expect(compare(0, 1)).toBe(-1); @@ -15,3 +15,29 @@ describe("compare", () => { expect(compare("hyphen-before-lowbar", "hyphen_before_lowbar")).toBe(-1); }); }); + +describe("dropPatchFromVersion()", () => { + [ + ["1.2.3-rc.1", "1.2"], + ["1.2.3", "1.2"], + ["1.2", "1.2"], + ["1", "1.0"], + [">0.68.0 <=0.70.0", ">0.68 <=0.70"], + ["0.68 - 0.70.2", "0.68 - 0.70"], + ["0.68.1 - 0.70.2", "0.68 - 0.70"], + ["1.0.1 - 3.0.3", "1.0 - 3.0"], + ["", "*"], + ["*", "*"], + ["1.X", "1.X"], + ["1.x", "1.x"], + ["1.2.X", "1.2"], + ["1.2.x", "1.2"], + ["~0.68.1 || ^0.69.2 || >=0.70.0", "0.68 || 0.69 || >=0.70"], + ["~1.0.1 || ^2.0.2 || >=3.0.3", "~1.0 || ^2.0 || >=3.0"], + ["1.x || >=2.5.0 || 5.0.0 - 7.2.3", "1.x || >=2.5 || 5.0 - 7.2"], + ].forEach(([input, expected]) => { + test(`drops patch number in '${input}'`, () => { + expect(dropPatchFromVersion(input)).toBe(expected); + }); + }); +}); diff --git a/packages/align-deps/test/preset.test.ts b/packages/align-deps/test/preset.test.ts index 9ee2b4669..8e4f09ae1 100644 --- a/packages/align-deps/test/preset.test.ts +++ b/packages/align-deps/test/preset.test.ts @@ -6,25 +6,25 @@ import profile_0_70 from "../src/presets/microsoft/react-native/profile-0.70"; describe("filterPreset()", () => { test("returns no profiles if requirements cannot be satisfied", () => { - const profiles = filterPreset( - ["react@17.0", "react-native@>=69.0"], - preset - ); + const profiles = filterPreset(preset, [ + "react@17.0", + "react-native@>=69.0", + ]); expect(profiles).toEqual({}); }); test("returns profiles satisfying single version range", () => { - const profiles = filterPreset(["react-native@0.70"], preset); + const profiles = filterPreset(preset, ["react-native@0.70"]); expect(profiles).toEqual({ "0.70": profile_0_70 }); }); test("returns profiles satisfying multiple version ranges", () => { - const profiles = filterPreset(["react-native@0.68 || 0.70"], preset); + const profiles = filterPreset(preset, ["react-native@0.68 || 0.70"]); expect(profiles).toEqual({ "0.68": profile_0_68, "0.70": profile_0_70 }); }); test("returns profiles satisfying wide version range", () => { - const profiles = filterPreset(["react-native@>=0.68"], preset); + const profiles = filterPreset(preset, ["react-native@>=0.68"]); expect(profiles).toEqual({ "0.68": profile_0_68, "0.69": profile_0_69, @@ -33,7 +33,7 @@ describe("filterPreset()", () => { }); test("returns profiles satisfying non-react-native requirements", () => { - const profiles = filterPreset(["react@18"], preset); + const profiles = filterPreset(preset, ["react@18"]); expect(profiles).toEqual({ "0.69": profile_0_69, "0.70": profile_0_70, @@ -41,10 +41,10 @@ describe("filterPreset()", () => { }); test("returns profiles satisfying multiple requirements", () => { - const profiles = filterPreset( - ["react@^18.0", "react-native@>=0.64"], - preset - ); + const profiles = filterPreset(preset, [ + "react@^18.0", + "react-native@>=0.64", + ]); expect(profiles).toEqual({ "0.69": profile_0_69, "0.70": profile_0_70, diff --git a/packages/align-deps/test/profiles.test.ts b/packages/align-deps/test/profiles.test.ts deleted file mode 100644 index 5aa16aee0..000000000 --- a/packages/align-deps/test/profiles.test.ts +++ /dev/null @@ -1,197 +0,0 @@ -import * as path from "path"; -import semver from "semver"; -import preset from "../src/presets/microsoft/react-native"; -import profile_0_62 from "../src/presets/microsoft/react-native/profile-0.62"; -import profile_0_63 from "../src/presets/microsoft/react-native/profile-0.63"; -import profile_0_64 from "../src/presets/microsoft/react-native/profile-0.64"; -import { - getProfilesFor, - getProfileVersionsFor, - loadCustomProfiles, -} from "../src/profiles"; - -describe("microsoft/react-native 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(); - }); -}); diff --git a/packages/align-deps/test/setVersion.test.ts b/packages/align-deps/test/setVersion.test.ts index f0fdb0db4..4fd3da10a 100644 --- a/packages/align-deps/test/setVersion.test.ts +++ b/packages/align-deps/test/setVersion.test.ts @@ -1,6 +1,7 @@ import type { PackageManifest } from "@rnx-kit/tools-node/package"; import prompts from "prompts"; -import { makeSetVersionCommand } from "../src/setVersion"; +import { makeSetVersionCommand } from "../src/commands/setVersion"; +import { defaultConfig } from "../src/config"; jest.mock("fs"); @@ -9,12 +10,13 @@ type Result = { manifest: Record; }; -xdescribe("makeSetVersionCommand()", () => { +describe("makeSetVersionCommand()", () => { const rnxKitConfig = require("@rnx-kit/config"); const fs = require("fs"); function setupMocks(manifest: PackageManifest): Result { fs.__setMockContent(manifest); + rnxKitConfig.__setMockConfig(manifest["rnx-kit"]); const result: Result = { didWrite: false, manifest: {} }; fs.__setMockFileWriter((_: string, content: string) => { @@ -29,19 +31,10 @@ xdescribe("makeSetVersionCommand()", () => { 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"], - }, + const options = { + presets: defaultConfig.presets, + loose: false, + write: false, }; afterEach(() => { @@ -50,155 +43,347 @@ xdescribe("makeSetVersionCommand()", () => { jest.clearAllMocks(); }); - test("rejects unsupported versions `react-native`", async () => { - expect(makeSetVersionCommand("0.59")).rejects.toEqual( + test("rejects invalid version numbers", async () => { + expect(makeSetVersionCommand("latest", options)).rejects.toEqual( expect.objectContaining({ message: expect.stringContaining( - "Unsupported 'react-native' version/range:" + "'latest' is not a valid version number" ), }) ); }); - test("updates dependencies", async () => { + test("rejects unsupported `react-native` versions", async () => { + expect(makeSetVersionCommand("0.59", options)).rejects.toEqual( + expect.objectContaining({ + message: expect.stringContaining( + "'0.59' is not a supported react-native version" + ), + }) + ); + }); + + test("skips unconfigured packages", async () => { const result = setupMocks({ - ...mockManifest, + name: "@rnx-kit/align-deps", + version: "1.0.0-test", + dependencies: { + react: "16.13.1", + "react-native": "^0.63.2", + }, + }); + + prompts.inject([["0.64"]]); + + const command = await makeSetVersionCommand("", options); + if (typeof command !== "function") { + fail(); + } + + expect(command("package.json")).toBe("not-configured"); + expect(result.didWrite).toBe(false); + }); + + test("skips partially configured packages", async () => { + const result = setupMocks({ + name: "@rnx-kit/align-deps", + version: "1.0.0-test", + dependencies: { + react: "16.13.1", + "react-native": "^0.63.2", + }, + "rnx-kit": { + kitType: "app", + alignDeps: { + presets: ["custom"], + }, + }, + }); + + prompts.inject([["0.64"]]); + + const command = await makeSetVersionCommand("", options); + if (typeof command !== "function") { + fail(); + } + + expect(command("package.json")).toBe("invalid-configuration"); + expect(result.didWrite).toBe(false); + }); + + test("updates `react-native` requirements", async () => { + const result = setupMocks({ + name: "@rnx-kit/align-deps", + version: "1.0.0-test", + peerDependencies: { + react: "16.13.1", + "react-native": "^0.63.2", + }, + devDependencies: { + react: "16.13.1", + "react-native": "^0.63.2", + }, "rnx-kit": { - ...mockManifest["rnx-kit"], kitType: "library", - reactNativeDevVersion: "^0.63.0", + alignDeps: { + requirements: { + development: ["react-native@0.63"], + production: ["react-native@0.63"], + }, + capabilities: ["core"], + }, }, }); - const command = await makeSetVersionCommand("0.64,0.63"); - expect(typeof command).toBe("function"); - expect(command("package.json")).toBe(0); + const command = await makeSetVersionCommand("0.64,0.63", options); + if (typeof command !== "function") { + fail(); + } + + expect(command("package.json")).toBe("success"); expect(result.manifest).toEqual({ - ...mockManifest, - dependencies: undefined, + name: "@rnx-kit/align-deps", + version: "1.0.0-test", + peerDependencies: { + react: "16.13.1 || 17.0.1", + "react-native": "^0.63.2 || ^0.64.2", + }, devDependencies: { react: "17.0.1", "react-native": "^0.64.2", }, + "rnx-kit": { + kitType: "library", + alignDeps: { + requirements: { + development: ["react-native@0.64"], + production: ["react-native@0.63 || 0.64"], + }, + capabilities: ["core"], + }, + }, + }); + }); + + test("updates `react-native` requirements (backwards compatibility)", async () => { + const result = setupMocks({ + name: "@rnx-kit/align-deps", + version: "1.0.0-test", + peerDependencies: { + react: "16.13.1", + "react-native": "^0.63.2", + }, + devDependencies: { + react: "16.13.1", + "react-native": "^0.63.2", + }, + "rnx-kit": { + kitType: "library", + reactNativeVersion: "0.63", + reactNativeDevVersion: "0.63", + capabilities: ["core"], + }, + }); + + const command = await makeSetVersionCommand("0.64,0.63", options); + if (typeof command !== "function") { + fail(); + } + + expect(command("package.json")).toBe("success"); + expect(result.manifest).toEqual({ + name: "@rnx-kit/align-deps", + version: "1.0.0-test", peerDependencies: { react: "16.13.1 || 17.0.1", "react-native": "^0.63.2 || ^0.64.2", }, + devDependencies: { + react: "17.0.1", + "react-native": "^0.64.2", + }, "rnx-kit": { - ...mockManifest["rnx-kit"], kitType: "library", - reactNativeVersion: "^0.63.0 || ^0.64.0", - reactNativeDevVersion: "^0.64.0", + reactNativeVersion: "0.63", + reactNativeDevVersion: "0.63", + capabilities: ["core"], + alignDeps: { + requirements: { + development: ["react-native@0.64"], + production: ["react-native@0.63 || 0.64"], + }, + capabilities: ["core"], + }, }, }); }); - test("removes `reactNativeDevVersion` if `kitType` is `app`", async () => { + test("only modifies the 'react-native' requirement", async () => { const result = setupMocks({ - ...mockManifest, + name: "@rnx-kit/align-deps", + version: "1.0.0-test", + dependencies: { + react: "16.13.1", + "react-native": "^0.63.2", + }, "rnx-kit": { - ...mockManifest["rnx-kit"], - reactNativeDevVersion: "^0.63.0", + kitType: "app", + alignDeps: { + requirements: { + development: ["react@>=16", "react-native@0.63"], + production: ["react@>=16", "react-native@0.63"], + }, + capabilities: ["core"], + }, }, }); - const command = await makeSetVersionCommand("0.64,0.63"); - expect(typeof command).toBe("function"); - expect(command("package.json")).toBe(0); + const command = await makeSetVersionCommand("0.64,0.63", options); + if (typeof command !== "function") { + fail(); + } + + expect(command("package.json")).toBe("success"); expect(result.manifest).toEqual({ - ...mockManifest, + name: "@rnx-kit/align-deps", + version: "1.0.0-test", dependencies: { react: "17.0.1", "react-native": "^0.64.2", }, - devDependencies: undefined, "rnx-kit": { - ...mockManifest["rnx-kit"], - reactNativeVersion: "^0.63.0 || ^0.64.0", + kitType: "app", + alignDeps: { + requirements: { + development: ["react@>=16", "react-native@0.64"], + production: ["react@>=16", "react-native@0.64"], + }, + capabilities: ["core"], + }, }, }); }); test("prompts the user if no version is specified", async () => { - const result = setupMocks(mockManifest); + const result = setupMocks({ + name: "@rnx-kit/align-deps", + version: "1.0.0-test", + dependencies: { + react: "16.13.1", + "react-native": "^0.63.2", + }, + "rnx-kit": { + kitType: "app", + alignDeps: { + requirements: ["react-native@0.63"], + capabilities: ["core"], + }, + }, + }); prompts.inject([["0.63", "0.64"], "0.64"]); - const command = await makeSetVersionCommand(""); - expect(typeof command).toBe("function"); - expect(command("package.json")).toBe(0); + const command = await makeSetVersionCommand("", options); + if (typeof command !== "function") { + fail(); + } + + expect(command("package.json")).toBe("success"); expect(result.manifest).toEqual({ - ...mockManifest, + name: "@rnx-kit/align-deps", + version: "1.0.0-test", dependencies: { react: "17.0.1", "react-native": "^0.64.2", }, - devDependencies: undefined, "rnx-kit": { - ...mockManifest["rnx-kit"], - reactNativeVersion: "^0.63 || ^0.64", + kitType: "app", + alignDeps: { + requirements: ["react-native@0.64"], + capabilities: ["core"], + }, }, }); }); test("skips the second prompt if only one version is supported", async () => { - const result = setupMocks(mockManifest); + const result = setupMocks({ + name: "@rnx-kit/align-deps", + version: "1.0.0-test", + dependencies: { + react: "16.13.1", + "react-native": "^0.63.2", + }, + "rnx-kit": { + kitType: "app", + alignDeps: { + requirements: ["react-native@0.63"], + capabilities: ["core"], + }, + }, + }); prompts.inject([["0.64"]]); - const command = await makeSetVersionCommand(""); - expect(typeof command).toBe("function"); - expect(command("package.json")).toBe(0); + const command = await makeSetVersionCommand("", options); + if (typeof command !== "function") { + fail(); + } + + expect(command("package.json")).toBe("success"); expect(result.manifest).toEqual({ - ...mockManifest, + name: "@rnx-kit/align-deps", + version: "1.0.0-test", dependencies: { react: "17.0.1", "react-native": "^0.64.2", }, - devDependencies: undefined, "rnx-kit": { - ...mockManifest["rnx-kit"], - reactNativeVersion: "^0.64", + kitType: "app", + alignDeps: { + requirements: ["react-native@0.64"], + capabilities: ["core"], + }, }, }); }); test('skips "dirty" packages', async () => { - rnxKitConfig.__setMockConfig(mockManifest["rnx-kit"]); - const result = setupMocks({ - ...mockManifest, + const mockManifest = { + name: "@rnx-kit/align-deps", + version: "1.0.0-test", dependencies: { + react: "16.13.1", "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); - }); + "rnx-kit": { + kitType: "app", + alignDeps: { + requirements: ["react-native@0.63"], + capabilities: ["core"], + }, + }, + }; - test("skips unconfigured packages", async () => { - const result = setupMocks({ - ...mockManifest, - "rnx-kit": undefined, - } as PackageManifest); + rnxKitConfig.__setMockConfig(mockManifest["rnx-kit"]); + const result = setupMocks(mockManifest); prompts.inject([["0.64"]]); - const command = await makeSetVersionCommand(""); - expect(typeof command).toBe("function"); - expect(command("package.json")).toBe(0); + const command = await makeSetVersionCommand("", options); + if (typeof command !== "function") { + fail(); + } + + expect(command("package.json")).toBe("unsatisfied"); expect(result.didWrite).toBe(false); }); test("exits if the user cancels during prompts", async () => { prompts.inject([undefined]); - expect(await makeSetVersionCommand("")).toBeUndefined(); + expect(await makeSetVersionCommand("", options)).toBeUndefined(); prompts.inject([["0.63", "0.64"], undefined]); - expect(await makeSetVersionCommand("")).toBeUndefined(); + expect(await makeSetVersionCommand("", options)).toBeUndefined(); }); }); diff --git a/packages/align-deps/test/vigilant.test.ts b/packages/align-deps/test/vigilant.test.ts index 541e41474..630ebc405 100644 --- a/packages/align-deps/test/vigilant.test.ts +++ b/packages/align-deps/test/vigilant.test.ts @@ -1,9 +1,9 @@ -import { parseProfilesString } from "../src/profiles"; import { buildManifestProfile, checkPackageManifestUnconfigured, inspect, } from "../src/commands/vigilant"; +import { defaultConfig } from "../src/config"; import type { AlignDepsConfig } from "../src/types"; jest.mock("fs"); @@ -58,11 +58,6 @@ describe("buildManifestProfile()", () => { expect("react-native-test-app" in peerDependencies).toBe(false); expect("react-native-test-app" 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("inspect()", () => { @@ -242,7 +237,7 @@ describe("checkPackageManifestUnconfigured()", () => { const result = checkPackageManifestUnconfigured( "package.json", - { loose: false, write: false }, + { presets: defaultConfig.presets, loose: false, write: false }, makeConfig(["react-native@0.70"], { name: "@rnx-kit/align-deps", version: "1.0.0", @@ -264,7 +259,7 @@ describe("checkPackageManifestUnconfigured()", () => { const result = checkPackageManifestUnconfigured( "package.json", - { loose: false, write: false }, + { presets: defaultConfig.presets, loose: false, write: false }, makeConfig(["react-native@0.70"], { name: "@rnx-kit/align-deps", version: "1.0.0", @@ -286,7 +281,7 @@ describe("checkPackageManifestUnconfigured()", () => { const result = checkPackageManifestUnconfigured( "package.json", - { loose: false, write: true }, + { presets: defaultConfig.presets, loose: false, write: true }, makeConfig(["react-native@0.70"], { name: "@rnx-kit/align-deps", version: "1.0.0", @@ -309,9 +304,10 @@ describe("checkPackageManifestUnconfigured()", () => { const result = checkPackageManifestUnconfigured( "package.json", { + presets: defaultConfig.presets, + loose: false, write: false, excludePackages: ["@rnx-kit/align-deps"], - loose: false, }, makeConfig(["react-native@0.70"], { name: "@rnx-kit/align-deps", @@ -365,7 +361,7 @@ describe("checkPackageManifestUnconfigured()", () => { const result = checkPackageManifestUnconfigured( "package.json", - { loose: false, write: true }, + { presets: defaultConfig.presets, loose: false, write: true }, { ...kitConfig, manifest: inputManifest } );