diff --git a/.changeset/friendly-peas-sneeze.md b/.changeset/friendly-peas-sneeze.md new file mode 100644 index 000000000..3d48606a1 --- /dev/null +++ b/.changeset/friendly-peas-sneeze.md @@ -0,0 +1,5 @@ +--- +"@rnx-kit/align-deps": minor +--- + +Added a flag, `--no-unmanaged`, to make unmanaged capabilities errors diff --git a/.changeset/tidy-windows-relate.md b/.changeset/tidy-windows-relate.md new file mode 100644 index 000000000..e8fcb1009 --- /dev/null +++ b/.changeset/tidy-windows-relate.md @@ -0,0 +1,5 @@ +--- +"@rnx-kit/cli": patch +--- + +align-deps: Added a flag, `--no-unmanaged`, to make unmanaged capabilities errors diff --git a/packages/align-deps/README.md b/packages/align-deps/README.md index 9dea20b74..2c6397533 100644 --- a/packages/align-deps/README.md +++ b/packages/align-deps/README.md @@ -100,6 +100,20 @@ cumbersome to manually add all capabilities yourself. You can run this tool with `--init`, and it will try to add a sensible configuration based on what is currently defined in the specified `package.json`. +### `--loose` + +Determines how strict the React Native version requirement should be. Useful for +apps that depend on a newer React Native version than their dependencies declare +support for. + +Default: `false` + +### `--no-unmanaged` + +Whether unmanaged capabilities should be treated as errors. + +Default: `false` + ### `--presets` Comma-separated list of presets. This can be names to built-in presets, or paths @@ -170,10 +184,18 @@ If the version numbers are omitted, an _interactive prompt_ will appear. > made. As such, this flag will fail if changes are needed before making any > modifications. +### `--verbose` + +Specify to increase logging verbosity. + +Default: `false` + ### `--write` Writes all proposed changes to the specified `package.json`. +Default: `false` + ## Configure While `@rnx-kit/align-deps` can ensure your dependencies are aligned without a diff --git a/packages/align-deps/src/cli.ts b/packages/align-deps/src/cli.ts index cc489f05e..cd8121bc3 100644 --- a/packages/align-deps/src/cli.ts +++ b/packages/align-deps/src/cli.ts @@ -42,6 +42,12 @@ export const cliOptions = { "Determines whether align-deps should try to update the config in 'package.json'.", type: "boolean", }, + "no-unmanaged": { + default: false, + description: + "Determines whether align-deps should treat unmanaged capabilities as errors.", + type: "boolean", + }, presets: { description: "Comma-separated list of presets. This can be names to built-in presets, or paths to external presets.", @@ -157,6 +163,7 @@ async function makeCommand(args: Args): Promise { init, loose, "migrate-config": migrateConfig, + "no-unmanaged": noUnmanaged, presets, requirements, "set-version": setVersion, @@ -168,6 +175,7 @@ async function makeCommand(args: Args): Promise { presets: presets?.toString()?.split(",") ?? defaultConfig.presets, loose, migrateConfig, + noUnmanaged, verbose, write, excludePackages: excludePackages?.toString()?.split(","), diff --git a/packages/align-deps/src/commands/vigilant.ts b/packages/align-deps/src/commands/vigilant.ts index cfee3e257..46aa1a8e5 100644 --- a/packages/align-deps/src/commands/vigilant.ts +++ b/packages/align-deps/src/commands/vigilant.ts @@ -266,7 +266,7 @@ export function inspect( */ export function checkPackageManifestUnconfigured( manifestPath: string, - { excludePackages, write }: Options, + { excludePackages, noUnmanaged, write }: Options, config: AlignDepsConfig, logError = error ): ErrorCode { @@ -281,17 +281,18 @@ export function checkPackageManifestUnconfigured( manifestProfile, write ); - - if ( + const hasUnmanagedDeps = config.alignDeps.capabilities.length > 0 && - unmanagedDependencies.length > 0 - ) { + unmanagedDependencies.length > 0; + + if (hasUnmanagedDeps) { + const log = noUnmanaged ? logError : warn; const dependencies = unmanagedDependencies .map(([name, capability]) => { return `\t - ${name} can be managed by '${capability}'`; }) .join("\n"); - warn( + log( `${manifestPath}: Found dependencies that are currently missing from capabilities:\n${dependencies}` ); info( @@ -311,5 +312,9 @@ export function checkPackageManifestUnconfigured( } } + if (noUnmanaged && hasUnmanagedDeps) { + return "unmanaged-capabilities"; + } + return "success"; } diff --git a/packages/align-deps/src/types.ts b/packages/align-deps/src/types.ts index 6d532c1ad..7de91194f 100644 --- a/packages/align-deps/src/types.ts +++ b/packages/align-deps/src/types.ts @@ -22,6 +22,7 @@ export type Options = { presets: string[]; loose: boolean; migrateConfig: boolean; + noUnmanaged: boolean; verbose: boolean; write: boolean; excludePackages?: string[]; @@ -31,6 +32,7 @@ export type Options = { export type Args = Pick & { "exclude-packages"?: string | number; "migrate-config": boolean; + "no-unmanaged": boolean; "set-version"?: string | number; init?: string; packages?: (string | number)[]; @@ -48,6 +50,7 @@ export type ErrorCode = | "invalid-manifest" | "missing-react-native" | "not-configured" + | "unmanaged-capabilities" | "unsatisfied"; export type Command = (manifest: string) => ErrorCode; diff --git a/packages/align-deps/test/check.app.test.ts b/packages/align-deps/test/check.app.test.ts index 09f245648..48078fd01 100644 --- a/packages/align-deps/test/check.app.test.ts +++ b/packages/align-deps/test/check.app.test.ts @@ -9,6 +9,7 @@ const defaultOptions = { presets: defaultConfig.presets, loose: false, migrateConfig: false, + noUnmanaged: false, verbose: false, write: true, }; diff --git a/packages/align-deps/test/check.test.ts b/packages/align-deps/test/check.test.ts index 09408df4a..ca06c7426 100644 --- a/packages/align-deps/test/check.test.ts +++ b/packages/align-deps/test/check.test.ts @@ -11,6 +11,7 @@ const defaultOptions = { presets: defaultConfig.presets, loose: false, migrateConfig: false, + noUnmanaged: false, verbose: false, write: false, }; @@ -26,6 +27,7 @@ describe("checkPackageManifest({ kitType: 'library' })", () => { const consoleLogSpy = jest.spyOn(global.console, "log"); const consoleWarnSpy = jest.spyOn(global.console, "warn"); + const consoleErrorSpy = jest.spyOn(global.console, "error"); const mockManifest = { name: "@rnx-kit/align-deps", @@ -49,6 +51,7 @@ describe("checkPackageManifest({ kitType: 'library' })", () => { rnxKitConfig.__setMockConfig(); consoleLogSpy.mockReset(); consoleWarnSpy.mockReset(); + consoleErrorSpy.mockReset(); }); afterAll(() => { @@ -60,6 +63,7 @@ describe("checkPackageManifest({ kitType: 'library' })", () => { expect(result).toBe("invalid-manifest"); expect(consoleLogSpy).not.toHaveBeenCalled(); expect(consoleWarnSpy).not.toHaveBeenCalled(); + expect(consoleErrorSpy).not.toHaveBeenCalled(); }); test("returns early if 'rnx-kit' is missing from the manifest", () => { @@ -73,6 +77,7 @@ describe("checkPackageManifest({ kitType: 'library' })", () => { expect(result).toBe("not-configured"); expect(consoleLogSpy).not.toHaveBeenCalled(); expect(consoleWarnSpy).toHaveBeenCalled(); + expect(consoleErrorSpy).not.toHaveBeenCalled(); }); test("prints warnings when detecting bad packages", () => { @@ -95,6 +100,7 @@ describe("checkPackageManifest({ kitType: 'library' })", () => { expect(result).toBe("success"); expect(consoleLogSpy).not.toHaveBeenCalled(); expect(consoleWarnSpy).toHaveBeenCalled(); + expect(consoleErrorSpy).not.toHaveBeenCalled(); }); test("prints warnings when detecting bad packages (with version range)", () => { @@ -111,6 +117,7 @@ describe("checkPackageManifest({ kitType: 'library' })", () => { expect(result).toBe("success"); expect(consoleLogSpy).not.toHaveBeenCalled(); expect(consoleWarnSpy).toHaveBeenCalled(); + expect(consoleErrorSpy).not.toHaveBeenCalled(); }); test("returns early if no capabilities are defined", () => { @@ -124,6 +131,7 @@ describe("checkPackageManifest({ kitType: 'library' })", () => { expect(result).toBe("success"); expect(consoleLogSpy).not.toHaveBeenCalled(); expect(consoleWarnSpy).not.toHaveBeenCalled(); + expect(consoleErrorSpy).not.toHaveBeenCalled(); }); test("returns if no changes are needed", () => { @@ -150,6 +158,7 @@ describe("checkPackageManifest({ kitType: 'library' })", () => { expect(result).toBe("success"); expect(consoleLogSpy).not.toHaveBeenCalled(); expect(consoleWarnSpy).not.toHaveBeenCalled(); + expect(consoleErrorSpy).not.toHaveBeenCalled(); }); test("prints additional information with `--verbose`", () => { @@ -179,6 +188,7 @@ describe("checkPackageManifest({ kitType: 'library' })", () => { expect(result).toBe("success"); expect(consoleLogSpy).toHaveBeenCalled(); expect(consoleWarnSpy).not.toHaveBeenCalled(); + expect(consoleErrorSpy).not.toHaveBeenCalled(); }); test("returns if no changes are needed (write: true)", () => { @@ -209,6 +219,7 @@ describe("checkPackageManifest({ kitType: 'library' })", () => { expect(didWriteToPath).toBe(false); expect(consoleLogSpy).not.toHaveBeenCalled(); expect(consoleWarnSpy).not.toHaveBeenCalled(); + expect(consoleErrorSpy).not.toHaveBeenCalled(); }); test("returns error code if changes are needed", () => { @@ -239,6 +250,7 @@ describe("checkPackageManifest({ kitType: 'library' })", () => { expect(result).not.toBe("success"); expect(consoleLogSpy).not.toHaveBeenCalled(); expect(consoleWarnSpy).not.toHaveBeenCalled(); + expect(consoleErrorSpy).not.toHaveBeenCalled(); }); test("writes changes back to 'package.json'", () => { @@ -259,6 +271,7 @@ describe("checkPackageManifest({ kitType: 'library' })", () => { expect(didWriteToPath).toBe("package.json"); expect(consoleLogSpy).not.toHaveBeenCalled(); expect(consoleWarnSpy).not.toHaveBeenCalled(); + expect(consoleErrorSpy).not.toHaveBeenCalled(); }); test("preserves indentation in 'package.json'", () => { @@ -279,6 +292,7 @@ describe("checkPackageManifest({ kitType: 'library' })", () => { expect(output).toMatchSnapshot(); expect(consoleLogSpy).not.toHaveBeenCalled(); expect(consoleWarnSpy).not.toHaveBeenCalled(); + expect(consoleErrorSpy).not.toHaveBeenCalled(); }); test("returns appropriate error code if package is excluded", () => { @@ -298,6 +312,7 @@ describe("checkPackageManifest({ kitType: 'library' })", () => { expect(result).toBe("excluded"); expect(consoleLogSpy).not.toHaveBeenCalled(); expect(consoleWarnSpy).not.toHaveBeenCalled(); + expect(consoleErrorSpy).not.toHaveBeenCalled(); }); test("uses minimum supported version as development version", () => { @@ -324,6 +339,7 @@ describe("checkPackageManifest({ kitType: 'library' })", () => { expect(result).toBe("success"); expect(consoleLogSpy).not.toHaveBeenCalled(); expect(consoleWarnSpy).not.toHaveBeenCalled(); + expect(consoleErrorSpy).not.toHaveBeenCalled(); }); test("uses declared development version", () => { @@ -353,6 +369,7 @@ describe("checkPackageManifest({ kitType: 'library' })", () => { expect(result).toBe("success"); expect(consoleLogSpy).not.toHaveBeenCalled(); expect(consoleWarnSpy).not.toHaveBeenCalled(); + expect(consoleErrorSpy).not.toHaveBeenCalled(); }); test("handles development version ranges", () => { @@ -382,6 +399,7 @@ describe("checkPackageManifest({ kitType: 'library' })", () => { expect(result).toBe("success"); expect(consoleLogSpy).not.toHaveBeenCalled(); expect(consoleWarnSpy).not.toHaveBeenCalled(); + expect(consoleErrorSpy).not.toHaveBeenCalled(); }); }); diff --git a/packages/align-deps/test/initialize.test.ts b/packages/align-deps/test/initialize.test.ts index e175c4b6f..f8f0bdbab 100644 --- a/packages/align-deps/test/initialize.test.ts +++ b/packages/align-deps/test/initialize.test.ts @@ -8,6 +8,7 @@ const defaultOptions = { presets: defaultConfig.presets, loose: false, migrateConfig: false, + noUnmanaged: false, verbose: false, write: false, }; @@ -190,13 +191,25 @@ describe("initializeConfig()", () => { describe("makeInitializeCommand()", () => { const options = { ...defaultOptions, presets: [] }; + const consoleErrorSpy = jest.spyOn(global.console, "error"); + + beforeEach(() => { + consoleErrorSpy.mockReset(); + }); + + afterAll(() => { + jest.clearAllMocks(); + }); + test("returns undefined for invalid kit types", () => { const command = makeInitializeCommand("random", options); expect(command).toBeUndefined(); + expect(consoleErrorSpy).toHaveBeenCalledTimes(1); }); test("returns command for kit types", () => { expect(makeInitializeCommand("app", options)).toBeDefined(); expect(makeInitializeCommand("library", options)).toBeDefined(); + expect(consoleErrorSpy).not.toHaveBeenCalled(); }); }); diff --git a/packages/align-deps/test/setVersion.test.ts b/packages/align-deps/test/setVersion.test.ts index 1a242bd0a..a2aac1f5e 100644 --- a/packages/align-deps/test/setVersion.test.ts +++ b/packages/align-deps/test/setVersion.test.ts @@ -35,6 +35,7 @@ describe("makeSetVersionCommand()", () => { presets: defaultConfig.presets, loose: false, migrateConfig: false, + noUnmanaged: false, verbose: false, write: false, }; diff --git a/packages/align-deps/test/vigilant.test.ts b/packages/align-deps/test/vigilant.test.ts index efff2c584..c5a59530c 100644 --- a/packages/align-deps/test/vigilant.test.ts +++ b/packages/align-deps/test/vigilant.test.ts @@ -1,3 +1,4 @@ +import type { Capability } from "@rnx-kit/config"; import { buildManifestProfile, checkPackageManifestUnconfigured, @@ -13,14 +14,15 @@ function makeConfig( manifest: AlignDepsConfig["manifest"] = { name: "@rnx-kit/align-deps", version: "1.0.0-test", - } + }, + capabilities: Capability[] = [] ): AlignDepsConfig { return { kitType: "library" as const, alignDeps: { presets: ["microsoft/react-native"], requirements, - capabilities: [], + capabilities, }, manifest, }; @@ -296,6 +298,7 @@ describe("checkPackageManifestUnconfigured()", () => { presets: defaultConfig.presets, loose: false, migrateConfig: false, + noUnmanaged: false, verbose: false, write: false, }; @@ -400,6 +403,60 @@ describe("checkPackageManifestUnconfigured()", () => { expect(consoleErrorSpy).not.toHaveBeenCalled(); }); + test("returns non-zero exit code when there are unmanaged capabilities", () => { + let didWrite = false; + fs.__setMockFileWriter(() => { + didWrite = true; + }); + + const result = checkPackageManifestUnconfigured( + "package.json", + { ...defaultOptions, noUnmanaged: true }, + makeConfig( + ["react-native@0.73"], + { + name: "@rnx-kit/align-deps", + version: "1.0.0", + dependencies: { + "react-native": "^0.73.0", + "react-native-test-app": "^2.5.34", + }, + }, + ["core"] + ) + ); + expect(result).toBe("unmanaged-capabilities"); + expect(didWrite).toBe(false); + expect(consoleErrorSpy).toHaveBeenCalledTimes(1); + }); + + test("returns non-zero exit code when writing with unmanaged capabilities", () => { + let didWrite = false; + fs.__setMockFileWriter(() => { + didWrite = true; + }); + + const result = checkPackageManifestUnconfigured( + "package.json", + { ...defaultOptions, noUnmanaged: true, write: true }, + makeConfig( + ["react-native@0.73"], + { + name: "@rnx-kit/align-deps", + version: "1.0.0", + dependencies: { + "react-native": "1000.0.0", + "react-native-test-app": "^2.5.34", + }, + }, + ["test-app"] + ) + ); + expect(result).toBe("unmanaged-capabilities"); + expect(didWrite).toBe(true); + expect(consoleErrorSpy).toHaveBeenCalledTimes(1); + }); + test("uses package-specific custom profiles", () => { const fixture = `${__dirname}/__fixtures__/config-custom-profiles-only`; const kitConfig = { diff --git a/packages/cli/src/align-deps.ts b/packages/cli/src/align-deps.ts index 423a5c9c7..ce0b26838 100644 --- a/packages/cli/src/align-deps.ts +++ b/packages/cli/src/align-deps.ts @@ -21,6 +21,7 @@ export function rnxAlignDeps( ...pickValues(args, Object.values(optionsMap), Object.keys(optionsMap)), loose: Boolean(args.loose), "migrate-config": Boolean(args.migrateConfig), + "no-unmanaged": Boolean(args.noUnmanaged), verbose: Boolean(args.verbose), write: Boolean(args.write), packages: argv, @@ -49,6 +50,10 @@ export const rnxAlignDepsCommand = { name: "--migrate-config", description: cliOptions["migrate-config"].description, }, + { + name: "--no-unmanaged", + description: cliOptions["no-unmanaged"].description, + }, { name: "--presets [presets]", description: cliOptions.presets.description,