Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fast-shirts-repeat.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@rnx-kit/align-deps": minor
---

When in vigilant mode, also suggest capabilities that can be added. This is only warning as there are legitimate reasons to not have dependencies managed by align-deps. For instance, the maintainers of AsyncStorage would not want to use the `storage` capability.
5 changes: 5 additions & 0 deletions .changeset/tasty-meals-fold.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@rnx-kit/align-deps": minor
---

The output format of regular and vigilant mode has been changed to be more legible and more consistent with each other.
1 change: 0 additions & 1 deletion packages/align-deps/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@
"detect-indent": "^6.0.0",
"eslint": "^8.0.0",
"jest": "^27.0.0",
"jest-diff": "^27.0.0",
"lodash": "^4.17.21",
"markdown-table": "^3.0.0",
"package-json": "^8.0.0",
Expand Down
10 changes: 10 additions & 0 deletions packages/align-deps/src/capabilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ type ResolvedDependencies = {
unresolvedCapabilities: Record<string, string[]>;
};

const ProvidesMeta = Symbol("provides");

/**
* Returns the list of capabilities used in the specified package manifest.
* @param packageManifest The package manifest to scan for dependencies
Expand Down Expand Up @@ -44,6 +46,12 @@ export function capabilitiesFor(
return Array.from(foundCapabilities).sort();
}

export function capabilityProvidedBy(
pkg: MetaPackage | Package
): string | undefined {
return pkg[ProvidesMeta];
}

export function isMetaPackage(pkg: MetaPackage | Package): pkg is MetaPackage {
return pkg.name === "#meta" && Array.isArray(pkg.capabilities);
}
Expand Down Expand Up @@ -74,6 +82,8 @@ function resolveCapability(
return;
}

pkg[ProvidesMeta] = capability;

pkg.capabilities?.forEach((capability) =>
resolveCapability(
capability,
Expand Down
61 changes: 25 additions & 36 deletions packages/align-deps/src/commands/check.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,16 @@
import { info } from "@rnx-kit/console";
import { pickValues } from "@rnx-kit/tools-language";
import type { PackageManifest } from "@rnx-kit/tools-node/package";
import { error, 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 { loadConfig } from "../config";
import { diff, stringify } from "../diff";
import { isError } from "../errors";
import { modifyManifest } from "../helpers";
import { updatePackageManifest } from "../manifest";
import { resolve } from "../preset";
import type { Command, ErrorCode, Options } from "../types";
import { checkPackageManifestUnconfigured } from "./vigilant";

const visibleKeys = [
"name",
"version",
"dependencies",
"peerDependencies",
"devDependencies",
];

function stringify(manifest: PackageManifest): string {
return JSON.stringify(pickValues(manifest, visibleKeys), undefined, 2);
}

/**
* Checks the specified package manifest for misaligned dependencies.
*
Expand All @@ -48,12 +33,14 @@ function stringify(manifest: PackageManifest): string {
* @param manifestPath Path to the package manifest to check
* @param options Command line options
* @param inputConfig Configuration in the package manifest
* @param logError Function for outputting changes
* @returns `success` when everything is in order; an {@link ErrorCode} otherwise
*/
export function checkPackageManifest(
manifestPath: string,
options: Options,
inputConfig = loadConfig(manifestPath, options)
inputConfig = loadConfig(manifestPath, options),
logError = error
): ErrorCode {
if (isError(inputConfig)) {
return inputConfig;
Expand Down Expand Up @@ -97,28 +84,18 @@ export function checkPackageManifest(
kitType
);

// Don't fail when manifests only have whitespace differences.
const updatedManifestJson = stringify(updatedManifest);
const normalizedManifestJson = stringify(manifest);

if (updatedManifestJson !== normalizedManifestJson) {
const allChanges = diff(manifest, updatedManifest);
if (allChanges) {
if (options.write) {
// The config object may be passed to other commands, so we need to
// update it in-place to ensure consistency.
inputConfig.manifest = updatedManifest;
modifyManifest(manifestPath, updatedManifest);
} else {
const diff = diffLinesUnified(
normalizedManifestJson.split("\n"),
updatedManifestJson.split("\n"),
{
aAnnotation: "Current",
aColor: chalk.red,
bAnnotation: "Expected",
bColor: chalk.green,
}
);
console.log(diff);
const violations = stringify(allChanges, [
`${manifestPath}: Changes are needed to satisfy all capabilities.`,
]);
logError(violations);
return "unsatisfied";
}
}
Expand Down Expand Up @@ -158,8 +135,20 @@ export function makeCheckCommand(options: Options): Command {

// If the package is configured, run the normal check first.
if (!isError(config)) {
const res1 = checkPackageManifest(manifest, options, config);
const res2 = checkPackageManifestUnconfigured(manifest, options, config);
const output: string[] = [];
const logError = (message: string) => {
output.push(message);
};
const res1 = checkPackageManifest(manifest, options, config, logError);
const res2 = checkPackageManifestUnconfigured(
manifest,
options,
config,
logError
);
for (const message of output) {
error(message);
}
return res1 !== "success" ? res1 : res2;
}

Expand Down
131 changes: 91 additions & 40 deletions packages/align-deps/src/commands/vigilant.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,34 @@
import type { Capability } from "@rnx-kit/config";
import { error } from "@rnx-kit/console";
import { error, info, warn } from "@rnx-kit/console";
import { keysOf } from "@rnx-kit/tools-language/properties";
import type { PackageManifest } from "@rnx-kit/tools-node/package";
import * as path from "path";
import semverSubset from "semver/ranges/subset";
import {
capabilityProvidedBy,
resolveCapabilities,
resolveCapabilitiesUnchecked,
} from "../capabilities";
import { stringify } from "../diff";
import { modifyManifest } from "../helpers";
import { updateDependencies } from "../manifest";
import { ensurePreset, filterPreset, mergePresets } from "../preset";
import type {
AlignDepsConfig,
Changes,
ErrorCode,
ManifestProfile,
Options,
Package,
Preset,
} from "../types";

type Change = {
name: string;
from: string;
to: string;
section: string;
type Report = {
changes: Changes;
changesCount: number;
unmanagedDependencies: [string, string][];
};

const allSections = [
"dependencies" as const,
"peerDependencies" as const,
"devDependencies" as const,
];

function getAllCapabilities(preset: Preset): Capability[] {
const capabilities = new Set<Capability>();
for (const profile of Object.values(preset)) {
Expand Down Expand Up @@ -65,7 +62,7 @@ function resolveUnmanagedCapabilities(
allCapabilities: Capability[],
preset: Preset,
managedDependencies: string[]
) {
): Record<string, Package[]> {
const dependencies = resolveCapabilities(
manifestPath,
allCapabilities,
Expand Down Expand Up @@ -125,14 +122,16 @@ export function buildManifestProfile(

// Use "development" type so we can check for `devOnly` packages under
// `dependencies` as well.
const unmanagedCapabilities = resolveUnmanagedCapabilities(
manifestPath,
allCapabilities,
targetPreset,
managedDependencies
);

const directDependencies = updateDependencies(
{},
resolveUnmanagedCapabilities(
manifestPath,
allCapabilities,
targetPreset,
managedDependencies
),
unmanagedCapabilities,
"development"
);

Expand All @@ -151,6 +150,12 @@ export function buildManifestProfile(
dependencies: directDependencies,
peerDependencies,
devDependencies: directDependencies,
unmanagedCapabilities: Object.fromEntries(
Object.values(unmanagedCapabilities).map((packages) => {
const pkg = packages[0];
return [pkg.name, capabilityProvidedBy(pkg)];
})
),
};
}

Expand Down Expand Up @@ -192,31 +197,58 @@ export function inspect(
manifest: PackageManifest,
profile: ManifestProfile,
write: boolean
): Change[] {
const changes: Change[] = [];
allSections.forEach((section) => {
): Report {
const allChanges: Report["changes"] = {
dependencies: [],
peerDependencies: [],
devDependencies: [],
};
const capabilities: Record<string, string> = {};

const { unmanagedCapabilities } = profile;

const changesCount = keysOf(allChanges).reduce((count, section) => {
const dependencies = manifest[section];
if (!dependencies) {
return;
return count;
}

const isMisaligned =
section === "peerDependencies" ? isMisalignedPeer : isMisalignedDirect;
const desiredDependencies = profile[section];
Object.keys(dependencies).forEach((name) => {
const changes = allChanges[section];

for (const name of Object.keys(dependencies)) {
if (name in desiredDependencies) {
const from = dependencies[name];
const to = desiredDependencies[name];
if (isMisaligned(from, to)) {
changes.push({ name, from, to, section });
changes.push({
type: "changed",
dependency: name,
target: to,
current: from,
});
if (write) {
dependencies[name] = to;
}
}

const capability = unmanagedCapabilities[name];
if (capability) {
capabilities[name] = capability;
}
}
});
});
return changes;
}

return count + changes.length;
}, 0);

return {
changes: allChanges,
changesCount,
unmanagedDependencies: Object.entries(capabilities),
};
}

/**
Expand All @@ -229,33 +261,52 @@ export function inspect(
* @param manifestPath The path to the package manifest
* @param options Options from command line
* @param config Configuration from `package.json` or "generated" from command line flags
* @param logError Function for outputting changes
* @returns Whether the package needs changes
*/
export function checkPackageManifestUnconfigured(
manifestPath: string,
{ excludePackages, write }: Options,
config: AlignDepsConfig
config: AlignDepsConfig,
logError = error
): ErrorCode {
if (excludePackages?.includes(config.manifest.name)) {
return "success";
}

const manifestProfile = buildManifestProfileCached(manifestPath, config);
const { manifest } = config;
const changes = inspect(manifest, manifestProfile, write);
if (changes.length > 0) {
const { changes, changesCount, unmanagedDependencies } = inspect(
manifest,
manifestProfile,
write
);

if (
config.alignDeps.capabilities.length > 0 &&
unmanagedDependencies.length > 0
) {
const dependencies = unmanagedDependencies
.map(([name, capability]) => {
return `\t - ${name} can be managed by '${capability}'`;
})
.join("\n");
warn(
`${manifestPath}: Found dependencies that are currently missing from capabilities:\n${dependencies}`
);
info(
"Note: Capabilities will never be added automatically, even with '--write'."
);
}

if (changesCount > 0) {
if (write) {
modifyManifest(manifestPath, manifest);
} else {
const violations = changes
.map(
({ name, from, to, section }) =>
`\t${name} "${from}" should be "${to}" (${section})`
)
.join("\n");
error(
`Found ${changes.length} violation(s) in '${manifestPath}':\n${violations}`
);
const violations = stringify(changes, [
`${manifestPath}: Found ${changesCount} violation(s) outside of capabilities.`,
]);
logError(violations);
return "unsatisfied";
}
}
Expand Down
Loading