From 1a13c1fdcb8f999ef0d537c7bd953810bf768e85 Mon Sep 17 00:00:00 2001 From: Tommy Nguyen <4123478+tido64@users.noreply.github.com> Date: Thu, 20 Oct 2022 19:27:55 +0200 Subject: [PATCH] refactor(align-deps): migrate init command (#1938) --- .changeset/metal-insects-behave.md | 2 + packages/align-deps/src/capabilities.ts | 92 +++---- packages/align-deps/src/cli.ts | 21 +- packages/align-deps/src/commands/check.ts | 115 +------- .../align-deps/src/commands/initialize.ts | 120 +++++++++ .../align-deps/src/compatibility/config.ts | 12 +- packages/align-deps/src/config.ts | 102 +++++++ packages/align-deps/src/errors.ts | 6 +- packages/align-deps/src/helpers.ts | 14 + packages/align-deps/src/initialize.ts | 62 ----- packages/align-deps/src/setVersion.ts | 9 +- packages/align-deps/src/types.ts | 4 +- packages/align-deps/test/capabilities.test.ts | 90 +++---- packages/align-deps/test/check.test.ts | 57 +--- packages/align-deps/test/config.test.ts | 52 ++++ packages/align-deps/test/initialize.test.ts | 253 ++++++++++-------- 16 files changed, 539 insertions(+), 472 deletions(-) create mode 100644 .changeset/metal-insects-behave.md create mode 100644 packages/align-deps/src/commands/initialize.ts create mode 100644 packages/align-deps/src/config.ts delete mode 100644 packages/align-deps/src/initialize.ts create mode 100644 packages/align-deps/test/config.test.ts diff --git a/.changeset/metal-insects-behave.md b/.changeset/metal-insects-behave.md new file mode 100644 index 000000000..a845151cc --- /dev/null +++ b/.changeset/metal-insects-behave.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/packages/align-deps/src/capabilities.ts b/packages/align-deps/src/capabilities.ts index c51abdd57..c78ec63de 100644 --- a/packages/align-deps/src/capabilities.ts +++ b/packages/align-deps/src/capabilities.ts @@ -1,70 +1,42 @@ -import type { Capability, KitCapabilities } from "@rnx-kit/config"; +import type { Capability } from "@rnx-kit/config"; +import { warn } from "@rnx-kit/console"; 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"; +import { keysOf } from "./helpers"; +import type { MetaPackage, Package, Preset, Profile } from "./types"; +/** + * Returns the list of capabilities used in the specified package manifest. + * @param packageManifest The package manifest to scan for dependencies + * @param preset The preset to use to resolve capabilities + * @returns A list of capabilities used in the specified package manifest + */ 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; + { + dependencies = {}, + devDependencies = {}, + peerDependencies = {}, + }: PackageManifest, + preset: Preset +): Capability[] { + const dependenciesSet = new Set(Object.keys(dependencies)); + Object.keys(peerDependencies).forEach((dep) => dependenciesSet.add(dep)); + Object.keys(devDependencies).forEach((dep) => dependenciesSet.add(dep)); + + if (dependenciesSet.size === 0) { + return []; } - const profiles = getProfilesFor(targetReactNativeVersion, customProfilesPath); - const packageToCapabilityMap: Record = {}; - profiles.forEach((profile) => { - keysOf(profile).reduce((result, capability) => { + const foundCapabilities = new Set(); + for (const profile of Object.values(preset)) { + for (const capability of keysOf(profile)) { const { name } = profile[capability]; - if (!result[name]) { - result[name] = [capability]; - } else { - result[name].push(capability); + if (dependenciesSet.has(name)) { + foundCapabilities.add(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(), - }; + return Array.from(foundCapabilities).sort(); } export function isMetaPackage(pkg: MetaPackage | Package): pkg is MetaPackage { @@ -140,7 +112,7 @@ export function resolveCapabilities( "The following capabilities could not be resolved for one or more profiles:" ); - console.warn(message); + warn(message); } return packages; diff --git a/packages/align-deps/src/cli.ts b/packages/align-deps/src/cli.ts index a201b25d5..aca0b38fd 100644 --- a/packages/align-deps/src/cli.ts +++ b/packages/align-deps/src/cli.ts @@ -9,6 +9,8 @@ import { } from "@rnx-kit/tools-workspaces"; import * as path from "path"; import { makeCheckCommand } from "./commands/check"; +import { makeInitializeCommand } from "./commands/initialize"; +import { defaultConfig } from "./config"; import { printError } from "./errors"; import type { Args, Command } from "./types"; @@ -79,19 +81,26 @@ async function makeCommand(args: Args): Promise { const { "exclude-packages": excludePackages, + init, loose, presets, requirements, write, } = args; - return makeCheckCommand({ + const options = { loose, write, excludePackages: excludePackages?.toString()?.split(","), - presets: presets?.toString()?.split(","), + presets: presets?.toString()?.split(",") ?? defaultConfig.presets, requirements: requirements?.toString()?.split(","), - }); + }; + + if (typeof init !== "undefined") { + return makeInitializeCommand(init, options); + } + + return makeCheckCommand(options); } export async function cli({ packages, ...args }: Args): Promise { @@ -145,6 +154,12 @@ if (require.main === module) { type: "string", requiresArg: true, }, + init: { + description: + "Writes an initial kit config to the specified 'package.json'. Note that this only works for React Native packages.", + choices: ["app", "library"], + conflicts: ["requirements"], + }, loose: { default: false, description: diff --git a/packages/align-deps/src/commands/check.ts b/packages/align-deps/src/commands/check.ts index 3b57a7daa..ca6ab41e0 100644 --- a/packages/align-deps/src/commands/check.ts +++ b/packages/align-deps/src/commands/check.ts @@ -1,120 +1,17 @@ -import type { KitConfig } from "@rnx-kit/config"; -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 { info } from "@rnx-kit/console"; +import { readPackage } from "@rnx-kit/tools-node/package"; import chalk from "chalk"; import { diffLinesUnified } from "jest-diff"; import * as path from "path"; import { migrateConfig } from "../compatibility/config"; -import { findBadPackages } from "../findBadPackages"; +import { getConfig } from "../config"; import { modifyManifest } from "../helpers"; import { updatePackageManifest } from "../manifest"; import { resolve } from "../preset"; -import type { - AlignDepsConfig, - CheckConfig, - Command, - ErrorCode, - Options, -} from "../types"; +import type { Command, ErrorCode, Options } from "../types"; import { checkPackageManifestUnconfigured } from "./vigilant"; -type ConfigResult = AlignDepsConfig | CheckConfig | ErrorCode; - -const defaultConfig: AlignDepsConfig["alignDeps"] = { - presets: ["microsoft/react-native"], - requirements: [], - capabilities: [], -}; - -export function containsValidPresets(config: KitConfig["alignDeps"]): boolean { - const presets = config?.presets; - return !presets || (Array.isArray(presets) && presets.length > 0); -} - -export function containsValidRequirements( - config: KitConfig["alignDeps"] -): boolean { - const requirements = config?.requirements; - if (requirements) { - if (Array.isArray(requirements)) { - return requirements.length > 0; - } else if (typeof requirements === "object") { - return ( - Array.isArray(requirements.production) && - requirements.production.length > 0 - ); - } - } - return false; -} - -function getConfig(manifestPath: string): ConfigResult { - const manifest = readPackage(manifestPath); - if (!isPackageManifest(manifest)) { - return "invalid-manifest"; - } - - const badPackages = findBadPackages(manifest); - if (badPackages) { - warn( - `Known bad packages are found in '${manifest.name}':\n` + - badPackages - .map((pkg) => `\t${pkg.name}@${pkg.version}: ${pkg.reason}`) - .join("\n") - ); - } - - const projectRoot = path.dirname(manifestPath); - const kitConfig = getKitConfig({ cwd: projectRoot }); - if (!kitConfig) { - return "not-configured"; - } - - const { kitType = "library", alignDeps, ...config } = kitConfig; - if (alignDeps) { - const errors = []; - if (!containsValidPresets(alignDeps)) { - errors.push(`${manifestPath}: 'alignDeps.presets' cannot be empty`); - } - if (!containsValidRequirements(alignDeps)) { - errors.push(`${manifestPath}: 'alignDeps.requirements' cannot be empty`); - } - if (errors.length > 0) { - for (const e of errors) { - error(e); - } - return "invalid-configuration"; - } - return { - kitType, - alignDeps: { - ...defaultConfig, - ...alignDeps, - }, - ...config, - manifest, - }; - } - - const { - capabilities, - customProfiles, - reactNativeDevVersion, - reactNativeVersion, - } = getKitCapabilities(config); - - return { - kitType, - reactNativeVersion, - ...(config.reactNativeDevVersion ? { reactNativeDevVersion } : undefined), - capabilities, - customProfiles, - manifest, - } as CheckConfig; -} - -function isError(config: ConfigResult): config is ErrorCode { +function isError(config: ReturnType): config is ErrorCode { return typeof config === "string"; } @@ -187,7 +84,7 @@ export function checkPackageManifest( } export function makeCheckCommand(options: Options): Command { - const { presets = defaultConfig.presets, requirements } = options; + const { presets, requirements } = options; if (!requirements) { return (manifest: string) => checkPackageManifest(manifest, options); } diff --git a/packages/align-deps/src/commands/initialize.ts b/packages/align-deps/src/commands/initialize.ts new file mode 100644 index 000000000..4a0f80950 --- /dev/null +++ b/packages/align-deps/src/commands/initialize.ts @@ -0,0 +1,120 @@ +import type { KitType } from "@rnx-kit/config"; +import { error } from "@rnx-kit/console"; +import type { PackageManifest } from "@rnx-kit/tools-node/package"; +import { readPackage } from "@rnx-kit/tools-node/package"; +import * as path from "path"; +import semverMinVersion from "semver/ranges/min-version"; +import { capabilitiesFor } from "../capabilities"; +import { defaultConfig } from "../config"; +import { dropPatchFromVersion, modifyManifest } from "../helpers"; +import { filterPreset, mergePresets } from "../preset"; +import type { Command, Options } from "../types"; + +function isKitType(type: string): type is KitType { + return type === "app" || type === "library"; +} + +function minVersion(versionRange: string): string { + const ver = semverMinVersion(versionRange); + if (!ver) { + throw new Error( + `Could not determine the lowest version that satisfies range: ${versionRange}` + ); + } + return ver.version; +} + +/** + * Generates an `align-deps` configuration for a React Native package by + * inspecting its dependencies. + * + * Note that this function uses the `react-native` version to determine which + * profile to use. If the package is not a React Native app/library, this + * function will return early. + * + * @param manifest The package manifest to update + * @param projectRoot The root of the project + * @param kitType The project type + * @param options Options from the command line + * @returns A configured package manifest; `null` if the React Native version could not be determined + */ +export function initializeConfig( + manifest: PackageManifest, + projectRoot: string, + kitType: KitType, + { presets }: Options +): PackageManifest | null { + const kitConfig = manifest["rnx-kit"]; + if (kitConfig?.alignDeps) { + return null; + } + + const { dependencies, devDependencies, peerDependencies } = manifest; + const targetReactNativeVersion = + peerDependencies?.["react-native"] || + dependencies?.["react-native"] || + devDependencies?.["react-native"]; + if (!targetReactNativeVersion) { + return null; + } + + const requirements = [ + `react-native@${dropPatchFromVersion(targetReactNativeVersion)}`, + ]; + const preset = filterPreset(requirements, mergePresets(presets, projectRoot)); + + return { + ...manifest, + "rnx-kit": { + ...kitConfig, + kitType, + alignDeps: { + presets: presets === defaultConfig.presets ? undefined : presets, + requirements: + kitType === "app" + ? requirements + : { + development: [ + `react-native@${dropPatchFromVersion( + devDependencies?.["react-native"] + ? devDependencies["react-native"] + : minVersion(targetReactNativeVersion) + )}`, + ], + production: requirements, + }, + capabilities: capabilitiesFor(manifest, preset), + }, + }, + }; +} + +export function makeInitializeCommand( + kitType: string, + options: Options +): Command | undefined { + if (!isKitType(kitType)) { + error(`Invalid kit type: '${kitType}'`); + return undefined; + } + + return (manifestPath: string) => { + const manifest = readPackage(manifestPath); + if (manifest["rnx-kit"]?.alignDeps) { + return "success"; + } + + const updatedManifest = initializeConfig( + manifest, + path.dirname(manifestPath), + kitType, + options + ); + if (!updatedManifest) { + return "missing-react-native"; + } + + modifyManifest(manifestPath, updatedManifest); + return "success"; + }; +} diff --git a/packages/align-deps/src/compatibility/config.ts b/packages/align-deps/src/compatibility/config.ts index 2d739aad6..258140fd7 100644 --- a/packages/align-deps/src/compatibility/config.ts +++ b/packages/align-deps/src/compatibility/config.ts @@ -1,18 +1,8 @@ import type { KitConfig } from "@rnx-kit/config"; import { warn } from "@rnx-kit/console"; -import semverCoerce from "semver/functions/coerce"; +import { dropPatchFromVersion } from "../helpers"; import type { AlignDepsConfig, CheckConfig } from "../types"; -function dropPatchFromVersion(version: string): string { - return version - .split("||") - .map((v) => { - const coerced = semverCoerce(v); - return coerced ? `${coerced.major}.${coerced.minor}` : "0.0"; - }) - .join(" || "); -} - function oldConfigKeys(config: KitConfig): (keyof KitConfig)[] { const oldKeys = [ "capabilities", diff --git a/packages/align-deps/src/config.ts b/packages/align-deps/src/config.ts new file mode 100644 index 000000000..a37a1d2c2 --- /dev/null +++ b/packages/align-deps/src/config.ts @@ -0,0 +1,102 @@ +import type { KitConfig } from "@rnx-kit/config"; +import { getKitCapabilities, getKitConfig } from "@rnx-kit/config"; +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"; + +type ConfigResult = AlignDepsConfig | CheckConfig | ErrorCode; + +export const defaultConfig: AlignDepsConfig["alignDeps"] = { + presets: ["microsoft/react-native"], + requirements: [], + capabilities: [], +}; + +export function containsValidPresets(config: KitConfig["alignDeps"]): boolean { + const presets = config?.presets; + return !presets || (Array.isArray(presets) && presets.length > 0); +} + +export function containsValidRequirements( + config: KitConfig["alignDeps"] +): boolean { + const requirements = config?.requirements; + if (requirements) { + if (Array.isArray(requirements)) { + return requirements.length > 0; + } else if (typeof requirements === "object") { + return ( + Array.isArray(requirements.production) && + requirements.production.length > 0 + ); + } + } + return false; +} + +export function getConfig(manifestPath: string): ConfigResult { + const manifest = readPackage(manifestPath); + if (!isPackageManifest(manifest)) { + return "invalid-manifest"; + } + + const badPackages = findBadPackages(manifest); + if (badPackages) { + warn( + `Known bad packages are found in '${manifest.name}':\n` + + badPackages + .map((pkg) => `\t${pkg.name}@${pkg.version}: ${pkg.reason}`) + .join("\n") + ); + } + + const projectRoot = path.dirname(manifestPath); + const kitConfig = getKitConfig({ cwd: projectRoot }); + if (!kitConfig) { + return "not-configured"; + } + + const { kitType = "library", alignDeps, ...config } = kitConfig; + if (alignDeps) { + const errors = []; + if (!containsValidPresets(alignDeps)) { + errors.push(`${manifestPath}: 'alignDeps.presets' cannot be empty`); + } + if (!containsValidRequirements(alignDeps)) { + errors.push(`${manifestPath}: 'alignDeps.requirements' cannot be empty`); + } + if (errors.length > 0) { + for (const e of errors) { + error(e); + } + return "invalid-configuration"; + } + return { + kitType, + alignDeps: { + ...defaultConfig, + ...alignDeps, + }, + ...config, + manifest, + }; + } + + const { + capabilities, + customProfiles, + reactNativeDevVersion, + reactNativeVersion, + } = getKitCapabilities(config); + + return { + kitType, + reactNativeVersion, + ...(config.reactNativeDevVersion ? { reactNativeDevVersion } : undefined), + capabilities, + customProfiles, + manifest, + } as CheckConfig; +} diff --git a/packages/align-deps/src/errors.ts b/packages/align-deps/src/errors.ts index ac7bc5268..60cada8ef 100644 --- a/packages/align-deps/src/errors.ts +++ b/packages/align-deps/src/errors.ts @@ -26,9 +26,11 @@ export function printError(manifestPath: string, code: ErrorCode): void { ); break; - case "missing-manifest": + case "missing-react-native": error( - `'${path.dirname(currentPackageJson)}' is missing a package manifest` + `Failed to infer requirements for '${manifestPath}'. This command ` + + "currently relies on the 'react-native' version in your project " + + "to generate the config." ); break; diff --git a/packages/align-deps/src/helpers.ts b/packages/align-deps/src/helpers.ts index 8c4e58a2d..ffa02315c 100644 --- a/packages/align-deps/src/helpers.ts +++ b/packages/align-deps/src/helpers.ts @@ -2,6 +2,7 @@ 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"; export function compare(lhs: T, rhs: T): -1 | 0 | 1 { if (lhs === rhs) { @@ -17,6 +18,19 @@ 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}`); + } + return `${coerced.major}.${coerced.minor}`; + }) + .join(" || "); +} + export function keysOf>(obj: T): (keyof T)[] { return Object.keys(obj); } diff --git a/packages/align-deps/src/initialize.ts b/packages/align-deps/src/initialize.ts deleted file mode 100644 index 4f5c2bf33..000000000 --- a/packages/align-deps/src/initialize.ts +++ /dev/null @@ -1,62 +0,0 @@ -import type { KitType } from "@rnx-kit/config"; -import { error } from "@rnx-kit/console"; -import { readPackage } from "@rnx-kit/tools-node/package"; -import { capabilitiesFor } from "./capabilities"; -import { modifyManifest } from "./helpers"; -import type { CapabilitiesOptions, Command } from "./types"; - -function ensureKitType(type: string): KitType | undefined { - switch (type) { - case "app": - case "library": - return type; - default: - return undefined; - } -} - -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); -} - -export 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 "success"; - }; -} diff --git a/packages/align-deps/src/setVersion.ts b/packages/align-deps/src/setVersion.ts index 0a4b271ca..9d2682692 100644 --- a/packages/align-deps/src/setVersion.ts +++ b/packages/align-deps/src/setVersion.ts @@ -2,6 +2,7 @@ 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"; @@ -62,8 +63,12 @@ export async function makeSetVersionCommand( return undefined; } - const checkOnly = { loose: false, write: false }; - const write = { loose: false, write: true }; + 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); diff --git a/packages/align-deps/src/types.ts b/packages/align-deps/src/types.ts index 2a3f11daf..e02a8f219 100644 --- a/packages/align-deps/src/types.ts +++ b/packages/align-deps/src/types.ts @@ -8,10 +8,10 @@ export type AlignDepsConfig = { }; export type Options = { + presets: string[]; loose: boolean; write: boolean; excludePackages?: string[]; - presets?: string[]; requirements?: string[]; }; @@ -37,7 +37,7 @@ export type ErrorCode = | "success" | "invalid-configuration" | "invalid-manifest" - | "missing-manifest" + | "missing-react-native" | "not-configured" | "unsatisfied"; diff --git a/packages/align-deps/test/capabilities.test.ts b/packages/align-deps/test/capabilities.test.ts index fe5060aac..3cb13b80f 100644 --- a/packages/align-deps/test/capabilities.test.ts +++ b/packages/align-deps/test/capabilities.test.ts @@ -1,5 +1,6 @@ import type { Capability } from "@rnx-kit/config"; import { capabilitiesFor, resolveCapabilities } from "../src/capabilities"; +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"; @@ -7,82 +8,64 @@ import { getProfilesFor } from "../src/profiles"; import { pickPackage } from "./helpers"; describe("capabilitiesFor()", () => { - test("returns `undefined` when react-native is not a dependency", () => { + test("returns an empty array when there are no dependencies", () => { 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(); + capabilitiesFor( + { name: "@rnx-kit/align-deps", version: "1.0.0" }, + defaultPreset + ) + ).toEqual([]); }); - test("returns capabilities when react-native is under dependencies", () => { + test("returns capabilities for dependencies declared under `dependencies`", () => { const manifest = { name: "@rnx-kit/align-deps", version: "1.0.0", dependencies: { + react: "^17.0.1", "react-native": "^0.64.1", }, }; - expect(capabilitiesFor(manifest)).toEqual({ - reactNativeVersion: "^0.64", - reactNativeDevVersion: "0.64.0", - kitType: "library", - capabilities: ["core", "core-android", "core-ios"], - }); + expect(capabilitiesFor(manifest, defaultPreset)).toEqual([ + "core", + "core-android", + "core-ios", + "react", + ]); }); - test("returns capabilities when react-native is under peerDependencies", () => { + test("returns capabilities for dependencies declared under `peerDependencies`", () => { const manifest = { name: "@rnx-kit/align-deps", version: "1.0.0", peerDependencies: { + react: "^17.0.1", "react-native": "^0.64.1", }, }; - expect(capabilitiesFor(manifest)).toEqual({ - reactNativeVersion: "^0.64", - reactNativeDevVersion: "0.64.0", - kitType: "library", - capabilities: ["core", "core-android", "core-ios"], - }); + expect(capabilitiesFor(manifest, defaultPreset)).toEqual([ + "core", + "core-android", + "core-ios", + "react", + ]); }); - test("returns capabilities when react-native is under devDependencies", () => { + test("returns capabilities for dependencies declared under `devDependencies`", () => { const manifest = { name: "@rnx-kit/align-deps", version: "1.0.0", devDependencies: { + react: "^17.0.1", "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"], - }); + expect(capabilitiesFor(manifest, defaultPreset)).toEqual([ + "core", + "core-android", + "core-ios", + "react", + ]); }); test("ignores packages that are not managed by align-deps", () => { @@ -98,11 +81,12 @@ describe("capabilitiesFor()", () => { "@rnx-kit/cli": "*", }, }; - expect(capabilitiesFor(manifest, { kitType: "app" })).toEqual({ - reactNativeVersion: "^0.64", - kitType: "app", - capabilities: ["core", "core-android", "core-ios", "react"], - }); + expect(capabilitiesFor(manifest, defaultPreset)).toEqual([ + "core", + "core-android", + "core-ios", + "react", + ]); }); }); diff --git a/packages/align-deps/test/check.test.ts b/packages/align-deps/test/check.test.ts index 9bc538dcc..23519c36b 100644 --- a/packages/align-deps/test/check.test.ts +++ b/packages/align-deps/test/check.test.ts @@ -1,8 +1,4 @@ -import { - checkPackageManifest, - containsValidPresets, - containsValidRequirements, -} from "../src/commands/check"; +import { checkPackageManifest } from "../src/commands/check"; import profile_0_68 from "../src/presets/microsoft/react-native/profile-0.68"; import profile_0_69 from "../src/presets/microsoft/react-native/profile-0.69"; import profile_0_70 from "../src/presets/microsoft/react-native/profile-0.70"; @@ -10,57 +6,6 @@ import { packageVersion } from "./helpers"; jest.mock("fs"); -describe("containsValidPresets()", () => { - test("is valid when 'presets' is unset", () => { - expect(containsValidPresets({})).toBe(true); - }); - - test("is invalid when 'presets' is empty", () => { - expect(containsValidPresets({ presets: [] })).toBe(false); - }); - - test("is invalid when 'presets' is not an array", () => { - // @ts-expect-error intentionally passing an invalid type - expect(containsValidPresets({ presets: "[]" })).toBe(false); - }); -}); - -describe("containsValidRequirements()", () => { - test("is invalid when 'requirements' is unset", () => { - expect(containsValidRequirements({})).toBe(false); - }); - - test("is invalid when 'requirements' is empty", () => { - expect(containsValidRequirements({ requirements: [] })).toBe(false); - expect( - // @ts-expect-error intentionally passing an invalid type - containsValidRequirements({ requirements: { production: [] } }) - ).toBe(false); - expect( - containsValidRequirements({ - requirements: { development: [], production: [] }, - }) - ).toBe(false); - }); - - test("is invalid when 'requirements' is not an array", () => { - // @ts-expect-error intentionally passing an invalid type - expect(containsValidRequirements({ requirements: "[]" })).toBe(false); - }); - - test("is valid when 'requirements' contains at least one requirement", () => { - expect( - containsValidRequirements({ requirements: ["react-native@*"] }) - ).toBe(true); - expect( - containsValidRequirements({ - // @ts-expect-error intentionally passing an invalid type - requirements: { production: ["react-native@*"] }, - }) - ).toBe(true); - }); -}); - describe("checkPackageManifest({ kitType: 'library' })", () => { const rnxKitConfig = require("@rnx-kit/config"); const fs = require("fs"); diff --git a/packages/align-deps/test/config.test.ts b/packages/align-deps/test/config.test.ts new file mode 100644 index 000000000..cbfc65a91 --- /dev/null +++ b/packages/align-deps/test/config.test.ts @@ -0,0 +1,52 @@ +import { containsValidPresets, containsValidRequirements } from "../src/config"; + +describe("containsValidPresets()", () => { + test("is valid when 'presets' is unset", () => { + expect(containsValidPresets({})).toBe(true); + }); + + test("is invalid when 'presets' is empty", () => { + expect(containsValidPresets({ presets: [] })).toBe(false); + }); + + test("is invalid when 'presets' is not an array", () => { + // @ts-expect-error intentionally passing an invalid type + expect(containsValidPresets({ presets: "[]" })).toBe(false); + }); +}); + +describe("containsValidRequirements()", () => { + test("is invalid when 'requirements' is unset", () => { + expect(containsValidRequirements({})).toBe(false); + }); + + test("is invalid when 'requirements' is empty", () => { + expect(containsValidRequirements({ requirements: [] })).toBe(false); + expect( + // @ts-expect-error intentionally passing an invalid type + containsValidRequirements({ requirements: { production: [] } }) + ).toBe(false); + expect( + containsValidRequirements({ + requirements: { development: [], production: [] }, + }) + ).toBe(false); + }); + + test("is invalid when 'requirements' is not an array", () => { + // @ts-expect-error intentionally passing an invalid type + expect(containsValidRequirements({ requirements: "[]" })).toBe(false); + }); + + test("is valid when 'requirements' contains at least one requirement", () => { + expect( + containsValidRequirements({ requirements: ["react-native@*"] }) + ).toBe(true); + expect( + containsValidRequirements({ + // @ts-expect-error intentionally passing an invalid type + requirements: { production: ["react-native@*"] }, + }) + ).toBe(true); + }); +}); diff --git a/packages/align-deps/test/initialize.test.ts b/packages/align-deps/test/initialize.test.ts index e1c8bd26d..2434efe5a 100644 --- a/packages/align-deps/test/initialize.test.ts +++ b/packages/align-deps/test/initialize.test.ts @@ -1,10 +1,10 @@ -import { initializeConfig } from "../src/initialize"; - -jest.mock("fs"); +import { + initializeConfig, + makeInitializeCommand, +} from "../src/commands/initialize"; +import { defaultConfig } from "../src/config"; describe("initializeConfig()", () => { - const fs = require("fs"); - const bundle = { entryPath: "src/index.ts", distPath: "dist", @@ -18,78 +18,54 @@ describe("initializeConfig()", () => { }, }; - 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; - }); + const result = initializeConfig( + { + name: "@rnx-kit/align-deps", + version: "0.0.0-test", + "rnx-kit": { alignDeps: {} }, + }, + ".", + "library", + { presets: [], loose: false, write: false } + ); - initializeConfig("package.json", {}); - expect(didWrite).toBe(false); + expect(result).toBeNull(); }); 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; - }); + const result = initializeConfig( + { + name: "@rnx-kit/align-deps", + version: "0.0.0-test", + }, + ".", + "library", + { presets: [], loose: false, write: false } + ); - initializeConfig("package.json", {}); - expect(didWrite).toBe(false); + expect(result).toBeNull(); }); test("keeps existing config", () => { - fs.__setMockContent({ - dependencies: { - "react-native": "^0.64.1", - }, - "rnx-kit": { - platformBundle: false, - bundle, + const result = initializeConfig( + { + name: "@rnx-kit/align-deps", + version: "0.0.0-test", + 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", {}); + ".", + "library", + { presets: defaultConfig.presets, loose: false, write: false } + ); - const kitConfig = content["rnx-kit"]; + const kitConfig = result?.["rnx-kit"]; if (!kitConfig) { fail(); } @@ -99,71 +75,124 @@ describe("initializeConfig()", () => { }); 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 result = initializeConfig( + { + name: "@rnx-kit/align-deps", + version: "0.0.0-test", + dependencies: { + "react-native": "^0.64.1", + }, + peerDependencies: { + "@react-native-community/netinfo": "^5.9.10", + "react-native-webview": "^10.10.2", + }, + "rnx-kit": { + bundle, + }, + }, + ".", + "app", + { presets: defaultConfig.presets, loose: false, write: false } + ); - const kitConfig = content["rnx-kit"]; + const kitConfig = result?.["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(); + expect(kitConfig.bundle).toEqual(bundle); + expect(kitConfig.kitType).toEqual("app"); + expect(kitConfig.alignDeps).toEqual({ + requirements: ["react-native@0.64"], + capabilities: ["core", "core-android", "core-ios", "netinfo", "webview"], + }); }); 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 result = initializeConfig( + { + name: "@rnx-kit/align-deps", + version: "0.0.0-test", + dependencies: { + "react-native": "^0.64.1", + }, + peerDependencies: { + "@react-native-community/netinfo": "^5.9.10", + "react-native-webview": "^10.10.2", + }, + "rnx-kit": { + bundle, + }, + }, + ".", + "library", + { presets: defaultConfig.presets, loose: false, write: false } + ); - const kitConfig = content["rnx-kit"]; + const kitConfig = result?.["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(); + expect(kitConfig.bundle).toEqual(bundle); + expect(kitConfig.kitType).toEqual("library"); + expect(kitConfig.alignDeps).toEqual({ + requirements: { + development: ["react-native@0.64"], + production: ["react-native@0.64"], + }, + capabilities: ["core", "core-android", "core-ios", "netinfo", "webview"], + }); }); 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 presets = [ + ...defaultConfig.presets, + "@rnx-kit/scripts/rnx-dep-check.js", + ]; + const result = initializeConfig( + { + name: "@rnx-kit/align-deps", + version: "0.0.0-test", + dependencies: { + "react-native": "^0.64.1", + }, + peerDependencies: { + "@react-native-community/netinfo": "^5.9.10", + "react-native-webview": "^10.10.2", + }, + "rnx-kit": { + bundle, + }, + }, + ".", + "library", + { presets, loose: false, write: false } + ); - const kitConfig = content["rnx-kit"]; - if (!kitConfig) { + const alignDeps = result?.["rnx-kit"]?.alignDeps; + if (!alignDeps) { fail(); } - expect(kitConfig["customProfiles"]).toEqual( - "@rnx-kit/scripts/rnx-dep-check.js" - ); + expect(alignDeps["presets"]).toEqual(presets); + }); +}); + +describe("makeInitializeCommand()", () => { + const options = { + presets: [], + loose: false, + write: false, + }; + + test("returns undefined for invalid kit types", () => { + const command = makeInitializeCommand("random", options); + expect(command).toBeUndefined(); + }); + + test("returns command for kit types", () => { + expect(makeInitializeCommand("app", options)).toBeDefined(); + expect(makeInitializeCommand("library", options)).toBeDefined(); }); });