diff --git a/package.json b/package.json index 708ea18c..b7195142 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "fs-extra": "11.1.1", "globby": "11.1.0", "minimatch": "9.0.2", + "npm-package-arg": "10.1.0", "ora": "5.4.1", "prompts": "2.4.2", "read-yaml-file": "2.1.0", @@ -54,6 +55,7 @@ "@types/fs-extra": "11.0.1", "@types/jest": "29.5.2", "@types/node": "14.18.36", + "@types/npm-package-arg": "6.1.1", "@types/prompts": "2.4.4", "@types/semver": "7.5.0", "@typescript-eslint/eslint-plugin": "5.60.1", diff --git a/src/bin-fix-mismatches/effects.ts b/src/bin-fix-mismatches/effects.ts index 04a19874..44df968f 100644 --- a/src/bin-fix-mismatches/effects.ts +++ b/src/bin-fix-mismatches/effects.ts @@ -39,10 +39,10 @@ export const fixMismatchesEffects: VersionEffects = { onSnappedToMismatch(input) { return Effect.sync(() => setVersions(input)); }, - onUnsupportedMismatch(input) { - return Effect.sync(() => pipe(input, logHeader, logUnsupportedMismatch)); + onNonSemverMismatch(input) { + return Effect.sync(() => pipe(input, logHeader, logNonSemverMismatch)); }, - onWorkspaceMismatch(input) { + onLocalPackageMismatch(input) { return Effect.sync(() => setVersions(input)); }, onComplete(ctx) { @@ -59,13 +59,13 @@ function logHeader(input: Input) { function setVersions({ report }: Input) { report.instances.forEach((instance) => { - instance.setVersion(report.expectedVersion); + instance.setSpecifier(report.expectedVersion); }); } function removeVersions({ report }: Input) { report.instances.forEach((instance) => { - instance.setVersion(DELETE); + instance.setSpecifier(DELETE); }); } @@ -79,7 +79,7 @@ function logSameRangeMismatch({ ctx, report }: Input) { +function logNonSemverMismatch({ ctx, report }: Input) { ctx.isInvalid = true; console.log( chalk`{yellow %s %s} {dim has mismatched unsupported versions which syncpack cannot auto fix}%s`, diff --git a/src/bin-lint-semver-ranges/effects.ts b/src/bin-lint-semver-ranges/effects.ts index bfb2deb9..fbf93190 100644 --- a/src/bin-lint-semver-ranges/effects.ts +++ b/src/bin-lint-semver-ranges/effects.ts @@ -22,10 +22,10 @@ export const lintSemverRangesEffects: SemverRangeEffects = { onSemverRangeMismatch(input) { return Effect.sync(() => pipe(input, logHeader, logRangeMismatch)); }, - onUnsupportedVersion(input) { - return Effect.sync(() => pipe(input, logHeader, logUnsupportedVersion)); + onNonSemverVersion(input) { + return Effect.sync(() => pipe(input, logHeader, logNonSemverVersion)); }, - onWorkspaceSemverRangeMismatch(input) { + onLocalPackageSemverRangeMismatch(input) { return Effect.sync(() => pipe(input, logHeader, logRangeMismatch)); }, onComplete() { @@ -46,7 +46,7 @@ function logRangeMismatch({ report, ctx }: Input chalk`{red %s} %s {red %s} %s {green %s} {dim in %s of %s}`, ICON.cross, report.name, - report.instance.version, + report.instance.specifier, ICON.rightArrow, report.expectedVersion, report.instance.strategy.path, @@ -54,11 +54,11 @@ function logRangeMismatch({ report, ctx }: Input ); } -function logUnsupportedVersion({ report }: Input) { +function logNonSemverVersion({ report }: Input) { console.log( chalk`{yellow %s} %s {yellow %s} {dim ignored as a format which syncpack cannot apply semver ranges to}`, ICON.panic, report.name, - report.instance.version, + report.instance.specifier, ); } diff --git a/src/bin-list-mismatches/effects.ts b/src/bin-list-mismatches/effects.ts index 587c3ab5..040401c8 100644 --- a/src/bin-list-mismatches/effects.ts +++ b/src/bin-list-mismatches/effects.ts @@ -35,11 +35,11 @@ export const listMismatchesEffects: VersionEffects = { onSnappedToMismatch(input) { return Effect.sync(() => pipe(input, logHeader, logSnappedToMismatch)); }, - onUnsupportedMismatch(input) { - return Effect.sync(() => pipe(input, logHeader, logUnsupportedMismatch)); + onNonSemverMismatch(input) { + return Effect.sync(() => pipe(input, logHeader, logNonSemverMismatch)); }, - onWorkspaceMismatch(input) { - return Effect.sync(() => pipe(input, logHeader, logWorkspaceMismatch)); + onLocalPackageMismatch(input) { + return Effect.sync(() => pipe(input, logHeader, logLocalPackageMismatch)); }, onComplete() { return Effect.unit(); @@ -59,7 +59,7 @@ function logBanned({ report, ctx }: Input) { report.instances.forEach((instance) => { console.log( chalk` {red %s} {dim in %s of %s}`, - instance.version, + instance.specifier, instance.strategy.path, instance.packageJsonFile.shortPath, ); @@ -79,10 +79,10 @@ function logHighLowSemverMismatch({ report._tag === 'LowestSemverMismatch' ? 'lowest' : 'highest', ); report.instances.forEach((instance) => { - if (instance.version !== report.expectedVersion) { + if (instance.specifier !== report.expectedVersion) { console.log( chalk` {red %s} {dim in %s of %s}`, - instance.version, + instance.specifier, instance.strategy.path, instance.packageJsonFile.shortPath, ); @@ -99,10 +99,10 @@ function logPinnedMismatch({ report, ctx }: Input { - if (instance.version !== report.expectedVersion) { + if (instance.specifier !== report.expectedVersion) { console.log( chalk` {red %s} {dim in %s of %s}`, - instance.version, + instance.specifier, instance.strategy.path, instance.packageJsonFile.shortPath, ); @@ -120,10 +120,10 @@ function logSnappedToMismatch({ report, ctx }: Input { - if (instance.version !== report.expectedVersion) { + if (instance.specifier !== report.expectedVersion) { console.log( chalk` {red %s} {dim in %s of %s}`, - instance.version, + instance.specifier, instance.strategy.path, instance.packageJsonFile.shortPath, ); @@ -142,14 +142,14 @@ function logSameRangeMismatch({ report, ctx }: Input { console.log( chalk` {yellow %s} {dim in %s of %s}`, - instance.version, + instance.specifier, instance.strategy.path, instance.packageJsonFile.shortPath, ); }); } -function logUnsupportedMismatch({ report, ctx }: Input) { +function logNonSemverMismatch({ report, ctx }: Input) { ctx.isInvalid = true; console.log( chalk`{yellow %s %s} {dim has mismatched unsupported versions which syncpack cannot auto fix}%s`, @@ -160,27 +160,27 @@ function logUnsupportedMismatch({ report, ctx }: Input { console.log( chalk` {yellow %s} {dim in %s of %s}`, - instance.version, + instance.specifier, instance.strategy.path, instance.packageJsonFile.shortPath, ); }); } -function logWorkspaceMismatch({ report, ctx }: Input) { +function logLocalPackageMismatch({ report, ctx }: Input) { ctx.isInvalid = true; console.log( chalk`{red %s} %s {green %s} {dim is developed in this repo at %s}`, ICON.cross, report.name, report.expectedVersion, - report.workspaceInstance.packageJsonFile.shortPath, + report.localPackageInstance.packageJsonFile.shortPath, ); report.instances.forEach((instance) => { - if (instance.version !== report.expectedVersion) { + if (instance.specifier !== report.expectedVersion) { console.log( chalk` {red %s} {dim in %s of %s}`, - instance.version, + instance.specifier, instance.strategy.path, instance.packageJsonFile.shortPath, ); diff --git a/src/bin-list/effects.ts b/src/bin-list/effects.ts index ec379b5e..17defa5a 100644 --- a/src/bin-list/effects.ts +++ b/src/bin-list/effects.ts @@ -4,10 +4,9 @@ import chalk from 'chalk'; import { uniq } from 'tightrope/array/uniq'; import { ICON } from '../constants'; import type { VersionEffectInput as Input, VersionEffects } from '../create-program/effects'; -import type { Instance } from '../get-package-json-files/instance'; import type { VersionGroupReport } from '../get-version-groups'; -import { getUniqueVersions } from '../get-version-groups/lib/get-unique-versions'; -import { isSupported } from '../guards/is-supported'; +import { getUniqueSpecifiers } from '../get-version-groups/lib/get-unique-specifiers'; +import type { Instance } from '../instance'; import { logGroupHeader } from '../lib/log-group-header'; export const listEffects: VersionEffects = { @@ -38,10 +37,10 @@ export const listEffects: VersionEffects = { onSnappedToMismatch(input) { return Effect.sync(() => pipe(input, logHeader, logFixableMismatch)); }, - onUnsupportedMismatch(input) { + onNonSemverMismatch(input) { return Effect.sync(() => pipe(input, logHeader, logUnfixableMismatch)); }, - onWorkspaceMismatch(input) { + onLocalPackageMismatch(input) { return Effect.sync(() => pipe(input, logHeader, logFixableMismatch)); }, onComplete() { @@ -62,7 +61,7 @@ function logFixableMismatch({ report, chalk`{red %s %s} %s`, ICON.cross, report.name, - listColouredVersions(report.expectedVersion, report.instances), + listColouredSpecifiers(report.expectedVersion, report.instances), ); } @@ -75,8 +74,12 @@ function logUnfixableMismatch({ chalk`{red %s %s} %s`, ICON.cross, report.name, - getUniqueVersions(report.instances) - .map((version) => (isSupported(version) ? chalk.red(version) : chalk.yellow(version))) + getUniqueSpecifiers(report.instances) + .map((instance) => + instance.getSemverSpecifier() !== null + ? chalk.red(instance.specifier) + : chalk.yellow(instance.specifier), + ) .join(chalk.dim(', ')), ); } @@ -95,11 +98,11 @@ function logIgnored({ report }: Input) { } function logValid({ report }: Input) { - console.log(chalk`{dim -} {white %s} {dim %s}`, report.name, report.instances?.[0]?.version); + console.log(chalk`{dim -} {white %s} {dim %s}`, report.name, report.instances?.[0]?.specifier); } -function listColouredVersions(pinVersion: string, instances: Instance[]) { - return getAllVersions(pinVersion, instances) +function listColouredSpecifiers(pinVersion: string, instances: Instance.Any[]) { + return getAllSpecifiers(pinVersion, instances) .map((version) => withColour(pinVersion, version)) .join(chalk.dim(', ')); } @@ -108,6 +111,6 @@ function withColour(pinVersion: string, version: string) { return version === pinVersion ? chalk.green(version) : chalk.red(version); } -function getAllVersions(pinVersion: string, instances: Instance[]) { - return uniq([pinVersion].concat(instances.map((i) => i.version))); +function getAllSpecifiers(pinVersion: string, instances: Instance.Any[]) { + return uniq([pinVersion].concat(instances.map((i) => i.specifier))); } diff --git a/src/bin-prompt/effects.ts b/src/bin-prompt/effects.ts index d25e1c74..41be4e67 100644 --- a/src/bin-prompt/effects.ts +++ b/src/bin-prompt/effects.ts @@ -5,7 +5,7 @@ import { ICON } from '../constants'; import type { VersionEffectInput as Input, VersionEffects } from '../create-program/effects'; import { EnvTag } from '../env/tags'; import type { VersionGroupReport } from '../get-version-groups'; -import { getUniqueVersions } from '../get-version-groups/lib/get-unique-versions'; +import { getUniqueSpecifiers } from '../get-version-groups/lib/get-unique-specifiers'; import { logGroupHeader } from '../lib/log-group-header'; export const promptEffects: VersionEffects = { @@ -39,13 +39,13 @@ export const promptEffects: VersionEffects = { onSnappedToMismatch() { return Effect.unit(); }, - onUnsupportedMismatch(input) { + onNonSemverMismatch(input) { return pipe( Effect.sync(() => logHeader(input)), Effect.flatMap(askForNextVersion), ); }, - onWorkspaceMismatch() { + onLocalPackageMismatch() { return Effect.unit(); }, onComplete() { @@ -69,7 +69,7 @@ function askForNextVersion({ report }: Input) const choice = yield* $( env.askForChoice({ message: chalk`${report.name} {dim Choose a version to replace the others}`, - choices: [...getUniqueVersions(report.instances), OTHER, SKIP], + choices: [...getUniqueSpecifiers(report.instances).map((i) => i.specifier), OTHER, SKIP], }), ); if (choice === SKIP) return; @@ -84,7 +84,7 @@ function askForNextVersion({ report }: Input) yield* $( Effect.sync(() => { report.instances.forEach((instance) => { - instance.setVersion(nextVersion); + instance.setSpecifier(nextVersion); }); }), ); diff --git a/src/bin-set-semver-ranges/effects.ts b/src/bin-set-semver-ranges/effects.ts index 5995c5d1..b6fab569 100644 --- a/src/bin-set-semver-ranges/effects.ts +++ b/src/bin-set-semver-ranges/effects.ts @@ -20,10 +20,10 @@ export const setSemverRangesEffects: SemverRangeEffects = { onSemverRangeMismatch(input) { return Effect.sync(() => setVersions(input)); }, - onUnsupportedVersion(input) { - return Effect.sync(() => logUnsupportedVersion(input)); + onNonSemverVersion(input) { + return Effect.sync(() => logNonSemverVersion(input)); }, - onWorkspaceSemverRangeMismatch(input) { + onLocalPackageSemverRangeMismatch(input) { return Effect.sync(() => setVersions(input)); }, onComplete() { @@ -32,14 +32,14 @@ export const setSemverRangesEffects: SemverRangeEffects = { }; function setVersions({ report }: Input) { - report.instance.setVersion(report.expectedVersion); + report.instance.setSpecifier(report.expectedVersion); } -function logUnsupportedVersion({ report }: Input) { +function logNonSemverVersion({ report }: Input) { console.log( chalk`{yellow %s} %s {yellow %s} {dim ignored as a format which syncpack cannot apply semver ranges to}`, ICON.panic, report.name, - report.instance.version, + report.instance.specifier, ); } diff --git a/src/bin-update/effects.ts b/src/bin-update/effects.ts index 9c4c912f..241093c7 100644 --- a/src/bin-update/effects.ts +++ b/src/bin-update/effects.ts @@ -12,7 +12,7 @@ import { ICON } from '../constants'; import type { VersionEffectInput as Input, VersionEffects } from '../create-program/effects'; import type { VersionGroupReport } from '../get-version-groups'; import { getHighestVersion } from '../get-version-groups/lib/get-highest-version'; -import { getUniqueVersions } from '../get-version-groups/lib/get-unique-versions'; +import { getUniqueSpecifiers } from '../get-version-groups/lib/get-unique-specifiers'; import { logVerbose } from '../lib/log-verbose'; import { getSemverRange, setSemverRange } from '../lib/set-semver-range'; @@ -57,10 +57,10 @@ export const updateEffects: VersionEffects = { onSnappedToMismatch() { return Effect.unit(); }, - onUnsupportedMismatch() { + onNonSemverMismatch() { return Effect.unit(); }, - onWorkspaceMismatch() { + onLocalPackageMismatch() { return Effect.unit(); }, onComplete(ctx, results) { @@ -131,7 +131,9 @@ function promptForUpdates(results: Array) { const input = result.input; const latestVersion = result.versions.latest; - const uniqueVersions = getUniqueVersions(input.report.instances); + const uniqueVersions = getUniqueSpecifiers(input.report.instances).map( + (i) => i.specifier, + ); const highestVersion = unwrap(getHighestVersion(uniqueVersions)); const exactHighestVersion = setSemverRange('', highestVersion); @@ -177,9 +179,9 @@ function promptForUpdates(results: Array) { Effect.sync(() => { chosenUpdates.forEach(({ input, versions }) => { input.report.instances.forEach((instance) => { - const semverRange = getSemverRange(instance.version); + const semverRange = getSemverRange(instance.specifier); const latestWithRange = setSemverRange(semverRange, versions.latest); - instance.setVersion(latestWithRange); + instance.setSpecifier(latestWithRange); }); }); }), diff --git a/src/config/get-enabled-types.ts b/src/config/get-enabled-types.ts index ffbf19bc..d4df3e0b 100644 --- a/src/config/get-enabled-types.ts +++ b/src/config/get-enabled-types.ts @@ -58,7 +58,7 @@ export function getEnabledTypes({ enabledTypes.push(new VersionsByNameStrategy('resolutions', 'resolutions')); } if (useDefaults || enabledTypeNames.includes('workspace')) { - enabledTypes.push(new NameAndVersionPropsStrategy('workspace', 'version', 'name')); + enabledTypes.push(new NameAndVersionPropsStrategy('localPackage', 'version', 'name')); } getCustomTypes({ cli, rcFile }).forEach((customType) => { diff --git a/src/create-program/effects.ts b/src/create-program/effects.ts index 239a4bf7..b218749e 100644 --- a/src/create-program/effects.ts +++ b/src/create-program/effects.ts @@ -26,12 +26,12 @@ export interface SemverRangeEffects { onSemverRangeMismatch: ( input: SemverRangeEffectInput, ) => Effect.Effect; - onUnsupportedVersion: ( - input: SemverRangeEffectInput, + onNonSemverVersion: ( + input: SemverRangeEffectInput, ) => Effect.Effect; onValid: (input: SemverRangeEffectInput) => Effect.Effect; - onWorkspaceSemverRangeMismatch: ( - input: SemverRangeEffectInput, + onLocalPackageSemverRangeMismatch: ( + input: SemverRangeEffectInput, ) => Effect.Effect; onComplete: (ctx: Ctx, results: A[]) => Effect.Effect; } @@ -57,12 +57,12 @@ export interface VersionEffects { onSnappedToMismatch: ( input: VersionEffectInput, ) => Effect.Effect; - onUnsupportedMismatch: ( - input: VersionEffectInput, + onNonSemverMismatch: ( + input: VersionEffectInput, ) => Effect.Effect; onValid: (input: VersionEffectInput) => Effect.Effect; - onWorkspaceMismatch: ( - input: VersionEffectInput, + onLocalPackageMismatch: ( + input: VersionEffectInput, ) => Effect.Effect; onComplete: (ctx: Ctx, results: A[]) => Effect.Effect; } diff --git a/src/create-program/semver-ranges.ts b/src/create-program/semver-ranges.ts index fb2e759d..35d57d08 100644 --- a/src/create-program/semver-ranges.ts +++ b/src/create-program/semver-ranges.ts @@ -41,11 +41,11 @@ export function createSemverRangesProgram>( SemverRangeMismatch(report) { return effects.onSemverRangeMismatch({ ctx, group, index, report }); }, - UnsupportedVersion(report) { - return effects.onUnsupportedVersion({ ctx, group, index, report }); + NonSemverVersion(report) { + return effects.onNonSemverVersion({ ctx, group, index, report }); }, - WorkspaceSemverRangeMismatch(report) { - return effects.onWorkspaceSemverRangeMismatch({ ctx, group, index, report }); + LocalPackageSemverRangeMismatch(report) { + return effects.onLocalPackageSemverRangeMismatch({ ctx, group, index, report }); }, }), ), diff --git a/src/create-program/versions.ts b/src/create-program/versions.ts index 60cc45c4..53b78e9b 100644 --- a/src/create-program/versions.ts +++ b/src/create-program/versions.ts @@ -56,11 +56,11 @@ export function createVersionsProgram>( SnappedToMismatch(report) { return effects.onSnappedToMismatch({ ctx, group, index, report }); }, - UnsupportedMismatch(report) { - return effects.onUnsupportedMismatch({ ctx, group, index, report }); + NonSemverMismatch(report) { + return effects.onNonSemverMismatch({ ctx, group, index, report }); }, - WorkspaceMismatch(report) { - return effects.onWorkspaceMismatch({ ctx, group, index, report }); + LocalPackageMismatch(report) { + return effects.onLocalPackageMismatch({ ctx, group, index, report }); }, }), ), diff --git a/src/get-context/get-context.spec.ts b/src/get-context/get-context.spec.ts index f77e262f..d2ecd9f0 100644 --- a/src/get-context/get-context.spec.ts +++ b/src/get-context/get-context.spec.ts @@ -22,6 +22,7 @@ describe('getContext', () => { packageJsonFiles: [ { contents, + dirPath: CWD, filePath, json, config: expect.toBeNonEmptyObject(), diff --git a/src/get-package-json-files/instance.ts b/src/get-package-json-files/instance.ts deleted file mode 100644 index b8a310db..00000000 --- a/src/get-package-json-files/instance.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { pipe } from '@effect/data/Function'; -import type { Strategy } from '../config/get-custom-types'; -import type { Delete } from '../get-version-groups/lib/delete'; -import { $R } from '../lib/$R'; -import type { PackageJsonFile } from './package-json-file'; - -export class Instance { - /** the name of this dependency */ - name: string; - /** The package this dependency is installed in this specific time */ - packageJsonFile: PackageJsonFile; - /** locates where in the file this dependency is installed */ - strategy: Strategy.Any; - /** The .name property of the package.json file of this instance */ - pkgName: string; - /** the version of this dependency */ - version: string; - - constructor( - strategy: Strategy.Any, - name: string, - packageJsonFile: PackageJsonFile, - version: string, - ) { - this.strategy = strategy; - this.name = name; - this.packageJsonFile = packageJsonFile; - this.pkgName = packageJsonFile.contents.name || 'PACKAGE_JSON_HAS_NO_NAME'; - this.version = version; - } - - /** - * In the case of banned dependencies, their version is set to `undefined`, - * which causes them to be removed by `JSON.stringify`. - */ - setVersion(version: string | Delete): void { - const file = this.packageJsonFile; - pipe(this.strategy.write(file, [this.name, version]), $R.tapErrVerbose); - } -} diff --git a/src/get-package-json-files/package-json-file.ts b/src/get-package-json-files/package-json-file.ts index 1907440b..14326641 100644 --- a/src/get-package-json-files/package-json-file.ts +++ b/src/get-package-json-files/package-json-file.ts @@ -1,12 +1,13 @@ import { pipe } from '@effect/data/Function'; -import { relative } from 'path'; +import { dirname, relative } from 'path'; import { map } from 'tightrope/result/map'; import type { Strategy } from '../config/get-custom-types'; import { CWD } from '../constants'; import type { Ctx } from '../get-context'; +import type { Instance } from '../instance'; +import { createInstance } from '../instance/create'; import { logVerbose } from '../lib/log-verbose'; import type { JsonFile } from './get-patterns/read-json-safe'; -import { Instance } from './instance'; export interface PackageJson { bugs?: { url: string } | string; @@ -39,6 +40,9 @@ export class PackageJsonFile { /** absolute path on disk to this file */ readonly filePath: string; + /** absolute path on disk to this file's directory */ + readonly dirPath: string; + /** raw file contents of the file */ readonly json: string; @@ -52,22 +56,23 @@ export class PackageJsonFile { this.config = config; this.contents = jsonFile.contents; this.filePath = jsonFile.filePath; + this.dirPath = dirname(jsonFile.filePath); this.json = jsonFile.json; this.shortPath = relative(CWD, jsonFile.filePath); } - getInstances(enabledTypes: Strategy.Any[]): Instance[] { - const instances: Instance[] = []; + getInstances(enabledTypes: Strategy.Any[]): Instance.Any[] { + const instances: Instance.Any[] = []; enabledTypes.forEach((strategy) => { pipe( strategy.read(this), map((entries) => - entries.forEach(([name, version]) => { + entries.forEach(([name, specifier]) => { logVerbose( - `add ${name}@${version} to ${strategy.name}:${strategy._tag} ${this.shortPath}`, + `add ${name}@${specifier} to ${strategy.name}:${strategy._tag} ${this.shortPath}`, ); - instances.push(new Instance(strategy, name, this, version)); + instances.push(createInstance(strategy, name, this, specifier)); }), ), ); diff --git a/src/get-semver-groups/filtered-out.ts b/src/get-semver-groups/filtered-out.ts index 84e154a1..b4b3ccfb 100644 --- a/src/get-semver-groups/filtered-out.ts +++ b/src/get-semver-groups/filtered-out.ts @@ -4,12 +4,12 @@ import { SemverGroupReport } from '.'; import { getFilter } from '../config/get-filter'; import type { GroupConfig } from '../config/types'; import type { Ctx } from '../get-context'; -import type { Instance } from '../get-package-json-files/instance'; +import type { Instance } from '../instance'; export class FilteredOutSemverGroup extends Data.TaggedClass('FilteredOut')<{ config: GroupConfig; filter: string; - instances: Instance[]; + instances: Instance.Any[]; }> { constructor(ctx: Ctx) { super({ @@ -24,7 +24,7 @@ export class FilteredOutSemverGroup extends Data.TaggedClass('FilteredOut')<{ }); } - canAdd(instance: Instance): boolean { + canAdd(instance: Instance.Any): boolean { return instance.name.search(new RegExp(this.filter)) === -1; } diff --git a/src/get-semver-groups/ignored.ts b/src/get-semver-groups/ignored.ts index f91ff25a..5e50bad3 100644 --- a/src/get-semver-groups/ignored.ts +++ b/src/get-semver-groups/ignored.ts @@ -2,11 +2,11 @@ import * as Data from '@effect/data/Data'; import * as Effect from '@effect/io/Effect'; import { SemverGroupReport } from '.'; import type { SemverGroupConfig } from '../config/types'; -import type { Instance } from '../get-package-json-files/instance'; +import type { Instance } from '../instance'; export class IgnoredSemverGroup extends Data.TaggedClass('Ignored')<{ config: SemverGroupConfig.Ignored; - instances: Instance[]; + instances: Instance.Any[]; }> { constructor(config: SemverGroupConfig.Ignored) { super({ @@ -15,7 +15,7 @@ export class IgnoredSemverGroup extends Data.TaggedClass('Ignored')<{ }); } - canAdd(_: Instance): boolean { + canAdd(_: Instance.Any): boolean { return true; } diff --git a/src/get-semver-groups/index.ts b/src/get-semver-groups/index.ts index 03da9452..775a52de 100644 --- a/src/get-semver-groups/index.ts +++ b/src/get-semver-groups/index.ts @@ -10,9 +10,9 @@ import type { DeprecatedTypesError } from '../config/get-enabled-types'; import { getEnabledTypes } from '../config/get-enabled-types'; import { getSemverRange } from '../config/get-semver-range'; import type { Ctx } from '../get-context'; -import type { Instance } from '../get-package-json-files/instance'; import { canAddToGroup } from '../guards/can-add-to-group'; import { isValidSemverRange } from '../guards/is-valid-semver-range'; +import type { Instance } from '../instance'; import { sortByName } from '../lib/sort-by-name'; import { FilteredOutSemverGroup } from './filtered-out'; import { IgnoredSemverGroup } from './ignored'; @@ -25,53 +25,53 @@ export type AnySemverGroup = Union.Strict< export namespace SemverGroupReport { export class FilteredOut extends Data.TaggedClass('FilteredOut')<{ name: string; - instance: Instance; + instance: Instance.Any; readonly isValid: true; }> {} export class Ignored extends Data.TaggedClass('Ignored')<{ name: string; - instance: Instance; + instance: Instance.Any; readonly isValid: true; }> {} export class Valid extends Data.TaggedClass('Valid')<{ name: string; - instance: Instance; + instance: Instance.Any; readonly isValid: true; }> {} - export class WorkspaceSemverRangeMismatch extends Data.TaggedClass( - 'WorkspaceSemverRangeMismatch', + export class LocalPackageSemverRangeMismatch extends Data.TaggedClass( + 'LocalPackageSemverRangeMismatch', )<{ name: string; - instance: Instance; + instance: Instance.Any; readonly isValid: false; readonly expectedVersion: string; }> {} export class SemverRangeMismatch extends Data.TaggedClass('SemverRangeMismatch')<{ name: string; - instance: Instance; + instance: Instance.Any; readonly isValid: false; readonly expectedVersion: string; }> {} - export class UnsupportedVersion extends Data.TaggedClass('UnsupportedVersion')<{ + export class NonSemverVersion extends Data.TaggedClass('NonSemverVersion')<{ name: string; - instance: Instance; + instance: Instance.Any; readonly isValid: false; }> {} export type ValidCases = Union.Strict; export type InvalidCases = Union.Strict< - SemverRangeMismatch | UnsupportedVersion | WorkspaceSemverRangeMismatch + SemverRangeMismatch | NonSemverVersion | LocalPackageSemverRangeMismatch >; - export type FixableCases = Union.Strict; + export type FixableCases = Union.Strict; - export type UnfixableCases = Union.Strict; + export type UnfixableCases = Union.Strict; export type Any = Union.Strict; } diff --git a/src/get-semver-groups/with-range.ts b/src/get-semver-groups/with-range.ts index 925f1752..8d5f34ff 100644 --- a/src/get-semver-groups/with-range.ts +++ b/src/get-semver-groups/with-range.ts @@ -1,14 +1,14 @@ import * as Data from '@effect/data/Data'; +import * as Option from '@effect/data/Option'; import * as Effect from '@effect/io/Effect'; import { SemverGroupReport } from '.'; import type { SemverGroupConfig } from '../config/types'; -import type { Instance } from '../get-package-json-files/instance'; -import { isSupported } from '../guards/is-supported'; +import type { Instance } from '../instance'; import { setSemverRange } from '../lib/set-semver-range'; export class WithRangeSemverGroup extends Data.TaggedClass('WithRange')<{ config: SemverGroupConfig.WithRange; - instances: Instance[]; + instances: Instance.Any[]; isCatchAll: boolean; }> { constructor(isCatchAll: boolean, config: SemverGroupConfig.WithRange) { @@ -19,21 +19,21 @@ export class WithRangeSemverGroup extends Data.TaggedClass('WithRange')<{ }); } - canAdd(_: Instance): boolean { + canAdd(_: Instance.Any): boolean { return true; } inspect(): Effect.Effect< never, - | SemverGroupReport.UnsupportedVersion - | SemverGroupReport.WorkspaceSemverRangeMismatch + | SemverGroupReport.NonSemverVersion + | SemverGroupReport.LocalPackageSemverRangeMismatch | SemverGroupReport.SemverRangeMismatch, SemverGroupReport.Valid >[] { return this.instances.map((instance) => { - if (!isSupported(instance.version)) { + if (Option.isNone(instance.getSemverSpecifier())) { return Effect.fail( - new SemverGroupReport.UnsupportedVersion({ + new SemverGroupReport.NonSemverVersion({ name: instance.name, instance, isValid: false, @@ -41,13 +41,13 @@ export class WithRangeSemverGroup extends Data.TaggedClass('WithRange')<{ ); } - const isWsInstance = instance.strategy.name === 'workspace'; - const exactVersion = setSemverRange('', instance.version); - const expectedVersion = setSemverRange(this.config.range, instance.version); + const isLocalPackageInstance = instance.strategy.name === 'localPackage'; + const exactVersion = setSemverRange('', instance.specifier); + const expectedVersion = setSemverRange(this.config.range, instance.specifier); - if (isWsInstance && instance.version !== exactVersion) { + if (isLocalPackageInstance && instance.specifier !== exactVersion) { return Effect.fail( - new SemverGroupReport.WorkspaceSemverRangeMismatch({ + new SemverGroupReport.LocalPackageSemverRangeMismatch({ name: instance.name, instance, isValid: false, @@ -55,7 +55,7 @@ export class WithRangeSemverGroup extends Data.TaggedClass('WithRange')<{ }), ); } - if (instance.version === expectedVersion) { + if (instance.specifier === expectedVersion) { return Effect.succeed( new SemverGroupReport.Valid({ name: instance.name, diff --git a/src/get-version-groups/banned.ts b/src/get-version-groups/banned.ts index 846e5cf1..1e70cb89 100644 --- a/src/get-version-groups/banned.ts +++ b/src/get-version-groups/banned.ts @@ -2,12 +2,12 @@ import * as Data from '@effect/data/Data'; import * as Effect from '@effect/io/Effect'; import { VersionGroupReport } from '.'; import type { VersionGroupConfig } from '../config/types'; -import type { Instance } from '../get-package-json-files/instance'; +import type { Instance } from '../instance'; import { groupBy } from './lib/group-by'; export class BannedVersionGroup extends Data.TaggedClass('Banned')<{ config: VersionGroupConfig.Banned; - instances: Instance[]; + instances: Instance.Any[]; }> { constructor(config: VersionGroupConfig.Banned) { super({ @@ -16,7 +16,7 @@ export class BannedVersionGroup extends Data.TaggedClass('Banned')<{ }); } - canAdd(_: Instance): boolean { + canAdd(_: Instance.Any): boolean { return true; } diff --git a/src/get-version-groups/filtered-out.ts b/src/get-version-groups/filtered-out.ts index ab4d1d73..b49172f6 100644 --- a/src/get-version-groups/filtered-out.ts +++ b/src/get-version-groups/filtered-out.ts @@ -4,13 +4,13 @@ import { VersionGroupReport } from '.'; import { getFilter } from '../config/get-filter'; import type { GroupConfig } from '../config/types'; import type { Ctx } from '../get-context'; -import type { Instance } from '../get-package-json-files/instance'; +import type { Instance } from '../instance'; import { groupBy } from './lib/group-by'; export class FilteredOutVersionGroup extends Data.TaggedClass('FilteredOut')<{ config: GroupConfig; filter: string; - instances: Instance[]; + instances: Instance.Any[]; }> { constructor(ctx: Ctx) { super({ @@ -21,11 +21,11 @@ export class FilteredOutVersionGroup extends Data.TaggedClass('FilteredOut')<{ packages: ['**'], }, filter: getFilter(ctx.config), - instances: [] satisfies Instance[], + instances: [] satisfies Instance.Any[], }); } - canAdd(instance: Instance): boolean { + canAdd(instance: Instance.Any): boolean { return instance.name.search(new RegExp(this.filter)) === -1; } diff --git a/src/get-version-groups/ignored.ts b/src/get-version-groups/ignored.ts index af802669..78dd5ca2 100644 --- a/src/get-version-groups/ignored.ts +++ b/src/get-version-groups/ignored.ts @@ -2,12 +2,12 @@ import * as Data from '@effect/data/Data'; import * as Effect from '@effect/io/Effect'; import { VersionGroupReport } from '.'; import type { VersionGroupConfig } from '../config/types'; -import type { Instance } from '../get-package-json-files/instance'; +import type { Instance } from '../instance'; import { groupBy } from './lib/group-by'; export class IgnoredVersionGroup extends Data.TaggedClass('Ignored')<{ config: VersionGroupConfig.Ignored; - instances: Instance[]; + instances: Instance.Any[]; }> { constructor(config: VersionGroupConfig.Ignored) { super({ @@ -16,7 +16,7 @@ export class IgnoredVersionGroup extends Data.TaggedClass('Ignored')<{ }); } - canAdd(_: Instance): boolean { + canAdd(_: Instance.Any): boolean { return true; } diff --git a/src/get-version-groups/index.ts b/src/get-version-groups/index.ts index 79cc536e..7cf8d945 100644 --- a/src/get-version-groups/index.ts +++ b/src/get-version-groups/index.ts @@ -9,8 +9,8 @@ import type { Union } from 'ts-toolbelt'; import type { DeprecatedTypesError } from '../config/get-enabled-types'; import { getEnabledTypes } from '../config/get-enabled-types'; import type { Ctx } from '../get-context'; -import type { Instance } from '../get-package-json-files/instance'; import { canAddToGroup } from '../guards/can-add-to-group'; +import type { Instance } from '../instance'; import { sortByName } from '../lib/sort-by-name'; import { BannedVersionGroup } from './banned'; import { FilteredOutVersionGroup } from './filtered-out'; @@ -31,77 +31,125 @@ export type AnyVersionGroup = Union.Strict< >; export namespace VersionGroupReport { + /** + * Every instance in this `BannedVersionGroup` matched its configuration and + * will be removed if fixed + */ export class Banned extends Data.TaggedClass('Banned')<{ name: string; - instances: Instance[]; + instances: Instance.Any[]; readonly isValid: false; }> {} + /** + * Every instance in this `FilteredOutVersionGroup` has name which does not + * match the RegExp produced by the `--filter` option. + */ export class FilteredOut extends Data.TaggedClass('FilteredOut')<{ name: string; - instances: Instance[]; + instances: Instance.Any[]; readonly isValid: true; }> {} + /** + * One or more instances has a version which is not identical to the others, + * to resolve this issue the highest semver version present should be used + */ export class HighestSemverMismatch extends Data.TaggedClass('HighestSemverMismatch')<{ name: string; - instances: Instance[]; + instances: Instance.Any[]; readonly isValid: false; readonly expectedVersion: string; }> {} + /** + * Every instance in this `IgnoredVersionGroup` matched its configuration + */ export class Ignored extends Data.TaggedClass('Ignored')<{ name: string; - instances: Instance[]; + instances: Instance.Any[]; readonly isValid: true; }> {} + /** + * One or more instances has a version which is not identical to the others, + * to resolve this issue the lowest semver version present should be used + */ export class LowestSemverMismatch extends Data.TaggedClass('LowestSemverMismatch')<{ name: string; - instances: Instance[]; + instances: Instance.Any[]; readonly isValid: false; readonly expectedVersion: string; }> {} + /** + * One or more instances has a version which is not identical to the + * `pinVersion` value set in this `PinnedVersionGroup` + */ export class PinnedMismatch extends Data.TaggedClass('PinnedMismatch')<{ name: string; - instances: Instance[]; + instances: Instance.Any[]; readonly isValid: false; readonly expectedVersion: string; }> {} + /** + * One or more instances has a version which is not satisfied by the semver + * version ranges defined by every other instance of this same dependency. In + * a `SameRangeVersionGroup`, each version does not have to be indentical, but + * they do have to all satisfy the ranges set by the others. + */ export class SameRangeMismatch extends Data.TaggedClass('SameRangeMismatch')<{ name: string; - instances: Instance[]; + instances: Instance.Any[]; readonly isValid: false; }> {} + /** + * One or more instances has a version which does not match the version used + * in any of the locally developed packages whose names are set in the + * `snapTo` array of this `SnappedToVersionGroup` + */ export class SnappedToMismatch extends Data.TaggedClass('SnappedToMismatch')<{ name: string; - instances: Instance[]; + instances: Instance.Any[]; readonly isValid: false; readonly expectedVersion: string; readonly snapTo: string[]; }> {} - export class UnsupportedMismatch extends Data.TaggedClass('UnsupportedMismatch')<{ + /** + * One or more instances has a version which does not match the others and + * also, at least one of the instances has a version which is not semver. + * Syncpack cannot guess what any given Developer will want to do in this + * situation + */ + export class NonSemverMismatch extends Data.TaggedClass('NonSemverMismatch')<{ name: string; - instances: Instance[]; + instances: Instance.Any[]; readonly isValid: false; }> {} + /** + * Every instance satisfies the rules of the Version Group they belong to + */ export class Valid extends Data.TaggedClass('Valid')<{ name: string; - instances: Instance[]; + instances: Instance.Any[]; readonly isValid: true; }> {} - export class WorkspaceMismatch extends Data.TaggedClass('WorkspaceMismatch')<{ + /** + * This dependency is one of the packages developed in this monorepo and one + * or more of the packages in this monorepo depend on it with a version which + * is not identical to the `.version` property of its package.json file + */ + export class LocalPackageMismatch extends Data.TaggedClass('LocalPackageMismatch')<{ name: string; - instances: Instance[]; + instances: Instance.Any[]; readonly isValid: false; readonly expectedVersion: string; - readonly workspaceInstance: Instance; + readonly localPackageInstance: Instance.Any; }> {} export type ValidCases = Union.Strict; @@ -113,8 +161,8 @@ export namespace VersionGroupReport { | PinnedMismatch | SameRangeMismatch | SnappedToMismatch - | UnsupportedMismatch - | WorkspaceMismatch + | NonSemverMismatch + | LocalPackageMismatch >; export type FixableCases = Union.Strict< @@ -122,11 +170,11 @@ export namespace VersionGroupReport { | LowestSemverMismatch | PinnedMismatch | SnappedToMismatch - | WorkspaceMismatch + | LocalPackageMismatch >; export type UnfixableCases = Union.Strict< - SameRangeMismatch | UnsupportedMismatch | WorkspaceMismatch + SameRangeMismatch | NonSemverMismatch | LocalPackageMismatch >; export type HighLowSemverMismatch = diff --git a/src/get-version-groups/lib/sort.ts b/src/get-version-groups/lib/compare-semver.ts similarity index 79% rename from src/get-version-groups/lib/sort.ts rename to src/get-version-groups/lib/compare-semver.ts index 3e0c0e1a..94344c48 100644 --- a/src/get-version-groups/lib/sort.ts +++ b/src/get-version-groups/lib/compare-semver.ts @@ -1,6 +1,5 @@ import gt from 'semver/functions/gt'; import lt from 'semver/functions/lt'; -import { isSupported } from '../../guards/is-supported'; import { clean } from './clean'; import { getRangeScore } from './get-range-score'; @@ -9,8 +8,6 @@ const LT = -1; const GT = 1; export function compareSemver(a: string, b: string): -1 | 0 | 1 { - if (!isSupported(a)) throw new Error(`"${a}" is not supported`); - if (!isSupported(b)) throw new Error(`"${b}" is not supported`); if (a.startsWith('workspace:')) return LT; if (b.startsWith('workspace:')) return GT; if (a === b) return EQ; diff --git a/src/get-version-groups/lib/get-highest-version.ts b/src/get-version-groups/lib/get-highest-version.ts index f38324dd..45a67b50 100644 --- a/src/get-version-groups/lib/get-highest-version.ts +++ b/src/get-version-groups/lib/get-highest-version.ts @@ -4,7 +4,7 @@ import type { Result } from 'tightrope/result'; import { filter } from 'tightrope/result/filter'; import { fromTry } from 'tightrope/result/from-try'; import { map } from 'tightrope/result/map'; -import { compareSemver } from './sort'; +import { compareSemver } from './compare-semver'; export function getHighestVersion(versions: string[]): Result { return pipe( diff --git a/src/get-version-groups/lib/get-lowest-version.ts b/src/get-version-groups/lib/get-lowest-version.ts index c1d2b21e..b97789ed 100644 --- a/src/get-version-groups/lib/get-lowest-version.ts +++ b/src/get-version-groups/lib/get-lowest-version.ts @@ -4,7 +4,7 @@ import type { Result } from 'tightrope/result'; import { filter } from 'tightrope/result/filter'; import { fromTry } from 'tightrope/result/from-try'; import { map } from 'tightrope/result/map'; -import { compareSemver } from './sort'; +import { compareSemver } from './compare-semver'; export function getLowestVersion(versions: string[]): Result { return pipe( diff --git a/src/get-version-groups/lib/get-unique-specifiers.ts b/src/get-version-groups/lib/get-unique-specifiers.ts new file mode 100644 index 00000000..25caa972 --- /dev/null +++ b/src/get-version-groups/lib/get-unique-specifiers.ts @@ -0,0 +1,9 @@ +import type { Instance } from '../../instance'; + +export function getUniqueSpecifiers(instances: Instance.Any[]): Instance.Any[] { + const instancesBySpecifier: Record = {}; + instances.forEach((instance) => { + instancesBySpecifier[instance.specifier] = instance; + }); + return Object.values(instancesBySpecifier); +} diff --git a/src/get-version-groups/lib/get-unique-versions.ts b/src/get-version-groups/lib/get-unique-versions.ts deleted file mode 100644 index c1b2635a..00000000 --- a/src/get-version-groups/lib/get-unique-versions.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { uniq } from 'tightrope/array/uniq'; -import type { Instance } from '../../get-package-json-files/instance'; - -export function getUniqueVersions(instances: Instance[]): string[] { - return uniq(instances.map((i) => i.version)); -} diff --git a/src/get-version-groups/pinned.ts b/src/get-version-groups/pinned.ts index 7ecd61e5..03caf091 100644 --- a/src/get-version-groups/pinned.ts +++ b/src/get-version-groups/pinned.ts @@ -2,12 +2,12 @@ import * as Data from '@effect/data/Data'; import * as Effect from '@effect/io/Effect'; import { VersionGroupReport } from '.'; import type { VersionGroupConfig } from '../config/types'; -import type { Instance } from '../get-package-json-files/instance'; +import type { Instance } from '../instance'; import { groupBy } from './lib/group-by'; export class PinnedVersionGroup extends Data.TaggedClass('Pinned')<{ config: VersionGroupConfig.Pinned; - instances: Instance[]; + instances: Instance.Any[]; }> { constructor(config: VersionGroupConfig.Pinned) { super({ @@ -16,14 +16,13 @@ export class PinnedVersionGroup extends Data.TaggedClass('Pinned')<{ }); } - canAdd(_: Instance): boolean { + canAdd(_: Instance.Any): boolean { return true; } inspect(): Effect.Effect[] { const instancesByName = groupBy('name', this.instances); const expectedVersion = this.config.pinVersion; - return Object.entries(instancesByName).map(([name, instances]) => { if (hasMismatch(expectedVersion, instances)) { return Effect.fail( @@ -47,6 +46,6 @@ export class PinnedVersionGroup extends Data.TaggedClass('Pinned')<{ } } -function hasMismatch(pinVersion: string, instances: Instance[]) { - return instances.some((instance) => instance.version !== pinVersion); +function hasMismatch(pinVersion: string, instances: Instance.Any[]) { + return instances.some((instance) => instance.specifier !== pinVersion); } diff --git a/src/get-version-groups/same-range.ts b/src/get-version-groups/same-range.ts index 19b8ea35..190f82e5 100644 --- a/src/get-version-groups/same-range.ts +++ b/src/get-version-groups/same-range.ts @@ -3,12 +3,12 @@ import * as Effect from '@effect/io/Effect'; import intersects from 'semver/ranges/intersects'; import { VersionGroupReport } from '.'; import type { VersionGroupConfig } from '../config/types'; -import type { Instance } from '../get-package-json-files/instance'; +import type { Instance } from '../instance'; import { groupBy } from './lib/group-by'; export class SameRangeVersionGroup extends Data.TaggedClass('SameRange')<{ config: VersionGroupConfig.SameRange; - instances: Instance[]; + instances: Instance.Any[]; }> { constructor(config: VersionGroupConfig.SameRange) { super({ @@ -17,7 +17,7 @@ export class SameRangeVersionGroup extends Data.TaggedClass('SameRange')<{ }); } - canAdd(_: Instance): boolean { + canAdd(_: Instance.Any): boolean { return true; } @@ -27,7 +27,6 @@ export class SameRangeVersionGroup extends Data.TaggedClass('SameRange')<{ VersionGroupReport.Valid >[] { const instancesByName = groupBy('name', this.instances); - return Object.entries(instancesByName).map(([name, instances]) => { if (hasMismatch(instances)) { return Effect.fail( @@ -51,11 +50,11 @@ export class SameRangeVersionGroup extends Data.TaggedClass('SameRange')<{ } /** Every range must fall within every other range */ -function hasMismatch(instances: Instance[]) { +function hasMismatch(instances: Instance.Any[]) { const loose = true; return instances.some((a) => instances.some( - (b) => !intersects(aliasWorkspaceRange(a.version), aliasWorkspaceRange(b.version), loose), + (b) => !intersects(aliasWorkspaceRange(a.specifier), aliasWorkspaceRange(b.specifier), loose), ), ); } diff --git a/src/get-version-groups/snapped-to.ts b/src/get-version-groups/snapped-to.ts index 109b523c..08d2a874 100644 --- a/src/get-version-groups/snapped-to.ts +++ b/src/get-version-groups/snapped-to.ts @@ -2,12 +2,12 @@ import * as Data from '@effect/data/Data'; import * as Effect from '@effect/io/Effect'; import { VersionGroupReport } from '.'; import type { VersionGroupConfig } from '../config/types'; -import type { Instance } from '../get-package-json-files/instance'; +import type { Instance } from '../instance'; import { groupBy } from './lib/group-by'; export class SnappedToVersionGroup extends Data.TaggedClass('SnappedTo')<{ config: VersionGroupConfig.SnappedTo; - instances: Instance[]; + instances: Instance.Any[]; }> { constructor(config: VersionGroupConfig.SnappedTo) { super({ @@ -16,7 +16,7 @@ export class SnappedToVersionGroup extends Data.TaggedClass('SnappedTo')<{ }); } - canAdd(_: Instance): boolean { + canAdd(_: Instance.Any): boolean { return true; } @@ -26,11 +26,9 @@ export class SnappedToVersionGroup extends Data.TaggedClass('SnappedTo')<{ VersionGroupReport.Valid >[] { const instancesByName = groupBy('name', this.instances); - return Object.entries(instancesByName).map(([name, instances]) => { const snapTo = this.config.snapTo; const expectedVersion = getExpectedVersion(snapTo, instances); - if (hasMismatch(expectedVersion, instances)) { return Effect.fail( new VersionGroupReport.SnappedToMismatch({ @@ -54,15 +52,15 @@ export class SnappedToVersionGroup extends Data.TaggedClass('SnappedTo')<{ } } -function getExpectedVersion(snapTo: string[], instances: Instance[]): string { +function getExpectedVersion(snapTo: string[], instances: Instance.Any[]): string { const expectedVersion = instances .filter((i) => snapTo.includes(i.pkgName)) - .find((i) => i.version)?.version; + .find((i) => i.specifier)?.specifier; if (expectedVersion) return expectedVersion; // @FIXME: create tagged error for this throw new Error('versionGroup.snapTo does not match any package versions'); } -function hasMismatch(expectedVersion: string, instances: Instance[]) { - return instances.some((instance) => instance.version !== expectedVersion); +function hasMismatch(expectedVersion: string, instances: Instance.Any[]) { + return instances.some((instance) => instance.specifier !== expectedVersion); } diff --git a/src/get-version-groups/standard.ts b/src/get-version-groups/standard.ts index 9cd556ad..b79c1a42 100644 --- a/src/get-version-groups/standard.ts +++ b/src/get-version-groups/standard.ts @@ -1,18 +1,18 @@ import * as Data from '@effect/data/Data'; +import * as Option from '@effect/data/Option'; import * as Effect from '@effect/io/Effect'; import { unwrap } from 'tightrope/result/unwrap'; import { VersionGroupReport } from '.'; import type { VersionGroupConfig } from '../config/types'; -import type { Instance } from '../get-package-json-files/instance'; -import { isSupported } from '../guards/is-supported'; +import type { Instance } from '../instance'; import { getHighestVersion } from './lib/get-highest-version'; import { getLowestVersion } from './lib/get-lowest-version'; -import { getUniqueVersions } from './lib/get-unique-versions'; +import { getUniqueSpecifiers } from './lib/get-unique-specifiers'; import { groupBy } from './lib/group-by'; export class StandardVersionGroup extends Data.TaggedClass('Standard')<{ config: VersionGroupConfig.Standard; - instances: Instance[]; + instances: Instance.Any[]; isCatchAll: boolean; }> { constructor(isCatchAll: boolean, config: VersionGroupConfig.Standard) { @@ -23,20 +23,19 @@ export class StandardVersionGroup extends Data.TaggedClass('Standard')<{ }); } - canAdd(_: Instance): boolean { + canAdd(_: Instance.Any): boolean { return true; } inspect(): Effect.Effect< never, - | VersionGroupReport.WorkspaceMismatch - | VersionGroupReport.UnsupportedMismatch + | VersionGroupReport.LocalPackageMismatch + | VersionGroupReport.NonSemverMismatch | VersionGroupReport.HighestSemverMismatch | VersionGroupReport.LowestSemverMismatch, VersionGroupReport.Valid >[] { const instancesByName = groupBy('name', this.instances); - return Object.entries(instancesByName).map(([name, instances]) => { if (!hasMismatch(instances)) { return Effect.succeed( @@ -47,27 +46,27 @@ export class StandardVersionGroup extends Data.TaggedClass('Standard')<{ }), ); } - const wsInstance = getWorkspaceInstance(instances); - const wsFile = wsInstance?.packageJsonFile; - const wsVersion = wsFile?.contents?.version; - const isWorkspacePackage = wsInstance && wsVersion; - if (isWorkspacePackage) { + if (hasNonSemverSpecifiers(instances)) { return Effect.fail( - new VersionGroupReport.WorkspaceMismatch({ + new VersionGroupReport.NonSemverMismatch({ name, instances, isValid: false, - expectedVersion: wsVersion, - workspaceInstance: wsInstance, }), ); } - if (hasUnsupported(instances)) { + const localPackageInstance = getLocalPackageInstance(instances); + const localPackageFile = localPackageInstance?.packageJsonFile; + const localPackageVersion = localPackageFile?.contents?.version; + const isLocalPackage = localPackageInstance && localPackageVersion; + if (isLocalPackage) { return Effect.fail( - new VersionGroupReport.UnsupportedMismatch({ + new VersionGroupReport.LocalPackageMismatch({ name, instances, isValid: false, + expectedVersion: localPackageVersion, + localPackageInstance, }), ); } @@ -94,22 +93,22 @@ export class StandardVersionGroup extends Data.TaggedClass('Standard')<{ function getExpectedVersion( preferVersion: VersionGroupConfig.Standard['preferVersion'], - instances: Instance[], + instances: Instance.Any[], ): string { - const versions = getUniqueVersions(instances); + const versions = getUniqueSpecifiers(instances).map((i) => i.specifier); return unwrap( preferVersion === 'highestSemver' ? getHighestVersion(versions) : getLowestVersion(versions), ); } -function hasMismatch(instances: Instance[]): boolean { - return getUniqueVersions(instances).length > 1; +function hasMismatch(instances: Instance.Any[]): boolean { + return getUniqueSpecifiers(instances).length > 1; } -function hasUnsupported(instances: Instance[]): boolean { - return instances.some((instance) => !isSupported(instance.version)); +function hasNonSemverSpecifiers(instances: Instance.Any[]): boolean { + return instances.some((instance) => Option.isNone(instance.getSemverSpecifier())); } -function getWorkspaceInstance(instances: Instance[]): Instance | undefined { - return instances.find((instance) => instance.strategy.name === 'workspace'); +function getLocalPackageInstance(instances: Instance.Any[]): Instance.Any | undefined { + return instances.find((instance) => instance.strategy.name === 'localPackage'); } diff --git a/src/guards/can-add-to-group.ts b/src/guards/can-add-to-group.ts index 027ab4bb..9ccddd68 100644 --- a/src/guards/can-add-to-group.ts +++ b/src/guards/can-add-to-group.ts @@ -1,12 +1,12 @@ import { minimatch } from 'minimatch'; import { isNonEmptyArray } from 'tightrope/guard/is-non-empty-array'; -import type { Instance } from '../get-package-json-files/instance'; import type { AnySemverGroup } from '../get-semver-groups'; import type { AnyVersionGroup } from '../get-version-groups'; +import type { Instance } from '../instance'; export function canAddToGroup( group: AnySemverGroup | AnyVersionGroup, - instance: Instance, + instance: Instance.Any, ): boolean { const { dependencies, dependencyTypes, packages } = group.config; return ( diff --git a/src/guards/is-loose-semver.ts b/src/guards/is-loose-semver.ts index 2a93b1c4..d7fdf427 100644 --- a/src/guards/is-loose-semver.ts +++ b/src/guards/is-loose-semver.ts @@ -1,6 +1,7 @@ import { isString } from 'tightrope/guard/is-string'; import { isSemver } from './is-semver'; +/** @deprecated */ export function isLooseSemver(version: unknown): boolean { return isString(version) && isSemver(version) && version.search(/\.x(\.|$)/) !== -1; } diff --git a/src/guards/is-semver.ts b/src/guards/is-semver.ts index 9f27c6d4..8afca68d 100644 --- a/src/guards/is-semver.ts +++ b/src/guards/is-semver.ts @@ -1,5 +1,6 @@ import { isString } from 'tightrope/guard/is-string'; +/** @deprecated */ export function isSemver(version: unknown): boolean { const range = '(~|\\^|>=|>|<=|<)?'; const ints = '[0-9]+'; diff --git a/src/guards/is-supported.ts b/src/guards/is-supported.ts deleted file mode 100644 index 2be51355..00000000 --- a/src/guards/is-supported.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { isString } from 'tightrope/guard/is-string'; -import { isSemver } from './is-semver'; - -export function isSupported(version: unknown): version is string { - return version === '*' || isSemver(version) || isWorkspaceProtocol(version); -} -function isWorkspaceProtocol(version: unknown): boolean { - if (!isString(version)) return false; - if (!version.startsWith('workspace:')) return false; - const value = version.replace(/^workspace:/, ''); - return value === '*' || isSemver(value); -} diff --git a/src/guards/is-valid-semver-range.ts b/src/guards/is-valid-semver-range.ts index dae05426..443f5d4c 100644 --- a/src/guards/is-valid-semver-range.ts +++ b/src/guards/is-valid-semver-range.ts @@ -1,6 +1,7 @@ import type { SemverRange } from '../config/types'; import { RANGE } from '../constants'; +/** @deprecated */ export function isValidSemverRange(value: unknown): value is SemverRange { return ( value === RANGE.ANY || diff --git a/src/instance/create.ts b/src/instance/create.ts new file mode 100644 index 00000000..6f019385 --- /dev/null +++ b/src/instance/create.ts @@ -0,0 +1,153 @@ +import type { FileResult, HostedGitResult, RegistryResult } from 'npm-package-arg'; +import npa from 'npm-package-arg'; +import type { NpmPackageArgResult, WorkspaceProtocolResult } from '.'; +import { Instance } from '.'; +import type { Strategy } from '../config/get-custom-types'; +import type { PackageJsonFile } from '../get-package-json-files/package-json-file'; + +export function createInstance( + strategy: Strategy.Any, + name: string, + packageJsonFile: PackageJsonFile, + specifier: string, +): Instance.Any { + const pkgName = packageJsonFile.contents.name || 'PACKAGE_JSON_HAS_NO_NAME'; + try { + const parsedSpecifier = parseSpecifier(name, specifier, packageJsonFile); + switch (parsedSpecifier.type) { + case 'file': + case 'directory': { + return new Instance.FileInstance({ + name, + packageJsonFile, + parsedSpecifier, + pkgName, + specifier, + strategy, + }); + } + case 'remote': { + return new Instance.UrlInstance({ + name, + packageJsonFile, + parsedSpecifier, + pkgName, + specifier, + strategy, + }); + } + case 'git': { + return 'hosted' in parsedSpecifier + ? new Instance.HostedGitInstance({ + name, + packageJsonFile, + parsedSpecifier: parsedSpecifier as HostedGitResult, + pkgName, + specifier, + strategy, + }) + : new Instance.UrlInstance({ + name, + packageJsonFile, + parsedSpecifier, + pkgName, + specifier, + strategy, + }); + } + case 'alias': { + return new Instance.AliasInstance({ + name, + packageJsonFile, + parsedSpecifier, + pkgName, + specifier, + strategy, + }); + } + case 'version': { + return new Instance.VersionInstance({ + name, + packageJsonFile, + parsedSpecifier: parsedSpecifier as RegistryResult & { type: 'version' }, + pkgName, + specifier, + strategy, + }); + } + case 'range': { + return new Instance.RangeInstance({ + name, + packageJsonFile, + parsedSpecifier: parsedSpecifier as RegistryResult & { type: 'range' }, + pkgName, + specifier, + strategy, + }); + } + case 'tag': { + return new Instance.TagInstance({ + name, + packageJsonFile, + parsedSpecifier: parsedSpecifier as RegistryResult & { type: 'tag' }, + pkgName, + specifier, + strategy, + }); + } + case 'workspaceProtocol': { + return new Instance.WorkspaceProtocolInstance({ + name, + packageJsonFile, + parsedSpecifier: parsedSpecifier as WorkspaceProtocolResult, + pkgName, + specifier, + strategy, + }); + } + default: { + return new Instance.UnsupportedInstance({ + name, + packageJsonFile, + parsedSpecifier, + pkgName, + specifier, + strategy, + }); + } + } + } catch (err) { + return new Instance.UnsupportedInstance({ + name, + packageJsonFile, + parsedSpecifier: err, + pkgName, + specifier, + strategy, + }); + } +} + +function parseSpecifier( + name: string, + specifier: string, + packageJsonFile: PackageJsonFile, +): NpmPackageArgResult { + if (specifier.startsWith('workspace:')) { + const parsed = npa.resolve( + name, + specifier.replace('workspace:', 'file:'), + packageJsonFile.dirPath, + ) as FileResult; + return { + type: 'workspaceProtocol', + raw: parsed.raw.replace('file:', 'workspace:'), + name: parsed.name, + escapedName: parsed.escapedName, + scope: parsed.scope, + rawSpec: parsed.rawSpec.replace('file:', 'workspace:'), + saveSpec: parsed.saveSpec.replace('file:', 'workspace:'), + }; + } + return npa.resolve(name, specifier, packageJsonFile.dirPath); +} diff --git a/src/instance/index.ts b/src/instance/index.ts new file mode 100644 index 00000000..4f5cf75d --- /dev/null +++ b/src/instance/index.ts @@ -0,0 +1,176 @@ +import * as Data from '@effect/data/Data'; +import { pipe } from '@effect/data/Function'; +import * as Option from '@effect/data/Option'; +import type npa from 'npm-package-arg'; +import type { + AliasResult, + FileResult, + HostedGitResult, + RegistryResult, + URLResult, +} from 'npm-package-arg'; +import type { Strategy } from '../config/get-custom-types'; +import type { PackageJsonFile } from '../get-package-json-files/package-json-file'; +import type { Delete } from '../get-version-groups/lib/delete'; +import { $R } from '../lib/$R'; + +/** Extends npm/npm-package-arg to support "workspace:*" */ +export interface WorkspaceProtocolResult { + type: 'workspaceProtocol'; + raw: string; + name: string | null; + escapedName: string | null; + scope: string | null; + rawSpec: string; + saveSpec: string; +} + +export type NpmPackageArgResult = ReturnType | WorkspaceProtocolResult; + +interface BaseInstance { + /** the name of this dependency */ + name: string; + /** The package this dependency is installed in this specific time */ + packageJsonFile: PackageJsonFile; + /** @see https://github.com/npm/npm-package-arg */ + parsedSpecifier: T; + /** The .name property of the package.json file of this instance */ + pkgName: string; + /** The raw: semver, git, workspace: etc version value */ + specifier: string; + /** locates where in the file this dependency is installed */ + strategy: Strategy.Any; +} + +export namespace Instance { + /** + * A helper to create specific classes for each of the possible + * `RegistryResult` types from npm/npm-package-arg. Instead of grouping them + * together we are being more specific + */ + type SpecificRegistryResult = Omit< + RegistryResult, + 'type' + > & { + type: T; + }; + + /** An `Instance` whose specifier is a path to a local file or directory */ + export class FileInstance extends Data.TaggedClass('FileInstance')> { + setSpecifier = setSpecifier; + + getSemverSpecifier(): Option.Option { + return Option.none(); + } + } + + /** An `Instance` whose specifier is a Git URL */ + export class HostedGitInstance extends Data.TaggedClass('HostedGitInstance')< + BaseInstance + > { + setSpecifier = setSpecifier; + + getSemverSpecifier(): Option.Option { + // @TODO: If git tag is semver, return that + return Option.none(); + } + } + + /** An `Instance` whose specifier is a URL to a tarball */ + export class UrlInstance extends Data.TaggedClass('URLInstance')> { + setSpecifier = setSpecifier; + + getSemverSpecifier(): Option.Option { + // @TODO: If file name is semver, return that + return Option.none(); + } + } + + /** An `Instance` whose specifier is eg "npm:imageoptim-cli@3.1.7" */ + export class AliasInstance extends Data.TaggedClass('AliasInstance')> { + setSpecifier = setSpecifier; + + getSemverSpecifier(): Option.Option { + const subSpec = this.parsedSpecifier.subSpec; + if (['range', 'version'].includes(subSpec.type) && subSpec.fetchSpec !== null) { + return Option.some(subSpec.fetchSpec); + } + return Option.none(); + } + } + + /** An `Instance` whose specifier is exact semver */ + export class VersionInstance extends Data.TaggedClass('VersionInstance')< + BaseInstance> + > { + setSpecifier = setSpecifier; + + getSemverSpecifier(): Option.Option { + return Option.some(this.parsedSpecifier.fetchSpec); + } + } + + /** An `Instance` whose specifier is eg. "*" or "^1.2.3" */ + export class RangeInstance extends Data.TaggedClass('RangeInstance')< + BaseInstance> + > { + setSpecifier = setSpecifier; + + getSemverSpecifier(): Option.Option { + return Option.some(this.parsedSpecifier.fetchSpec); + } + } + + /** An `Instance` whose specifier is eg. "latest" or "made-up-by-some-dev" */ + export class TagInstance extends Data.TaggedClass('TagInstance')< + BaseInstance> + > { + setSpecifier = setSpecifier; + + getSemverSpecifier(): Option.Option { + return Option.none(); + } + } + + /** An `Instance` whose specifier is "workspace:*" */ + export class WorkspaceProtocolInstance extends Data.TaggedClass('WorkspaceProtocolInstance')< + BaseInstance + > { + setSpecifier = setSpecifier; + + getSemverSpecifier(): Option.Option { + return Option.none(); + } + } + + /** An `Instance` whose specifier is not supported by npm */ + export class UnsupportedInstance extends Data.TaggedClass('UnsupportedInstance')< + BaseInstance + > { + setSpecifier = setSpecifier; + + getSemverSpecifier(): Option.Option { + return Option.none(); + } + } + + export type Any = + | FileInstance + | HostedGitInstance + | UrlInstance + | AliasInstance + | VersionInstance + | RangeInstance + | TagInstance + | WorkspaceProtocolInstance + | UnsupportedInstance; +} + +/** + * In the case of banned dependencies, their version is set to `undefined`, + * which causes them to be removed by `JSON.stringify`. + */ +function setSpecifier(this: Instance.Any, version: string | Delete): void { + const file = this.packageJsonFile; + pipe(this.strategy.write(file, [this.name, version]), $R.tapErrVerbose); +} diff --git a/src/strategy/name-and-version-props.spec.ts b/src/strategy/name-and-version-props.spec.ts index 909c557d..3eb92c63 100644 --- a/src/strategy/name-and-version-props.spec.ts +++ b/src/strategy/name-and-version-props.spec.ts @@ -4,7 +4,7 @@ import { PackageJsonFile } from '../get-package-json-files/package-json-file'; import { NameAndVersionPropsStrategy } from './name-and-version-props'; it('gets and sets a name and version from 2 seperate locations', () => { - const strategy = new NameAndVersionPropsStrategy('workspace', 'version', 'name'); + const strategy = new NameAndVersionPropsStrategy('localPackage', 'version', 'name'); const jsonFile = mockPackage('foo', { otherProps: { version: '1.2.3' } }); const file = new PackageJsonFile(jsonFile, {} as any); const initial = [['foo', '1.2.3']]; @@ -31,14 +31,14 @@ it('gets and sets a name and version from 2 seperate nested locations', () => { }); it('returns new Err when namePath is not found', () => { - const strategy = new NameAndVersionPropsStrategy('workspace', 'version', 'never.gonna'); + const strategy = new NameAndVersionPropsStrategy('localPackage', 'version', 'never.gonna'); const jsonFile = mockPackage('foo', { otherProps: { version: '1.2.3' } }); const file = new PackageJsonFile(jsonFile, {} as any); expect(strategy.read(file)).toEqual(new Err(expect.any(Error))); }); it('returns new Err when version (path) is not found', () => { - const strategy = new NameAndVersionPropsStrategy('workspace', 'never.gonna', 'name'); + const strategy = new NameAndVersionPropsStrategy('localPackage', 'never.gonna', 'name'); const jsonFile = mockPackage('foo', {}); const file = new PackageJsonFile(jsonFile, {} as any); expect(strategy.read(file)).toEqual(new Err(expect.any(Error))); diff --git a/src/strategy/named-version-string.spec.ts b/src/strategy/named-version-string.spec.ts index a15ff604..d99e6b9d 100644 --- a/src/strategy/named-version-string.spec.ts +++ b/src/strategy/named-version-string.spec.ts @@ -4,7 +4,7 @@ import { PackageJsonFile } from '../get-package-json-files/package-json-file'; import { NamedVersionStringStrategy } from './named-version-string'; it('gets and sets a name and version from a single string', () => { - const strategy = new NamedVersionStringStrategy('workspace', 'packageManager'); + const strategy = new NamedVersionStringStrategy('localPackage', 'packageManager'); const jsonFile = mockPackage('foo', { otherProps: { packageManager: 'yarn@1.2.3' } }); const file = new PackageJsonFile(jsonFile, {} as any); const initial = [['yarn', '1.2.3']]; @@ -30,7 +30,7 @@ it('gets and sets a name and version from a single string nested location', () = }); it('returns new Err when path is not found', () => { - const strategy = new NamedVersionStringStrategy('workspace', 'never.gonna'); + const strategy = new NamedVersionStringStrategy('localPackage', 'never.gonna'); const jsonFile = mockPackage('foo', {}); const file = new PackageJsonFile(jsonFile, {} as any); expect(strategy.read(file)).toEqual(new Err(expect.any(Error))); diff --git a/src/strategy/unnamed-version-string.spec.ts b/src/strategy/unnamed-version-string.spec.ts index 5803d505..208421ca 100644 --- a/src/strategy/unnamed-version-string.spec.ts +++ b/src/strategy/unnamed-version-string.spec.ts @@ -4,7 +4,7 @@ import { PackageJsonFile } from '../get-package-json-files/package-json-file'; import { UnnamedVersionStringStrategy } from './unnamed-version-string'; it('gets and sets an anonymous version from a single string', () => { - const strategy = new UnnamedVersionStringStrategy('workspace', 'someVersion'); + const strategy = new UnnamedVersionStringStrategy('localPackage', 'someVersion'); const jsonFile = mockPackage('foo', { otherProps: { someVersion: '1.2.3' } }); const file = new PackageJsonFile(jsonFile, {} as any); const initial = [['someVersion', '1.2.3']]; @@ -30,7 +30,7 @@ it('gets and sets an anonymous version from a single string in a nested location }); it('returns new Err when path is not found', () => { - const strategy = new UnnamedVersionStringStrategy('workspace', 'never.gonna'); + const strategy = new UnnamedVersionStringStrategy('localPackage', 'never.gonna'); const jsonFile = mockPackage('foo', {}); const file = new PackageJsonFile(jsonFile, {} as any); expect(strategy.read(file)).toEqual(new Err(expect.any(Error))); diff --git a/src/strategy/versions-by-name.spec.ts b/src/strategy/versions-by-name.spec.ts index e28daeb3..9f8c00ab 100644 --- a/src/strategy/versions-by-name.spec.ts +++ b/src/strategy/versions-by-name.spec.ts @@ -4,7 +4,7 @@ import { PackageJsonFile } from '../get-package-json-files/package-json-file'; import { VersionsByNameStrategy } from './versions-by-name'; it('gets and sets names and versions in an object', () => { - const strategy = new VersionsByNameStrategy('workspace', 'dependencies'); + const strategy = new VersionsByNameStrategy('localPackage', 'dependencies'); const jsonFile = mockPackage('foo', { deps: ['bar@1.2.3', 'baz@4.4.4'] }); const file = new PackageJsonFile(jsonFile, {} as any); const initial = [ @@ -40,7 +40,7 @@ it('gets and sets a name and version from a single string nested location', () = }); it('returns new Err when path is not found', () => { - const strategy = new VersionsByNameStrategy('workspace', 'never.gonna'); + const strategy = new VersionsByNameStrategy('localPackage', 'never.gonna'); const jsonFile = mockPackage('foo', {}); const file = new PackageJsonFile(jsonFile, {} as any); expect(strategy.read(file)).toEqual(new Err(expect.any(Error))); diff --git a/test/matchers/semver-group.ts b/test/matchers/semver-group.ts index 2bd42bf3..a5e05615 100644 --- a/test/matchers/semver-group.ts +++ b/test/matchers/semver-group.ts @@ -1,12 +1,12 @@ import 'expect-more-jest'; -import { Instance } from '../../src/get-package-json-files/instance'; import type { SemverGroupReport } from '../../src/get-semver-groups'; +import { toBeSupportedInstance } from './version-group'; export function toBeFilteredOut({ name }: Pick) { return expect.objectContaining({ _tag: 'FilteredOut', name, - instance: expect.any(Instance), + instance: toBeSupportedInstance(), isValid: true, }); } @@ -15,7 +15,7 @@ export function toBeIgnored({ name }: Pick) { return expect.objectContaining({ _tag: 'Ignored', name, - instance: expect.any(Instance), + instance: toBeSupportedInstance(), isValid: true, }); } @@ -24,19 +24,19 @@ export function toBeValid({ name }: Pick) { return expect.objectContaining({ _tag: 'Valid', name, - instance: expect.any(Instance), + instance: toBeSupportedInstance(), isValid: true, }); } -export function toBeWorkspaceSemverRangeMismatch({ +export function toBeLocalPackageSemverRangeMismatch({ name, expectedVersion, -}: Pick) { +}: Pick) { return expect.objectContaining({ - _tag: 'WorkspaceSemverRangeMismatch', + _tag: 'LocalPackageSemverRangeMismatch', name, - instance: expect.any(Instance), + instance: toBeSupportedInstance(), isValid: false, expectedVersion, }); @@ -49,19 +49,17 @@ export function toBeSemverRangeMismatch({ return expect.objectContaining({ _tag: 'SemverRangeMismatch', name, - instance: expect.any(Instance), + instance: toBeSupportedInstance(), isValid: false, expectedVersion, }); } -export function toBeUnsupportedVersion({ - name, -}: Pick) { +export function toBeNonSemverVersion({ name }: Pick) { return expect.objectContaining({ - _tag: 'UnsupportedVersion', + _tag: 'NonSemverVersion', name, - instance: expect.any(Instance), + instance: toBeSupportedInstance(), isValid: false, }); } diff --git a/test/matchers/version-group.ts b/test/matchers/version-group.ts index b42b1ce9..5a5dcb92 100644 --- a/test/matchers/version-group.ts +++ b/test/matchers/version-group.ts @@ -1,12 +1,19 @@ import 'expect-more-jest'; -import { Instance } from '../../src/get-package-json-files/instance'; import type { VersionGroupReport } from '../../src/get-version-groups'; +export function toBeSupportedInstance() { + return expect.objectContaining({ + _tag: expect.stringMatching( + /^(File|HostedGit|Url|Alias|Version|Range|Tag|WorkspaceProtocol|Unsupported)Instance$/, + ), + }); +} + export function toBeBanned({ name }: Pick) { return expect.objectContaining({ _tag: 'Banned', name, - instances: expect.toBeArrayIncludingOnly([expect.any(Instance)]), + instances: expect.toBeArrayIncludingOnly([toBeSupportedInstance()]), isValid: false, }); } @@ -15,7 +22,7 @@ export function toBeFilteredOut({ name }: Pick) return expect.objectContaining({ _tag: 'Ignored', name, - instances: expect.toBeArrayIncludingOnly([expect.any(Instance)]), + instances: expect.toBeArrayIncludingOnly([toBeSupportedInstance()]), isValid: true, }); } @@ -49,7 +56,7 @@ export function toBeLowestSemverMismatch({ return expect.objectContaining({ _tag: 'LowestSemverMismatch', name, - instances: expect.toBeArrayIncludingOnly([expect.any(Instance)]), + instances: expect.toBeArrayIncludingOnly([toBeSupportedInstance()]), isValid: false, expectedVersion, }); @@ -62,7 +69,7 @@ export function toBePinnedMismatch({ return expect.objectContaining({ _tag: 'PinnedMismatch', name, - instances: expect.toBeArrayIncludingOnly([expect.any(Instance)]), + instances: expect.toBeArrayIncludingOnly([toBeSupportedInstance()]), isValid: false, expectedVersion, }); @@ -74,7 +81,7 @@ export function toBeSameRangeMismatch({ return expect.objectContaining({ _tag: 'SameRangeMismatch', name, - instances: expect.toBeArrayIncludingOnly([expect.any(Instance)]), + instances: expect.toBeArrayIncludingOnly([toBeSupportedInstance()]), isValid: false, }); } @@ -86,19 +93,19 @@ export function toBeSnappedToMismatch({ return expect.objectContaining({ _tag: 'SnappedToMismatch', name, - instances: expect.toBeArrayIncludingOnly([expect.any(Instance)]), + instances: expect.toBeArrayIncludingOnly([toBeSupportedInstance()]), isValid: false, expectedVersion, }); } -export function toBeUnsupportedMismatch({ +export function toBeNonSemverMismatch({ name, -}: Pick) { +}: Pick) { return expect.objectContaining({ - _tag: 'UnsupportedMismatch', + _tag: 'NonSemverMismatch', name, - instances: expect.toBeArrayIncludingOnly([expect.any(Instance)]), + instances: expect.toBeArrayIncludingOnly([toBeSupportedInstance()]), isValid: false, }); } @@ -107,21 +114,21 @@ export function toBeValid({ name }: Pick) { return expect.objectContaining({ _tag: 'Valid', name, - instances: expect.toBeArrayIncludingOnly([expect.any(Instance)]), + instances: expect.toBeArrayIncludingOnly([toBeSupportedInstance()]), isValid: true, }); } -export function toBeWorkspaceMismatch({ +export function toBeLocalPackageMismatch({ name, expectedVersion, -}: Pick) { +}: Pick) { return expect.objectContaining({ - _tag: 'WorkspaceMismatch', + _tag: 'LocalPackageMismatch', name, - instances: expect.toBeArrayIncludingOnly([expect.any(Instance)]), + instances: expect.toBeArrayIncludingOnly([toBeSupportedInstance()]), isValid: false, expectedVersion, - workspaceInstance: expect.any(Instance), + localPackageInstance: toBeSupportedInstance(), }); } diff --git a/test/mock-env.ts b/test/mock-env.ts index 2e85cd73..a0815f83 100644 --- a/test/mock-env.ts +++ b/test/mock-env.ts @@ -46,9 +46,9 @@ export function createMockSemverRangeEffects(): SemverRangeEffects { onIgnored: jest.fn(() => Effect.unit()), onSemverRangeMismatch: jest.fn(() => Effect.unit()), onComplete: jest.fn(() => Effect.unit()), - onUnsupportedVersion: jest.fn(() => Effect.unit()), + onNonSemverVersion: jest.fn(() => Effect.unit()), onValid: jest.fn(() => Effect.unit()), - onWorkspaceSemverRangeMismatch: jest.fn(() => Effect.unit()), + onLocalPackageSemverRangeMismatch: jest.fn(() => Effect.unit()), }; } @@ -63,8 +63,8 @@ export function createMockVersionEffects(): VersionEffects { onSameRangeMismatch: jest.fn(() => Effect.unit()), onSnappedToMismatch: jest.fn(() => Effect.unit()), onComplete: jest.fn(() => Effect.unit()), - onUnsupportedMismatch: jest.fn(() => Effect.unit()), + onNonSemverMismatch: jest.fn(() => Effect.unit()), onValid: jest.fn(() => Effect.unit()), - onWorkspaceMismatch: jest.fn(() => Effect.unit()), + onLocalPackageMismatch: jest.fn(() => Effect.unit()), }; } diff --git a/test/scenarios/semver-groups/workspace-range-mismatch.spec.ts b/test/scenarios/semver-groups/workspace-range-mismatch.spec.ts index 8630db5e..07838801 100644 --- a/test/scenarios/semver-groups/workspace-range-mismatch.spec.ts +++ b/test/scenarios/semver-groups/workspace-range-mismatch.spec.ts @@ -3,7 +3,7 @@ import { lintSemverRanges } from '../../../src/bin-lint-semver-ranges/lint-semve import { list } from '../../../src/bin-list/list'; import { prompt } from '../../../src/bin-prompt/prompt'; import { setSemverRanges } from '../../../src/bin-set-semver-ranges/set-semver-ranges'; -import { toBeValid, toBeWorkspaceSemverRangeMismatch } from '../../matchers/semver-group'; +import { toBeLocalPackageSemverRangeMismatch, toBeValid } from '../../matchers/semver-group'; import { mockPackage } from '../../mock'; import { createScenario } from '../lib/create-scenario'; @@ -49,7 +49,7 @@ describe('semverGroups', () => { const scenario = getScenario(); expect(scenario.report.semverGroups).toEqual([ [ - toBeWorkspaceSemverRangeMismatch({ expectedVersion: '0.1.1', name: 'b' }), + toBeLocalPackageSemverRangeMismatch({ expectedVersion: '0.1.1', name: 'b' }), toBeValid({ name: 'foo' }), ], ]); diff --git a/test/scenarios/version-groups/unsupported-mismatch.spec.ts b/test/scenarios/version-groups/unsupported-mismatch.spec.ts index 69d708cc..322b0dad 100644 --- a/test/scenarios/version-groups/unsupported-mismatch.spec.ts +++ b/test/scenarios/version-groups/unsupported-mismatch.spec.ts @@ -5,11 +5,11 @@ import { lint } from '../../../src/bin-lint/lint'; import { listMismatches } from '../../../src/bin-list-mismatches/list-mismatches'; import { list } from '../../../src/bin-list/list'; import { prompt } from '../../../src/bin-prompt/prompt'; -import { toBeUnsupportedMismatch } from '../../matchers/version-group'; +import { toBeNonSemverMismatch } from '../../matchers/version-group'; import { createScenarioVariants } from './lib/create-scenario-variants'; describe('versionGroups', () => { - describe('UnsupportedMismatch', () => { + describe('NonSemverMismatch', () => { const cases: [string, string][] = [ [ 'yarn@git://github.com/user/project.git#commit1', @@ -34,7 +34,7 @@ describe('versionGroups', () => { test('should identify as mismatching, but not possible to fix', () => { const scenario = getScenario(); expect(scenario.report.versionGroups).toEqual([ - [toBeUnsupportedMismatch({ name: 'yarn' })], + [toBeNonSemverMismatch({ name: 'yarn' })], ]); }); }); diff --git a/test/scenarios/version-groups/workspace-mismatch-nested.spec.ts b/test/scenarios/version-groups/workspace-mismatch-nested.spec.ts index 1991d5ba..070294f7 100644 --- a/test/scenarios/version-groups/workspace-mismatch-nested.spec.ts +++ b/test/scenarios/version-groups/workspace-mismatch-nested.spec.ts @@ -3,12 +3,12 @@ import { fixMismatches } from '../../../src/bin-fix-mismatches/fix-mismatches'; import { listMismatches } from '../../../src/bin-list-mismatches/list-mismatches'; import { list } from '../../../src/bin-list/list'; import { prompt } from '../../../src/bin-prompt/prompt'; -import { toBeWorkspaceMismatch } from '../../matchers/version-group'; +import { toBeLocalPackageMismatch } from '../../matchers/version-group'; import { mockPackage } from '../../mock'; import { createScenario } from '../lib/create-scenario'; describe('versionGroups', () => { - describe('WorkspaceMismatch', () => { + describe('LocalPackageMismatch', () => { describe('some packages are nested within sub-folders on the file system', () => { [ () => @@ -158,7 +158,7 @@ describe('versionGroups', () => { test('should identify as a mismatch against the canonical local package version', () => { const scenario = getScenario(); expect(scenario.report.versionGroups).toEqual([ - [toBeWorkspaceMismatch({ expectedVersion: '0.0.1', name: 'c' })], + [toBeLocalPackageMismatch({ expectedVersion: '0.0.1', name: 'c' })], ]); }); }); diff --git a/test/scenarios/version-groups/workspace-mismatch.spec.ts b/test/scenarios/version-groups/workspace-mismatch.spec.ts index 186f4aa1..fc8d0b39 100644 --- a/test/scenarios/version-groups/workspace-mismatch.spec.ts +++ b/test/scenarios/version-groups/workspace-mismatch.spec.ts @@ -4,12 +4,12 @@ import { lint } from '../../../src/bin-lint/lint'; import { listMismatches } from '../../../src/bin-list-mismatches/list-mismatches'; import { list } from '../../../src/bin-list/list'; import { prompt } from '../../../src/bin-prompt/prompt'; -import { toBeWorkspaceMismatch } from '../../matchers/version-group'; +import { toBeLocalPackageMismatch } from '../../matchers/version-group'; import { mockPackage } from '../../mock'; import { createScenario } from '../lib/create-scenario'; describe('versionGroups', () => { - describe('WorkspaceMismatch', () => { + describe('LocalPackageMismatch', () => { [ () => createScenario( @@ -162,7 +162,7 @@ describe('versionGroups', () => { test('should identify as a mismatch against the canonical local package version', () => { const scenario = getScenario(); expect(scenario.report.versionGroups).toEqual([ - [toBeWorkspaceMismatch({ expectedVersion: '0.0.1', name: 'c' })], + [toBeLocalPackageMismatch({ expectedVersion: '0.0.1', name: 'c' })], ]); }); }); diff --git a/yarn.lock b/yarn.lock index 4471ac71..b23dd2ef 100644 --- a/yarn.lock +++ b/yarn.lock @@ -868,6 +868,11 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.36.tgz#c414052cb9d43fab67d679d5f3c641be911f5835" integrity sha512-FXKWbsJ6a1hIrRxv+FoukuHnGTgEzKYGi7kilfMae96AL9UNkPFNWJEEYWzdRI9ooIkbr4AKldyuSTLql06vLQ== +"@types/npm-package-arg@6.1.1": + version "6.1.1" + resolved "https://registry.yarnpkg.com/@types/npm-package-arg/-/npm-package-arg-6.1.1.tgz#9e2d8adc04d39824a3d9f36f738010a3f7da3c1a" + integrity sha512-452/1Kp9IdM/oR10AyqAgZOxUt7eLbm+EMJ194L6oarMYdZNiFIFAOJ7IIr0OrZXTySgfHjJezh2oiyk2kc3ag== + "@types/prettier@^2.1.5": version "2.7.3" resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.7.3.tgz#3e51a17e291d01d17d3fc61422015a933af7a08f" @@ -1306,6 +1311,13 @@ buffer@^5.5.0: base64-js "^1.3.1" ieee754 "^1.1.13" +builtins@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/builtins/-/builtins-5.0.1.tgz#87f6db9ab0458be728564fa81d876d8d74552fa9" + integrity sha512-qwVpFEHNfhYJIzNRBvd2C1kyo6jz3ZSMPyyuR47OPdiKWlbYnZNyDWuyR175qDnAJLiCo5fBBqPb3RiXgWlkOQ== + dependencies: + semver "^7.0.0" + call-bind@^1.0.0, call-bind@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" @@ -2149,6 +2161,13 @@ has@^1.0.3: dependencies: function-bind "^1.1.1" +hosted-git-info@^6.0.0: + version "6.1.1" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-6.1.1.tgz#629442c7889a69c05de604d52996b74fe6f26d58" + integrity sha512-r0EI+HBMcXadMrugk0GCQ+6BQV39PiWAZVfq7oIckeGiN7sjRGyQxPdft3nQekFTCQbYxLBH+/axZMeH8UX6+w== + dependencies: + lru-cache "^7.5.1" + html-escaper@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" @@ -2923,6 +2942,11 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" +lru-cache@^7.5.1: + version "7.18.3" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.18.3.tgz#f793896e0fd0e954a59dfdd82f0773808df6aa89" + integrity sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA== + lru-cache@^9.1.1: version "9.1.2" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-9.1.2.tgz#255fdbc14b75589d6d0e73644ca167a8db506835" @@ -3036,6 +3060,16 @@ normalize-path@^3.0.0: resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== +npm-package-arg@10.1.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/npm-package-arg/-/npm-package-arg-10.1.0.tgz#827d1260a683806685d17193073cc152d3c7e9b1" + integrity sha512-uFyyCEmgBfZTtrKk/5xDfHp6+MdrqGotX/VoOyEEl3mBwiEE5FlBaePanazJSVMPT7vKepcjYBY2ztg9A3yPIA== + dependencies: + hosted-git-info "^6.0.0" + proc-log "^3.0.0" + semver "^7.3.5" + validate-npm-package-name "^5.0.0" + npm-run-path@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" @@ -3237,6 +3271,11 @@ pretty-format@^29.0.0, pretty-format@^29.4.1, pretty-format@^29.5.0: ansi-styles "^5.0.0" react-is "^18.0.0" +proc-log@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/proc-log/-/proc-log-3.0.0.tgz#fb05ef83ccd64fd7b20bbe9c8c1070fc08338dd8" + integrity sha512-++Vn7NS4Xf9NacaU9Xq3URUuqZETPsf8L4j5/ckhaRYsfPeRyzGw+iDjFhV/Jr3uNmTvvddEJFWh5R1gRgUH8A== + prompts@2.4.2, prompts@^2.0.1: version "2.4.2" resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069" @@ -3387,6 +3426,13 @@ semver@^6.0.0, semver@^6.3.0: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== +semver@^7.0.0: + version "7.5.4" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" + integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== + dependencies: + lru-cache "^6.0.0" + semver@^7.3.5, semver@^7.3.7: version "7.5.1" resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.1.tgz#c90c4d631cf74720e46b21c1d37ea07edfab91ec" @@ -3738,6 +3784,13 @@ v8-to-istanbul@^9.0.1: "@types/istanbul-lib-coverage" "^2.0.1" convert-source-map "^1.6.0" +validate-npm-package-name@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/validate-npm-package-name/-/validate-npm-package-name-5.0.0.tgz#f16afd48318e6f90a1ec101377fa0384cfc8c713" + integrity sha512-YuKoXDAhBYxY7SfOKxHBDoSyENFeW5VvIIQp2TGQuit8gpK6MnWaQelBKxso72DoxTZfZdcP3W90LqpSkgPzLQ== + dependencies: + builtins "^5.0.0" + walker@^1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.8.tgz#bd498db477afe573dc04185f011d3ab8a8d7653f"