From 2f1bf48ad5275393b2cfa3a1444129549139edac Mon Sep 17 00:00:00 2001 From: Liang Date: Sun, 27 Sep 2026 21:43:00 +0800 Subject: [PATCH] refactor(cli): replace direct semver usage with verkit --- packages/cli/package.json | 3 +- packages/cli/src/create/org-manifest.ts | 4 +- .../__tests__/vitest-v5-webdriverio.spec.ts | 8 +- packages/cli/src/migration/bin.ts | 8 +- .../cli/src/migration/migrator/catalog.ts | 6 +- packages/cli/src/migration/migrator/eslint.ts | 6 +- packages/cli/src/migration/migrator/setup.ts | 6 +- packages/cli/src/migration/migrator/shared.ts | 7 +- .../cli/src/migration/migrator/vitest-v5.ts | 80 +++++++++++-------- packages/cli/src/migration/migrator/yarn.ts | 4 +- .../cli/src/migration/vitest-v5/lockfile.ts | 8 +- .../src/migration/vitest-v5/webdriverio.ts | 38 +++++---- packages/cli/src/sync-versions/plan.ts | 6 +- packages/cli/src/utils/prompts.ts | 6 +- packages/tools/package.json | 3 +- .../src/__tests__/sync-remote-deps.spec.ts | 20 ++--- .../src/__tests__/sync-remote-yaml.spec.ts | 14 ++-- packages/tools/src/sync-remote-deps.ts | 40 +++++----- pnpm-lock.yaml | 33 ++++---- pnpm-workspace.yaml | 2 +- 20 files changed, 157 insertions(+), 145 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index 221e98d74f..ecf138c0fb 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -366,7 +366,6 @@ "@napi-rs/cli": "catalog:", "@nkzw/safe-word-list": "catalog:", "@types/cross-spawn": "catalog:", - "@types/semver": "catalog:", "@types/validate-npm-package-name": "catalog:", "@vitest/browser-playwright": "catalog:", "@vitest/browser-webdriverio": "catalog:", @@ -382,9 +381,9 @@ "minimatch": "catalog:", "nanotar": "catalog:", "picocolors": "catalog:", - "semver": "catalog:", "tsdown": "catalog:", "validate-npm-package-name": "catalog:", + "verkit": "catalog:", "yaml": "catalog:", "zod": "catalog:" }, diff --git a/packages/cli/src/create/org-manifest.ts b/packages/cli/src/create/org-manifest.ts index e549e9f815..7f32e178b2 100644 --- a/packages/cli/src/create/org-manifest.ts +++ b/packages/cli/src/create/org-manifest.ts @@ -1,6 +1,6 @@ import path from 'node:path'; -import semver from 'semver'; +import { normalize } from 'verkit'; import { fetchNpmResource, getNpmRegistry } from '../utils/npm-config.ts'; import { readPackageJsonFromTarball } from './org-tarball.ts'; @@ -350,7 +350,7 @@ export async function readOrgManifest( } // Registry versions become cache-path components, so reject malformed // values even when the registry has matching version metadata. - if (semver.valid(resolvedVersion) === null) { + if (normalize(resolvedVersion) === null) { throw new OrgManifestSchemaError( `invalid version "${resolvedVersion}" (expected a semantic version)`, packageName, diff --git a/packages/cli/src/migration/__tests__/vitest-v5-webdriverio.spec.ts b/packages/cli/src/migration/__tests__/vitest-v5-webdriverio.spec.ts index 888d232e76..71870e3668 100644 --- a/packages/cli/src/migration/__tests__/vitest-v5-webdriverio.spec.ts +++ b/packages/cli/src/migration/__tests__/vitest-v5-webdriverio.spec.ts @@ -2,7 +2,7 @@ import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; -import semver from 'semver'; +import { isRangeSubset, satisfies } from 'verkit'; import { afterEach, describe, expect, it } from 'vitest'; import { parse as yaml } from 'yaml'; @@ -64,9 +64,9 @@ describe('WebDriverIO migration floor', () => { 'narrows %s without allowing v4', (spec) => { const result = webdriverioMigrationSpec(spec)!; - expect(semver.subset(result, '>=5.0.0')).toBe(true); + expect(isRangeSubset(result, '>=5.0.0')).toBe(true); for (const version of ['5.0.0', '5.2.0', '6.1.0', '7.0.0']) { - expect(semver.satisfies(version, result)).toBe(semver.satisfies(version, spec)); + expect(satisfies(version, result)).toBe(satisfies(version, spec)); } expect(webdriverioMigrationSpec(result)).toBe(result); }, @@ -132,7 +132,7 @@ describe('WebDriverIO migration floor', () => { }); migrate(root); expect(read(root).peerDependencies[provider]).toBe('^4 || ^5'); - expect(semver.subset(read(root).devDependencies[provider], '>=5.0.0')).toBe(true); + expect(isRangeSubset(read(root).devDependencies[provider], '>=5.0.0')).toBe(true); }); it('reuses a newer peer-only provider without downgrading it', () => { const root = project({ diff --git a/packages/cli/src/migration/bin.ts b/packages/cli/src/migration/bin.ts index 630520386b..2a79654a7c 100644 --- a/packages/cli/src/migration/bin.ts +++ b/packages/cli/src/migration/bin.ts @@ -2,7 +2,7 @@ import path from 'node:path'; import { styleText } from 'node:util'; import * as prompts from '@voidzero-dev/vite-plus-prompts'; -import semver from 'semver'; +import { satisfies } from 'verkit'; import { parseMigrateArgs } from '../../binding/index.js'; import { @@ -672,13 +672,13 @@ async function downloadSupportedPackageManager(options: { if ( packageManager === PackageManager.yarn && - semver.satisfies(downloadResult.version, '>=4.0.0 <4.10.0') + satisfies(downloadResult.version, '>=4.0.0 <4.10.0') ) { updateMigrationProgress('Upgrading Yarn'); await upgradeYarn(rootDir, interactive, true); } else if ( packageManager === PackageManager.pnpm && - semver.satisfies(downloadResult.version, '< 9.5.0') + satisfies(downloadResult.version, '< 9.5.0') ) { failMigrationProgress('Migration failed'); prompts.log.error( @@ -687,7 +687,7 @@ async function downloadSupportedPackageManager(options: { cancelAndExit('Vite+ cannot automatically migrate this project yet.', 1); } else if ( packageManager === PackageManager.npm && - semver.satisfies(downloadResult.version, '< 8.3.0') + satisfies(downloadResult.version, '< 8.3.0') ) { failMigrationProgress('Migration failed'); prompts.log.error( diff --git a/packages/cli/src/migration/migrator/catalog.ts b/packages/cli/src/migration/migrator/catalog.ts index 30732c3202..26c7e59d5e 100644 --- a/packages/cli/src/migration/migrator/catalog.ts +++ b/packages/cli/src/migration/migrator/catalog.ts @@ -1,7 +1,7 @@ import fs from 'node:fs'; import path from 'node:path'; -import semver from 'semver'; +import { coerce, isGreaterThanOrEqual } from 'verkit'; import { Scalar, YAMLMap, YAMLSeq } from 'yaml'; import { PackageManager, type WorkspacePackage } from '../../types/index.ts'; @@ -77,9 +77,9 @@ const PNPM_MINIMUM_RELEASE_AGE_EXCLUDES = [ // only when it is one of the given dist-tags, which always point at a current // (feature-capable) release. function versionAtLeast(version: string, minVersion: string, tags: string[]): boolean { - const coerced = semver.coerce(version); + const coerced = coerce(version); if (coerced) { - return semver.gte(coerced, minVersion); + return isGreaterThanOrEqual(coerced, minVersion); } return tags.includes(version); } diff --git a/packages/cli/src/migration/migrator/eslint.ts b/packages/cli/src/migration/migrator/eslint.ts index af79ab37d2..7fc0aa8426 100644 --- a/packages/cli/src/migration/migrator/eslint.ts +++ b/packages/cli/src/migration/migrator/eslint.ts @@ -4,7 +4,7 @@ import { styleText } from 'node:util'; import * as prompts from '@voidzero-dev/vite-plus-prompts'; import { type OxlintConfig } from 'oxlint'; -import semver from 'semver'; +import { compareReversed, isLessThanOrEqual, normalize } from 'verkit'; import { rewriteEslint } from '../../../binding/index.js'; import { type WorkspacePackage } from '../../types/index.ts'; @@ -127,9 +127,9 @@ export async function resolveOxlintMigrateVersion(oxlintVersion: string): Promis return oxlintVersion; } const candidates = Object.keys(packument.versions ?? {}).filter( - (version) => semver.valid(version) && semver.lte(version, oxlintVersion), + (version) => normalize(version) && isLessThanOrEqual(version, oxlintVersion), ); - return candidates.length > 0 ? candidates.toSorted(semver.rcompare)[0] : oxlintVersion; + return candidates.length > 0 ? candidates.toSorted(compareReversed)[0] : oxlintVersion; } catch { return oxlintVersion; } diff --git a/packages/cli/src/migration/migrator/setup.ts b/packages/cli/src/migration/migrator/setup.ts index 1dfdc41b39..c889777abd 100644 --- a/packages/cli/src/migration/migrator/setup.ts +++ b/packages/cli/src/migration/migrator/setup.ts @@ -3,7 +3,7 @@ import path from 'node:path'; import * as prompts from '@voidzero-dev/vite-plus-prompts'; import { globSync } from 'glob'; -import semver from 'semver'; +import { normalize, normalizeRange } from 'verkit'; import { parse as parseYaml } from 'yaml'; import { type DownloadPackageManagerResult } from '../../../binding/index.js'; @@ -112,7 +112,7 @@ export function parseNvmrcVersion(alias: string): string | null { // Strip optional 'v' prefix, then validate as a semver version or range const normalized = version.startsWith('v') ? version.slice(1) : version; - if (!normalized || !semver.validRange(normalized)) { + if (!normalized || !normalizeRange(normalized)) { return null; } return normalized; @@ -257,7 +257,7 @@ export function migrateNodeVersionManagerFile( // Normalize Volta's "lts" alias to the .node-version compatible form const resolvedVersion = voltaNodeVersion === 'lts' ? 'lts/*' : voltaNodeVersion; - if (!semver.valid(resolvedVersion) && resolvedVersion !== 'lts/*') { + if (!normalize(resolvedVersion) && resolvedVersion !== 'lts/*') { warnMigration( `package.json volta.node "${voltaNodeVersion}" is not an exact version. Pin an exact version (e.g. ${voltaNodeVersion}.0 or run \`volta pin node@${voltaNodeVersion}\`) then re-run migration.`, report, diff --git a/packages/cli/src/migration/migrator/shared.ts b/packages/cli/src/migration/migrator/shared.ts index 1b7a5235b6..c5af65cef2 100644 --- a/packages/cli/src/migration/migrator/shared.ts +++ b/packages/cli/src/migration/migrator/shared.ts @@ -2,7 +2,7 @@ import fs from 'node:fs'; import path from 'node:path'; import * as prompts from '@voidzero-dev/vite-plus-prompts'; -import semver from 'semver'; +import { coerce, satisfies } from 'verkit'; import { VITEST_VERSION, VITE_PLUS_OVERRIDE_PACKAGES } from '../../utils/constants.ts'; import { readJsonFile } from '../../utils/json.ts'; @@ -251,7 +251,7 @@ function checkPackageVersion(projectPath: string, name: string, minVersion: stri if (!metadata || metadata.name !== name) { return true; } - if (semver.satisfies(metadata.version, `<${minVersion}`)) { + if (satisfies(metadata.version, `<${minVersion}`)) { const packageJsonFilePath = path.join(projectPath, 'package.json'); prompts.log.error( `✘ ${name}@${metadata.version} in ${displayRelative(packageJsonFilePath)} is not supported by auto migration`, @@ -304,8 +304,7 @@ export function readPackageJsonIfExists(packageJsonPath: string): DependencyBag // `package.json`'s `pnpm` field — vp migrate writes to whichever sink the // rest of the migration is already touching. export function pnpmMajor(version: string | undefined): number | undefined { - const coerced = version ? semver.coerce(version)?.version : undefined; - return coerced ? semver.major(coerced) : undefined; + return version ? coerce(version)?.major : undefined; } // Packages that own the Oxlint JS-plugin authoring API as a published contract. diff --git a/packages/cli/src/migration/migrator/vitest-v5.ts b/packages/cli/src/migration/migrator/vitest-v5.ts index 643f845cc8..671bb44529 100644 --- a/packages/cli/src/migration/migrator/vitest-v5.ts +++ b/packages/cli/src/migration/migrator/vitest-v5.ts @@ -2,7 +2,19 @@ import fs from 'node:fs'; import path from 'node:path'; import { applyEdits, findNodeAtLocation, parse as parseJsonc, parseTree } from 'jsonc-parser'; -import semver from 'semver'; +import { + type SemVer, + compare, + findMinimumForRange, + getMajor, + isGreaterThanOrEqual, + isRangeSubset, + normalize, + normalizeRange, + rangeToComparators, + rangesIntersect, + satisfies, +} from 'verkit'; import { isScalar, parseDocument, visit } from 'yaml'; import { isDirectoryGitignored } from '../../../binding/index.js'; @@ -221,7 +233,7 @@ function lockedSourceVersion( return follow(LEGACY_RUNNER, reference.slice(LEGACY_RUNNER.length + 1), depth + 1); } const version = reference.split('(')[0]; - if (!semver.valid(version)) { + if (!normalize(version)) { return undefined; } if (name === 'vitest') { @@ -233,7 +245,7 @@ function lockedSourceVersion( const legacy = record(record(lock?.packages)?.[`${name}@${version}`]); const runnerVersion = record(legacy?.peerDependencies)?.['@vitest/ui']; return typeof runnerVersion === 'string' - ? (semver.valid(runnerVersion) ?? undefined) + ? (normalize(runnerVersion) ?? undefined) : undefined; } const snapshot = record(record(lock?.snapshots)?.[`${name}@${reference}`]); @@ -252,15 +264,17 @@ function lockedSourceVersion( function unambiguousRunnerVersion(range: string): string | undefined { if ( - !semver.validRange(range) || - (!semver.subset(range, '<5.0.0-0', { includePrerelease: true }) && - !semver.subset(range, '>=5.0.0-0', { includePrerelease: true })) + !normalizeRange(range) || + (!isRangeSubset(range, '<5.0.0-0', { includePrerelease: true }) && + !isRangeSubset(range, '>=5.0.0-0', { includePrerelease: true })) ) { // A range that crosses the v4/v5 boundary cannot establish which defaults // the project used. Require its installed runner or an exact lockfile edge. return undefined; } - return semver.minVersion(range)?.version; + // Parsed versions are plain records; normalize them before returning a version string. + const minimum = findMinimumForRange(range); + return minimum ? (normalize(minimum) ?? undefined) : undefined; } function installedSourceVersion( @@ -275,18 +289,18 @@ function installedSourceVersion( } if ( expectedRange && - semver.validRange(expectedRange) && - !semver.satisfies(installed.version, expectedRange, { includePrerelease: true }) + normalizeRange(expectedRange) && + !satisfies(installed.version, expectedRange, { includePrerelease: true }) ) { return undefined; } if (installed.name === 'vitest') { - return semver.valid(installed.version) ?? undefined; + return normalize(installed.version) ?? undefined; } const pkg = readJson(path.join(installed.path, 'package.json')); if (installed.name === LEGACY_RUNNER) { const version = record(pkg.peerDependencies)?.['@vitest/ui']; - return typeof version === 'string' ? (semver.valid(version) ?? undefined) : undefined; + return typeof version === 'string' ? (normalize(version) ?? undefined) : undefined; } if (installed.name !== 'vite-plus') { return undefined; @@ -298,9 +312,9 @@ function installedSourceVersion( // or the CLI version: repeat runs must identify v5 without saved metadata. return installedSourceVersion(installed.path, 'vitest'); } - if (bundled && semver.validRange(bundled)) { + if (bundled && normalizeRange(bundled)) { const version = installedSourceVersion(installed.path, 'vitest'); - return version && semver.satisfies(version, bundled, { includePrerelease: true }) + return version && satisfies(version, bundled, { includePrerelease: true }) ? version : unambiguousRunnerVersion(bundled); } @@ -348,15 +362,15 @@ function sourceVersion( locked && (!spec || legacyAlias || - !semver.validRange(spec) || - semver.satisfies(locked, spec, { includePrerelease: true })) + !normalizeRange(spec) || + satisfies(locked, spec, { includePrerelease: true })) ) { return locked; } const installed = installedSourceVersion(directory, 'vitest', expectedRunnerPackage); if ( installed && - (!spec || legacyAlias || semver.satisfies(installed, spec, { includePrerelease: true })) + (!spec || legacyAlias || satisfies(installed, spec, { includePrerelease: true })) ) { return installed; } @@ -365,7 +379,7 @@ function sourceVersion( // version from the original install or lockfile when one is available. return '4.0.0'; } - if (spec && semver.validRange(spec)) { + if (spec && normalizeRange(spec)) { return unambiguousRunnerVersion(spec); } if (vitePlus) { @@ -395,41 +409,41 @@ function checkNodeRange( if (!publicContract && ['lts/*', 'lts', 'latest', 'current', 'node', 'stable'].includes(value)) { return undefined; } - const range = semver.validRange(value); + const range = normalizeRange(value); if (publicContract && range) { // A public engine contract is not a runtime pin. A supported minimum // is sufficient; open ranges need not exclude every unsupported major. - const minimum = semver.minVersion(range); + const minimum = findMinimumForRange(range); if (minimum) { // A whole-major range such as 24.x also permits a supported release. // Do not mistake its implicit 24.0.0 minimum for an exact runtime pin. const majorRange = `${minimum.major}.x`; if ( - semver.satisfies(minimum, cliPackage.engines.node) || - (semver.subset(majorRange, range) && semver.intersects(majorRange, cliPackage.engines.node)) + satisfies(minimum, cliPackage.engines.node) || + (isRangeSubset(majorRange, range) && rangesIntersect(majorRange, cliPackage.engines.node)) ) { return undefined; } } } let message = `Resolve ${label} (${value}) and select Node ${cliPackage.engines.node}.`; - if (range && !publicContract && !semver.intersects(range, cliPackage.engines.node)) { - const current = semver.minVersion(range); + if (range && !publicContract && !rangesIntersect(range, cliPackage.engines.node)) { + const current = findMinimumForRange(range); const upgrade = current && - new semver.Range(cliPackage.engines.node).set - .map((comparators) => semver.minVersion(comparators.map(({ value }) => value).join(' '))) - .filter((version): version is semver.SemVer => version !== null) - .toSorted(semver.compare) - .find((version) => semver.gte(version, current)); + rangeToComparators(cliPackage.engines.node) + .map((comparators) => findMinimumForRange(comparators.join(' '))) + .filter((version): version is SemVer => version !== null) + .toSorted(compare) + .find((version) => isGreaterThanOrEqual(version, current)); if (upgrade) { // Choose the first supported minimum at or above the requested version. // For example, 20 -> 22.18.0, 24.10 -> 24.11.0, and 25 -> 26.0.0. - return upgrade.version; + return normalize(upgrade) ?? undefined; } } else if (range) { if ( - semver.subset(range, cliPackage.engines.node) || + isRangeSubset(range, cliPackage.engines.node) || (!publicContract && !/[<>|*]/.test(value)) ) { return undefined; @@ -727,8 +741,8 @@ export function planVitestV5Migration( const options: SourceOptions = { // Only the resolved source runner authorizes v4 compatibility edits. // The final scan disables edits while retaining this run's review context. - preserveV4: original?.options.preserveV4 ?? (!!version && semver.major(version) < 5), - reviewV4: !!version && semver.major(version) < 5, + preserveV4: original?.options.preserveV4 ?? (!!version && getMajor(version) < 5), + reviewV4: !!version && getMajor(version) < 5, browser: [...sources.values()].some((source) => BROWSER_SIGNAL.test(source)), browserPossible, temporalPolyfill: [...sources.values()].some((source) => @@ -747,7 +761,7 @@ export function planVitestV5Migration( ), ); } - if (active && version && semver.major(version) < 4) { + if (active && version && getMajor(version) < 4) { findings.push( finding( path.join(directory, 'package.json'), diff --git a/packages/cli/src/migration/migrator/yarn.ts b/packages/cli/src/migration/migrator/yarn.ts index d01a39635c..fb39f76881 100644 --- a/packages/cli/src/migration/migrator/yarn.ts +++ b/packages/cli/src/migration/migrator/yarn.ts @@ -2,7 +2,7 @@ import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; -import semver from 'semver'; +import { coerce } from 'verkit'; import { Scalar, YAMLSeq } from 'yaml'; import { type WorkspacePackage } from '../../types/index.ts'; @@ -187,7 +187,7 @@ export function detectYarnPnpMode( projectPath: string, yarnVersion: string, ): YarnPnpDetection | undefined { - const coercedVersion = semver.coerce(yarnVersion); + const coercedVersion = coerce(yarnVersion); if (coercedVersion?.major === 1) { return undefined; } diff --git a/packages/cli/src/migration/vitest-v5/lockfile.ts b/packages/cli/src/migration/vitest-v5/lockfile.ts index 9a9b6dc746..85e3b3a3cb 100644 --- a/packages/cli/src/migration/vitest-v5/lockfile.ts +++ b/packages/cli/src/migration/vitest-v5/lockfile.ts @@ -2,7 +2,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { parse as parseJsonc, type ParseError } from 'jsonc-parser'; -import semver from 'semver'; +import { normalize, normalizeRange, satisfies } from 'verkit'; import { parseDocument } from 'yaml'; import { PackageManager } from '../../types/index.ts'; @@ -39,15 +39,15 @@ export function lockedVitestVersion( spec: string, ): string | undefined { const range = spec.replace(/^npm:vitest@/, ''); - const validRange = semver.validRange(range); + const validRange = normalizeRange(range); if (!validRange && !/^[\w.-]+$/.test(range)) { return undefined; } function valid(version: unknown): string | undefined { - if (typeof version !== 'string' || !semver.valid(version)) { + if (typeof version !== 'string' || !normalize(version)) { return undefined; } - return !validRange || semver.satisfies(version, range, { includePrerelease: true }) + return !validRange || satisfies(version, range, { includePrerelease: true }) ? version : undefined; } diff --git a/packages/cli/src/migration/vitest-v5/webdriverio.ts b/packages/cli/src/migration/vitest-v5/webdriverio.ts index 2c0a258814..fe1be6a186 100644 --- a/packages/cli/src/migration/vitest-v5/webdriverio.ts +++ b/packages/cli/src/migration/vitest-v5/webdriverio.ts @@ -2,7 +2,14 @@ import fs from 'node:fs'; import path from 'node:path'; import { applyEdits, modify, parse } from 'jsonc-parser'; -import semver from 'semver'; +import { + findMinimumForRange, + isGreaterThanOrEqual, + normalize, + normalizeRange, + rangeToComparators, + rangesIntersect, +} from 'verkit'; import { parseDocument } from 'yaml'; import { PackageManager, type WorkspacePackage } from '../../types/index.ts'; @@ -27,22 +34,21 @@ export function webdriverioMigrationSpec(spec: string): string | undefined { const migrated = webdriverioMigrationSpec(spec.slice(REGISTRY_ALIAS.length)); return migrated === undefined ? undefined : `${REGISTRY_ALIAS}${migrated}`; } - if (!semver.validRange(spec)) { + if (!normalizeRange(spec)) { return undefined; } - const range = new semver.Range(spec); - const branches = range.set.map((set) => set.map((item) => item.value).join(' ')); + const branches = rangeToComparators(spec).map((set) => set.join(' ')); if ( branches.every((branch) => { - const min = semver.minVersion(branch); - return min && semver.gte(min, MIN_VERSION); + const min = findMinimumForRange(branch); + return min && isGreaterThanOrEqual(min, MIN_VERSION); }) ) { return spec; } const narrowed = branches .map((branch) => `${branch} >=${MIN_VERSION}`.trim()) - .filter((branch) => semver.minVersion(branch) !== null); + .filter((branch) => findMinimumForRange(branch) !== null); return narrowed.length ? narrowed.join(' || ') : DEFAULT_SPEC; } @@ -182,8 +188,8 @@ export function migrateWebdriverioDependencies( if ( pkg.name === WEBDRIVERIO_PROVIDER && typeof pkg.version === 'string' && - semver.valid(pkg.version) && - semver.gte(pkg.version, MIN_VERSION) + normalize(pkg.version) && + isGreaterThanOrEqual(pkg.version, MIN_VERSION) ) { return; } @@ -245,7 +251,7 @@ export function migrateWebdriverioDependencies( } const entry = spec.startsWith('catalog:') ? catalogEntry(spec, name) : undefined; const resolved = entry ? get(entry.file, entry.keys) : spec; - if (typeof resolved === 'string' && semver.validRange(resolved)) { + if (typeof resolved === 'string' && normalizeRange(resolved)) { peer = name === 'webdriverio' ? spec : resolved; break; } @@ -253,7 +259,7 @@ export function migrateWebdriverioDependencies( // An existing installed framework is another safe fallback, e.g. a // WebDriverIO CLI that brings its framework transitively. const metadata = peer === '*' ? detectPackageMetadata(directory, 'webdriverio') : undefined; - if (metadata && semver.valid(metadata.version)) { + if (metadata && normalize(metadata.version)) { const metadataFile = path.join(metadata.path, 'package.json'); inputs.set(metadataFile, fs.readFileSync(metadataFile, 'utf8')); peer = `^${metadata.version}`; @@ -304,14 +310,14 @@ export function migrateWebdriverioDependencies( const pin = catalog ? get(catalog.file, catalog.keys) : before; const pinnedRange = typeof pin === 'string' - ? semver.validRange( + ? normalizeRange( pin.startsWith(REGISTRY_ALIAS) ? pin.slice(REGISTRY_ALIAS.length) : pin, ) : null; // An old forcing pin must not override a newer direct declaration. // Remove v4-only pins and let the migrated dependency choose its version. // Preserve the children of npm's long-form override objects. - if (pinnedRange && !semver.intersects(pinnedRange, `>=${MIN_VERSION}`)) { + if (pinnedRange && !rangesIntersect(pinnedRange, `>=${MIN_VERSION}`)) { set(file, versionPath, undefined); continue; } @@ -345,9 +351,9 @@ export function migrateWebdriverioDependencies( continue; } else if ( typeof direct === 'string' && - semver.validRange(direct) && - semver.validRange(after) && - semver.intersects(direct, after) + normalizeRange(direct) && + normalizeRange(after) && + rangesIntersect(direct, after) ) { set(rootManifest, [field, WEBDRIVERIO_PROVIDER], after); set(file, versionPath, `$${WEBDRIVERIO_PROVIDER}`); diff --git a/packages/cli/src/sync-versions/plan.ts b/packages/cli/src/sync-versions/plan.ts index d76dc4b150..72258b163b 100644 --- a/packages/cli/src/sync-versions/plan.ts +++ b/packages/cli/src/sync-versions/plan.ts @@ -1,7 +1,7 @@ import path from 'node:path'; import { applyEdits, modify, parse as parseJsonc, type ParseError } from 'jsonc-parser'; -import semver from 'semver'; +import { normalizeRange } from 'verkit'; import { isScalar, parseDocument, Scalar } from 'yaml'; import { z } from 'zod'; @@ -163,7 +163,7 @@ function alignedNpmAlias(current: string, toolchain: SyncVersionsToolchain): str if ( target === undefined || currentVersion === target || - semver.validRange(currentVersion) === null + normalizeRange(currentVersion) === null ) { return current; } @@ -180,7 +180,7 @@ function alignedSpec(name: string, current: string, toolchain: SyncVersionsToolc current === target || current.startsWith('$') || /^[a-z][a-z+.-]*:/i.test(current) || - semver.validRange(current) === null + normalizeRange(current) === null ) { return current; } diff --git a/packages/cli/src/utils/prompts.ts b/packages/cli/src/utils/prompts.ts index 231aada721..65212ff0b6 100644 --- a/packages/cli/src/utils/prompts.ts +++ b/packages/cli/src/utils/prompts.ts @@ -1,5 +1,5 @@ import * as prompts from '@voidzero-dev/vite-plus-prompts'; -import semver from 'semver'; +import { coerce, isGreaterThanOrEqual } from 'verkit'; import { downloadPackageManager as downloadPackageManagerBinding } from '../../binding/index.js'; import { PackageManager } from '../types/index.ts'; @@ -36,11 +36,11 @@ export function shouldIgnoreScriptsForAutoInstall( if (packageManager !== PackageManager.pnpm) { return false; } - const coerced = packageManagerVersion ? semver.coerce(packageManagerVersion)?.version : undefined; + const coerced = packageManagerVersion ? coerce(packageManagerVersion) : undefined; if (!coerced) { return false; } - return semver.gte(coerced, '11.0.0'); + return isGreaterThanOrEqual(coerced, '11.0.0'); } export function cancelAndExit(message = 'Operation cancelled', exitCode = 0): never { diff --git a/packages/tools/package.json b/packages/tools/package.json index 0b5ce3ba5c..2ce45c219f 100644 --- a/packages/tools/package.json +++ b/packages/tools/package.json @@ -9,8 +9,7 @@ "type": "module", "devDependencies": { "@oxc-node/core": "catalog:", - "@types/semver": "catalog:", - "semver": "catalog:", + "verkit": "catalog:", "vitest": "catalog:", "yaml": "catalog:" } diff --git a/packages/tools/src/__tests__/sync-remote-deps.spec.ts b/packages/tools/src/__tests__/sync-remote-deps.spec.ts index ffbea06383..9d570a87ae 100644 --- a/packages/tools/src/__tests__/sync-remote-deps.spec.ts +++ b/packages/tools/src/__tests__/sync-remote-deps.spec.ts @@ -3,7 +3,7 @@ import { copyFileSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSy import { tmpdir } from 'node:os'; import { join } from 'node:path'; -import * as semver from 'semver'; +import * as verkit from 'verkit'; import { describe, expect, test } from 'vitest'; import * as yaml from 'yaml'; @@ -109,7 +109,7 @@ describe('vendored Vitest dependency alignment', () => { { catalog: { vitest: '5.0.0' } }, { catalog: { vitest: '^4.1.6' } }, { catalog: { vitest: '^4.1.10' } }, - semver, + verkit, ); expect(merged.catalog?.vitest).toBe('5.0.0'); }); @@ -121,7 +121,7 @@ describe('vendored Vitest dependency alignment', () => { { catalog: { vitest: '5.0.0' } }, { catalog: { vitest: upstream } }, {}, - semver, + verkit, ); expect(merged.catalog?.vitest).toBe('5.0.0'); }, @@ -155,7 +155,7 @@ describe('vendored Vitest dependency alignment', () => { '@vitest/web-worker': '4.1.11', }, }, - semver, + verkit, ); expect(merged.catalog).toEqual({ vitest: '5.1.2', @@ -173,14 +173,14 @@ describe('vendored Vitest dependency alignment', () => { test.each(['^5.0.0', '=5.0.0', '4.1.11', '6.0.0', '5.1.0-beta.1'])( 'rejects an unreviewed root runner version %s', (vitest) => { - expect(() => mergePnpmWorkspaces({ catalog: { vitest } }, {}, {}, semver)).toThrow( + expect(() => mergePnpmWorkspaces({ catalog: { vitest } }, {}, {}, verkit)).toThrow( 'The root Vitest catalog entry must be an exact stable v5 version', ); }, ); test('does not infer a missing root pin from an upstream catalog', () => { - expect(() => mergePnpmWorkspaces({}, { catalog: { vitest: '^4.1.6' } }, {}, semver)).toThrow( + expect(() => mergePnpmWorkspaces({}, { catalog: { vitest: '^4.1.6' } }, {}, verkit)).toThrow( 'The root Vitest catalog entry must be an exact stable v5 version', ); }); @@ -190,7 +190,7 @@ describe('vendored Vitest dependency alignment', () => { { catalog: { tinybench: '^6.0.0' } }, { catalog: { tinybench: '^2.9.0' } }, {}, - semver, + verkit, ); expect(merged.catalog?.tinybench).toBe('^6.0.0'); }); @@ -336,7 +336,7 @@ describe('mergePnpmWorkspaces() minimumReleaseAgeExclude', () => { }; const rolldownVite = {}; - const result = mergePnpmWorkspaces(main, rolldown, rolldownVite, semver); + const result = mergePnpmWorkspaces(main, rolldown, rolldownVite, verkit); // Nothing redundant should survive; the original broad rules plus the // genuinely-specific `lodash-es@4.18.1` pin remain. @@ -360,7 +360,7 @@ describe('mergePnpmWorkspaces() minimumReleaseAgeExclude', () => { minimumReleaseAgeExclude: ['some-pkg@1.2.3'], }; - const result = mergePnpmWorkspaces(main, rolldown, {}, semver); + const result = mergePnpmWorkspaces(main, rolldown, {}, verkit); expect(result.minimumReleaseAgeExclude).toContain('lodash-es@4.18.1'); expect(result.minimumReleaseAgeExclude).toContain('some-pkg@1.2.3'); @@ -374,7 +374,7 @@ describe('mergePnpmWorkspaces() minimumReleaseAgeExclude', () => { minimumReleaseAgeExclude: ['oxc-parser', '@oxc-parser/*'], }; - const result = mergePnpmWorkspaces(main, rolldown, {}, semver); + const result = mergePnpmWorkspaces(main, rolldown, {}, verkit); expect(result.minimumReleaseAgeExclude).toEqual(['@oxc-parser/*', 'oxc-parser']); }); diff --git a/packages/tools/src/__tests__/sync-remote-yaml.spec.ts b/packages/tools/src/__tests__/sync-remote-yaml.spec.ts index 102f1f7fd6..c1d4ffcedd 100644 --- a/packages/tools/src/__tests__/sync-remote-yaml.spec.ts +++ b/packages/tools/src/__tests__/sync-remote-yaml.spec.ts @@ -1,4 +1,4 @@ -import * as semver from 'semver'; +import * as verkit from 'verkit'; import { describe, expect, test } from 'vitest'; import * as yaml from 'yaml'; @@ -56,7 +56,7 @@ describe('mergeWorkspaceYaml()', () => { '@vitest/browser': '^5.2.0' `; - const output = mergeWorkspaceYaml(main, rolldown, vite, yaml, semver); + const output = mergeWorkspaceYaml(main, rolldown, vite, yaml, verkit); expect(yaml.parse(output).catalog).toEqual({ vitest: '5.1.2', @@ -64,11 +64,11 @@ describe('mergeWorkspaceYaml()', () => { '@vitest/utils': '5.1.2', }); expect(output).toContain('# Keep the runner and official packages on the selected release.'); - expect(mergeWorkspaceYaml(output, rolldown, vite, yaml, semver)).toBe(output); + expect(mergeWorkspaceYaml(output, rolldown, vite, yaml, verkit)).toBe(output); }); test('preserves comments from the main workspace', () => { - const output = mergeWorkspaceYaml(MAIN_SRC, ROLLDOWN_SRC, VITE_SRC, yaml, semver); + const output = mergeWorkspaceYaml(MAIN_SRC, ROLLDOWN_SRC, VITE_SRC, yaml, verkit); expect(output).toContain('# keep zod on v3 until bingo supports zod 4'); expect(output).toContain('# bingo introspects template option schemas via zod 3 internals;'); @@ -79,16 +79,16 @@ describe('mergeWorkspaceYaml()', () => { yaml.parse(MAIN_SRC), yaml.parse(ROLLDOWN_SRC), yaml.parse(VITE_SRC), - semver, + verkit, ); - const output = mergeWorkspaceYaml(MAIN_SRC, ROLLDOWN_SRC, VITE_SRC, yaml, semver); + const output = mergeWorkspaceYaml(MAIN_SRC, ROLLDOWN_SRC, VITE_SRC, yaml, verkit); expect(yaml.parse(output)).toEqual(expected); }); test('merges upstream catalog entries and dedupes redundant exclude entries', () => { - const output = mergeWorkspaceYaml(MAIN_SRC, ROLLDOWN_SRC, VITE_SRC, yaml, semver); + const output = mergeWorkspaceYaml(MAIN_SRC, ROLLDOWN_SRC, VITE_SRC, yaml, verkit); const parsed = yaml.parse(output); // Upstream catalog entries merged in, original pin kept. diff --git a/packages/tools/src/sync-remote-deps.ts b/packages/tools/src/sync-remote-deps.ts index a8c15e4a51..3024c5c4df 100755 --- a/packages/tools/src/sync-remote-deps.ts +++ b/packages/tools/src/sync-remote-deps.ts @@ -387,7 +387,7 @@ function mergeSemverVersions( v1: string, v2: string, packageName: string, - semver: typeof import('semver'), + verkit: typeof import('verkit'), ): string { // Handle special cases if (v1 === v2) { @@ -403,8 +403,8 @@ function mergeSemverVersions( if (syncedPackages(packageName)) { const ver1 = v1.slice(1); // Remove '=' prefix const ver2 = v2.slice(1); - if (semver.valid(ver1) && semver.valid(ver2)) { - const higher = semver.gt(ver1, ver2) ? v1 : v2; + if (verkit.normalize(ver1) && verkit.normalize(ver2)) { + const higher = verkit.isGreaterThan(ver1, ver2) ? v1 : v2; log(`Resolving ${packageName} version conflict: ${v1} vs ${v2} -> ${higher}`); return higher; } @@ -424,8 +424,8 @@ function mergeSemverVersions( return v1; } - const range1 = semver.validRange(v1); - const range2 = semver.validRange(v2); + const range1 = verkit.normalizeRange(v1); + const range2 = verkit.normalizeRange(v2); if (!range1 || !range2) { log(`Warning: Could not parse semver for ${packageName}: ${v1}, ${v2}. Using ${v1}`); @@ -454,13 +454,13 @@ function mergeSemverVersions( // Both have same major version, return the higher one // Compare the minimum versions - const minVersion1 = semver.minVersion(range1); - const minVersion2 = semver.minVersion(range2); + const minVersion1 = verkit.findMinimumForRange(range1); + const minVersion2 = verkit.findMinimumForRange(range2); if (minVersion1 && minVersion2) { - if (semver.gt(minVersion1, minVersion2)) { + if (verkit.isGreaterThan(minVersion1, minVersion2)) { return v1; - } else if (semver.gt(minVersion2, minVersion1)) { + } else if (verkit.isGreaterThan(minVersion2, minVersion1)) { return v2; } } @@ -483,7 +483,7 @@ function parseExcludeEntry(entry: string): { name: string; version?: string } { // Build a matcher for a version-less name pattern. The exclude list only ever // uses the `*` wildcard, so a tiny *-only glob (escape regex specials, `*` -> // `.*`, anchored) is enough and keeps `minimatch` out of this module: it is -// loaded via dynamic import before the yaml/semver install fallback runs, so a +// loaded via dynamic import before the yaml/verkit install fallback runs, so a // top-level dependency import could fail on a clean clone. function globToRegExp(pattern: string): RegExp { const escaped = pattern.replaceAll(/[.+?^${}()|[\]\\]/g, '\\$&').replaceAll('*', '.*'); @@ -520,7 +520,7 @@ export function mergePnpmWorkspaces( main: PnpmWorkspace, rolldown: PnpmWorkspace, rolldownVite: PnpmWorkspace, - semver: typeof import('semver'), + verkit: typeof import('verkit'), ): PnpmWorkspace { const result: PnpmWorkspace = { ...main }; @@ -549,7 +549,7 @@ export function mergePnpmWorkspaces( if ( !vitestVersion || !/^5\.\d+\.\d+$/.test(vitestVersion) || - semver.valid(vitestVersion) !== vitestVersion + verkit.normalize(vitestVersion) !== vitestVersion ) { throw new Error('The root Vitest catalog entry must be an exact stable v5 version'); } @@ -559,7 +559,7 @@ export function mergePnpmWorkspaces( catalog[pkg] = main.catalog[pkg]; } else { catalog[pkg] = catalog[pkg] - ? mergeSemverVersions(catalog[pkg], version, pkg, semver) + ? mergeSemverVersions(catalog[pkg], version, pkg, verkit) : version; } } @@ -680,7 +680,7 @@ export function mergeWorkspaceYaml( rolldownSrc: string, rolldownViteSrc: string, yaml: typeof import('yaml'), - semver: typeof import('semver'), + verkit: typeof import('verkit'), ): string { const mainDoc = yaml.parseDocument(mainSrc); const rolldown = yaml.parse(rolldownSrc) as PnpmWorkspace | null; @@ -690,7 +690,7 @@ export function mergeWorkspaceYaml( (mainDoc.toJSON() as PnpmWorkspace) ?? {}, rolldown ?? {}, rolldownVite ?? {}, - semver, + verkit, ); const stringifyOptions = { lineWidth: -1, singleQuote: true } as const; @@ -885,17 +885,17 @@ export async function syncRemote() { // Dynamically import dependencies after git clone. Capture the whole `yaml` // module (we need `yaml.parseDocument` to preserve comments). let yaml: typeof import('yaml'); - let semver: typeof import('semver'); + let verkit: typeof import('verkit'); try { yaml = await import('yaml'); - semver = await import('semver'); + verkit = await import('verkit'); } catch { log('Dependencies not found, running pnpm install...'); execCommand('pnpm install --no-frozen-lockfile', rootDir); log('Retrying imports...'); yaml = await import('yaml'); - semver = await import('semver'); + verkit = await import('verkit'); } log('Reading pnpm-workspace.yaml files...'); @@ -911,10 +911,10 @@ export async function syncRemote() { log('Merging pnpm-workspace.yaml files...'); // Merge upstream catalogs into the main workspace while preserving its comments. - const yamlContent = mergeWorkspaceYaml(mainSrc, rolldownSrc, rolldownViteSrc, yaml, semver); + const yamlContent = mergeWorkspaceYaml(mainSrc, rolldownSrc, rolldownViteSrc, yaml, verkit); const vitestVersion = (yaml.parse(yamlContent) as PnpmWorkspace).catalog?.vitest; - if (!vitestVersion || !semver.valid(vitestVersion)) { + if (!vitestVersion || !verkit.normalize(vitestVersion)) { throw new Error('The Vitest catalog entry must be an exact version'); } writeFileSync(mainWorkspacePath, yamlContent, 'utf-8'); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ee293f706a..642376652f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -78,9 +78,6 @@ catalogs: '@types/node': specifier: 24.10.3 version: 24.10.3 - '@types/semver': - specifier: ^7.8.0 - version: 7.8.0 '@types/serve-static': specifier: ^2.0.0 version: 2.2.0 @@ -246,6 +243,9 @@ catalogs: validate-npm-package-name: specifier: ^7.0.2 version: 7.0.2 + verkit: + specifier: ^0.5.0 + version: 0.5.0 vitest: specifier: 5.0.1 version: 5.0.1 @@ -375,9 +375,6 @@ importers: '@types/cross-spawn': specifier: 'catalog:' version: 6.0.6 - '@types/semver': - specifier: 'catalog:' - version: 7.8.0 '@types/validate-npm-package-name': specifier: 'catalog:' version: 4.0.2 @@ -423,15 +420,15 @@ importers: picocolors: specifier: 'catalog:' version: 1.1.1 - semver: - specifier: 'catalog:' - version: 7.8.5 tsdown: specifier: 'catalog:' version: 0.23.0(@arethetypeswrong/core@0.18.2)(@tsdown/css@0.23.0)(@tsdown/exe@0.23.0)(@typescript/native-preview@7.0.0-dev.20260605.1)(@vitejs/devtools@0.7.5(@devframes/json-render@1.0.0(@devframes/hub@1.0.0(@devframes/agentic@1.0.0)(crossws@0.4.12(srvx@1.0.5))(devframe@1.0.0))(devframe@1.0.0))(crossws@0.4.12(srvx@1.0.5))(srvx@1.0.5)(vite@packages+core))(publint@0.3.24)(tsx@4.23.15)(typescript@6.0.3)(unplugin-unused@0.5.6)(unrun@0.3.1) validate-npm-package-name: specifier: 'catalog:' version: 7.0.2 + verkit: + specifier: 'catalog:' + version: 0.5.0 yaml: specifier: 'catalog:' version: 2.9.0 @@ -618,12 +615,9 @@ importers: '@oxc-node/core': specifier: 'catalog:' version: 0.1.3 - '@types/semver': - specifier: 'catalog:' - version: 7.8.0 - semver: + verkit: specifier: 'catalog:' - version: 7.8.5 + version: 0.5.0 vitest: specifier: 'catalog:' version: 5.0.1(@edge-runtime/vm@5.0.0)(@opentelemetry/api@1.9.0)(@types/node@24.10.3)(@vitest/browser-playwright@5.0.1)(@vitest/browser-preview@5.0.1)(@vitest/browser-webdriverio@5.0.0)(@vitest/ui@5.0.1)(happy-dom@20.0.10)(jsdom@27.2.0(supports-color@8.1.1))(vite@packages+core) @@ -4300,9 +4294,6 @@ packages: '@types/resolve@1.20.2': resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} - '@types/semver@7.8.0': - resolution: {integrity: sha512-1mAINjtQCXXeLkJ9ehXkwOcBpqtLxiVtKhpUf83DdRNdQKV0iXZpaHYqRr7nj+wvxuJzoAmAwXI+sCNMv1CzLQ==} - '@types/serve-static@2.2.0': resolution: {integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==} @@ -8147,6 +8138,10 @@ packages: resolution: {integrity: sha512-sXMwN6DMHeouPfCxkxWkKAmxphWKEenHYY5H1nIBzU3PmDsmJp6kBXJdshjVdpMZuWCmL9SH7KFRx29AylpP6g==} engines: {node: '>=18.12.0'} + verkit@0.5.0: + resolution: {integrity: sha512-u4J1hr6Y+C/hit34GwL3eIcUf3wngT8TlYUjK7tk0jxydKLAq3sYvifjdTDA6U3KCKRcQCQDGf/srjxfZF3l7g==} + engines: {node: '>=18.12.0'} + vitest@5.0.1: resolution: {integrity: sha512-iA95lQbKEkvrtTkdAgnWbXfbipWiiWe/hDl2P5tMi6WFwD76G0NxXAGp/M9EOcYupeGJRr6wppMc7CoA41TQjg==} engines: {node: ^22.12.0 || ^24.0.0 || >=26.0.0} @@ -11184,8 +11179,6 @@ snapshots: '@types/resolve@1.20.2': {} - '@types/semver@7.8.0': {} - '@types/serve-static@2.2.0': dependencies: '@types/http-errors': 2.0.5 @@ -15007,6 +15000,8 @@ snapshots: verkit@0.4.0: {} + verkit@0.5.0: {} + vitest@5.0.1(@edge-runtime/vm@5.0.0)(@opentelemetry/api@1.9.0)(@types/node@24.10.3)(@vitest/browser-playwright@5.0.1)(@vitest/browser-preview@5.0.1)(@vitest/browser-webdriverio@5.0.0)(@vitest/ui@5.0.1)(happy-dom@20.0.10)(jsdom@27.2.0(supports-color@8.1.1))(vite@packages+core): dependencies: '@types/chai': 5.2.3 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index df14865a03..5c794386e3 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -44,7 +44,6 @@ catalog: '@types/picomatch': ^4.0.0 '@types/react': ^19.1.8 '@types/react-dom': ^19.1.6 - '@types/semver': ^7.8.0 '@types/serve-static': ^2.0.0 '@types/strip-comments': ^2.0.4 '@types/validate-npm-package-name': ^4.0.2 @@ -134,6 +133,7 @@ catalog: vitepress-plugin-group-icons: ^1.7.1 vitepress-plugin-llms: ^1.1.0 vitepress-plugin-og: ^0.0.5 + verkit: ^0.5.0 vitest: 5.0.1 vue: ^3.5.35 web-tree-sitter: ^0.27.0