diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 821c03b..d2e4d76 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,8 +1,12 @@ # Name of the pipeline name: CI -# When pushing to any branch -on: [push, pull_request] +# When pushing to `master` or when there is a PR for the branch. +on: + pull_request: + push: + branches: + - 'master' jobs: ci: @@ -40,10 +44,8 @@ jobs: run: npm run cover - name: Coveralls - uses: coverallsapp/github-action@1.1.3 + uses: coverallsapp/github-action@v2 if: ${{ matrix.version == 'current' }} - with: - github-token: ${{ secrets.github_token }} # Cancel running workflows for the same branch when a new one is started. concurrency: diff --git a/.typesyncrc.yaml b/.typesyncrc.yaml index 11c04b5..4b7aa3b 100644 --- a/.typesyncrc.yaml +++ b/.typesyncrc.yaml @@ -3,3 +3,9 @@ ignorePackages: - nodemon - prettier - rimraf + - eslint + # Can't upgrade these libraries yet since they require ESM. + - chalk + - detect-indent + - ora + - eslint-config-prettier diff --git a/CHANGELOG.md b/CHANGELOG.md index ca53e40..84249e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,13 @@ +# v0.12.0 + +- **[BREAKING CHANGE]** [#86](https://github.com/jeffijoe/typesync/issues/86): Use the DefinitelyTyped strategy for resolving typings versions. This also means we no longer use the existing semver range specifier used in `package.json`. +- **[BREAKING CHANGE]** Bump minimum supported Node version to 16. +- The success message after running `typesync` now indicates when `--dry` is used. +- Upgrade packages. + # v0.11.1 -- [#79](https://github.com/jeffijoe/typesync/issues/79): Ignore deprecated `@typings/` packages. +- [#79](https://github.com/jeffijoe/typesync/issues/79): Ignore deprecated `@typings/` packages. - Upgrade packages. # v0.11.0 diff --git a/README.md b/README.md index bae38de..ffd3ef5 100644 --- a/README.md +++ b/README.md @@ -120,7 +120,9 @@ TypeSync will add typings for packages that: TypeSync will try to respect semver parity for the code and typings packages, and will fall back to the latest available typings package. -If you use a Semver `^` or `~` for a package, the same prefix will be used for the typings package. If you pin to an exact version (`"some-package": "1.2.3"`), no prefix will be written. +When writing the typings package version to `package.json`, the `~` semver range is used. This is because typings published via [DefinitelyTyped](https://github.com/DefinitelyTyped/DefinitelyTyped#how-do-definitely-typed-package-versions-relate-to-versions-of-the-corresponding-library) align typings versions with library versions using major and minor only. + +For example, if you depend on `react@^16.14.0`, then TypeSync will only look for typings packages that match `16.14.*`. # Monorepos @@ -138,4 +140,4 @@ See [CHANGELOG.md](/CHANGELOG.md) Jeff Hansen - [@Jeffijoe](https://twitter.com/jeffijoe) - [cosmiconfig]: https://github.com/davidtheclark/cosmiconfig +[cosmiconfig]: https://github.com/davidtheclark/cosmiconfig diff --git a/src/__tests__/type-syncer.test.ts b/src/__tests__/type-syncer.test.ts index 8da051d..6f6e247 100644 --- a/src/__tests__/type-syncer.test.ts +++ b/src/__tests__/type-syncer.test.ts @@ -226,14 +226,14 @@ describe('type syncer', () => { packageService.writePackageFile as jest.Mock ).mock.calls.find((c) => c[0] === 'package.json')[1] as IPackageFile expect(writtenPackage.devDependencies).toEqual({ - '@types/package1': '^1.0.0', - '@types/package3': '^1.0.0', + '@types/package1': '~1.0.0', + '@types/package3': '~1.0.0', '@types/package4': '^1.0.0', - '@types/package5': '^1.0.0', - '@types/myorg__package7': '^1.0.0', + '@types/package5': '~1.0.0', + '@types/myorg__package7': '~1.0.0', '@types/package8': '~1.0.0', - '@types/package9': '1.0.0', - '@types/packageWithOldTypings': '^2.0.0', + '@types/package9': '~1.0.0', + '@types/packageWithOldTypings': '~2.0.0', package4: '^1.0.0', package5: '^1.0.0', }) @@ -276,16 +276,16 @@ describe('type syncer', () => { (c) => c[0] === 'package-ignore-dev.json', )[1] as IPackageFile expect(writtenPackage.devDependencies).toEqual({ - '@types/package1': '^1.0.0', - '@types/package3': '^1.0.0', + '@types/package1': '~1.0.0', + '@types/package3': '~1.0.0', // Package 4's typings were already in the root package's `devDependencies`, // but package 5's were not, that's why we still write package4's typings but not // package 5's. '@types/package4': '^1.0.0', - '@types/myorg__package7': '^1.0.0', + '@types/myorg__package7': '~1.0.0', '@types/package8': '~1.0.0', - '@types/package9': '1.0.0', - '@types/packageWithOldTypings': '^2.0.0', + '@types/package9': '~1.0.0', + '@types/packageWithOldTypings': '~2.0.0', package4: '^1.0.0', package5: '^1.0.0', }) @@ -300,13 +300,13 @@ describe('type syncer', () => { (c) => c[0] === 'package-ignore-package1.json', )[1] as IPackageFile expect(writtenPackage.devDependencies).toEqual({ - '@types/package3': '^1.0.0', + '@types/package3': '~1.0.0', '@types/package4': '^1.0.0', - '@types/package5': '^1.0.0', - '@types/myorg__package7': '^1.0.0', + '@types/package5': '~1.0.0', + '@types/myorg__package7': '~1.0.0', '@types/package8': '~1.0.0', - '@types/package9': '1.0.0', - '@types/packageWithOldTypings': '^2.0.0', + '@types/package9': '~1.0.0', + '@types/packageWithOldTypings': '~2.0.0', package4: '^1.0.0', package5: '^1.0.0', }) diff --git a/src/__tests__/versioning.test.ts b/src/__tests__/versioning.test.ts new file mode 100644 index 0000000..1ddb6cb --- /dev/null +++ b/src/__tests__/versioning.test.ts @@ -0,0 +1,80 @@ +import { IPackageVersionInfo } from '../types' +import { getClosestMatchingVersion } from '../versioning' + +describe('getClosestMatchingVersion', () => { + it('returns the closest matching version', () => { + const inputVersions: IPackageVersionInfo[] = [ + { + containsInternalTypings: false, + version: '1.16.0', + }, + { + containsInternalTypings: false, + version: '1.15.2', + }, + { + containsInternalTypings: false, + version: '1.15.1', + }, + { + containsInternalTypings: false, + version: '1.15.0', + }, + { + containsInternalTypings: false, + version: '1.14.2', + }, + { + containsInternalTypings: false, + version: '1.14.1', + }, + { + containsInternalTypings: false, + version: '1.14.0', + }, + { + containsInternalTypings: false, + version: '1.13.0', + }, + ] + + expect(getClosestMatchingVersion(inputVersions, '^1.17.0').version).toBe( + '1.16.0', + ) + expect(getClosestMatchingVersion(inputVersions, '^1.16.1').version).toBe( + '1.16.0', + ) + expect(getClosestMatchingVersion(inputVersions, '^1.16.0').version).toBe( + '1.16.0', + ) + expect(getClosestMatchingVersion(inputVersions, '^1.15.4').version).toBe( + '1.15.2', + ) + expect(getClosestMatchingVersion(inputVersions, '^1.15.3').version).toBe( + '1.15.2', + ) + expect(getClosestMatchingVersion(inputVersions, '^1.15.2').version).toBe( + '1.15.2', + ) + expect(getClosestMatchingVersion(inputVersions, '^1.15.1').version).toBe( + '1.15.2', + ) + expect(getClosestMatchingVersion(inputVersions, '^1.14.0').version).toBe( + '1.14.2', + ) + }) + + it('throws when unable to parse version', () => { + expect(() => + getClosestMatchingVersion( + [ + { + containsInternalTypings: false, + version: 'not a version', + }, + ], + 'also not a version', + ), + ).toThrow() + }) +}) diff --git a/src/cli.ts b/src/cli.ts index 3163656..bcbea7e 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -72,8 +72,10 @@ async function run(syncer: ITypeSyncer) { } C.success( totals.newTypings === 0 - ? `No new typings added, looks like you're all synced up!` - : chalk`${totals.newTypings.toString()} new typings added.\n\n${syncedFilesOutput}\n\n✨ Go ahead and run {green npm install} or {green yarn} to install the packages that were added.`, + ? `No new typings to add, looks like you're all synced up!` + : flags.dry + ? chalk`${totals.newTypings.toString()} new typings can be added.\n\n${syncedFilesOutput}\n\n✨ Run {green typesync} again without the {gray --dry} flag to update your {gray package.json}.` + : chalk`${totals.newTypings.toString()} new typings added.\n\n${syncedFilesOutput}\n\n✨ Go ahead and run {green npm install} or {green yarn} to install the packages that were added.`, ) } diff --git a/src/type-syncer.ts b/src/type-syncer.ts index ec88a45..9611c8e 100644 --- a/src/type-syncer.ts +++ b/src/type-syncer.ts @@ -9,7 +9,6 @@ import { ISyncResult, ISyncedFile, IPackageSource, - IPackageInfo, IConfigService, IDependencySection, ICLIArguments, @@ -25,7 +24,7 @@ import { ensureWorkspacesArray, } from './util' import { IGlobber } from './globber' -import { satisfies } from 'semver' +import { getClosestMatchingVersion } from './versioning' /** * Creates a type syncer. @@ -95,14 +94,14 @@ export function createTypeSyncer( const packageFile = file || (await packageJSONService.readPackageFile(filePath)) - const allPackages = flatten( + const allLocalPackages = flatten( Object.values(IDependencySection).map((dep) => { const section = getDependenciesBySection(packageFile, dep) const ignoredSection = ignoreDeps?.includes(dep) return getPackagesFromSection(section, ignoredSection, ignorePackages) }), ) - const allPackageNames = uniq(allPackages.map((p) => p.name)) + const allPackageNames = uniq(allLocalPackages.map((p) => p.name)) const potentiallyUntypedPackages = getPotentiallyUntypedPackages(allPackageNames) // This is pushed to in the inner `map`, because packages that have DT-typings @@ -119,14 +118,14 @@ export function createTypeSyncer( return {} } - const codePackage = allPackages.find( + const localCodePackage = allLocalPackages.find( (p) => p.name === t.codePackageName, )! // Find the closest matching code package version relative to what's in our package.json const closestMatchingCodeVersion = getClosestMatchingVersion( - codePackageInfo, - codePackage.version, + codePackageInfo.versions, + localCodePackage.version, ) // If the closest matching version contains internal typings, don't include it. @@ -144,15 +143,12 @@ export function createTypeSyncer( // Gets the closest matching typings version, or the newest one. const closestMatchingTypingsVersion = getClosestMatchingVersion( - typePackageInfo, - codePackage.version, + typePackageInfo.versions, + localCodePackage.version, ) const version = closestMatchingTypingsVersion.version - const semverRangeSpecifier = getSemverRangeSpecifier( - codePackage.version, - ) - + const semverRangeSpecifier = '~' used.push(t) return { [t.typesPackageName]: semverRangeSpecifier + version, @@ -178,19 +174,6 @@ export function createTypeSyncer( } } -/** - * Gets the closest matching package version info. - * - * @param packageInfo - * @param version - */ -function getClosestMatchingVersion(packageInfo: IPackageInfo, version: string) { - return ( - packageInfo.versions.find((v) => satisfies(v.version, version)) || - packageInfo.versions[0] - ) -} - /** * Returns an array of packages that do not have a `@types/` package. * @@ -305,22 +288,3 @@ function getDependenciesBySection( })() return dependenciesSection ?? {} } - -const CARET = '^'.charCodeAt(0) -const TILDE = '~'.charCodeAt(0) - -/** - * Gets the semver range specifier (~, ^) - * @param version - */ -function getSemverRangeSpecifier(version: string): string { - if (version.charCodeAt(0) === CARET) { - return '^' - } - - if (version.charCodeAt(0) === TILDE) { - return '~' - } - - return '' -} diff --git a/src/versioning.ts b/src/versioning.ts new file mode 100644 index 0000000..761fcc0 --- /dev/null +++ b/src/versioning.ts @@ -0,0 +1,54 @@ +import { IPackageVersionInfo } from './types' +import { parse } from 'semver' + +/** + * Gets the closest matching package version info. + * + * @param availableVersions + * @param version + */ +export function getClosestMatchingVersion( + availableVersions: IPackageVersionInfo[], + version: string, +) { + const parsedVersion = parseOrThrow(version) + + return ( + availableVersions.find((v) => { + const parsedAvailableVersion = parseOrThrow(v.version) + if (parsedVersion.major !== parsedAvailableVersion.major) { + return false + } + + if (parsedVersion.minor !== parsedAvailableVersion.minor) { + return false + } + + return true + }) || availableVersions[0] + ) +} + +/** + * Parses the version or throws an error. + * + * @param version + * @returns + */ +function parseOrThrow(version: string) { + const parsed = parse(cleanVersion(version)) + if (!parsed) { + throw new Error(`Could not parse version '${version}'`) + } + + return parsed +} + +/** + * Cleans the version of any semver range specifiers. + * @param version + * @returns + */ +function cleanVersion(version: string) { + return version.replace(/^[\^~=\s]/, '') +}