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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:",
Expand All @@ -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:"
},
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/create/org-manifest.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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);
},
Expand Down Expand Up @@ -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({
Expand Down
8 changes: 4 additions & 4 deletions packages/cli/src/migration/bin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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(
Expand All @@ -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(
Expand Down
6 changes: 3 additions & 3 deletions packages/cli/src/migration/migrator/catalog.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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);
}
Expand Down
6 changes: 3 additions & 3 deletions packages/cli/src/migration/migrator/eslint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
}
Expand Down
6 changes: 3 additions & 3 deletions packages/cli/src/migration/migrator/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down
7 changes: 3 additions & 4 deletions packages/cli/src/migration/migrator/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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`,
Expand Down Expand Up @@ -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.
Expand Down
80 changes: 47 additions & 33 deletions packages/cli/src/migration/migrator/vitest-v5.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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') {
Expand All @@ -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}`]);
Expand All @@ -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(
Expand All @@ -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;
Expand All @@ -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);
}
Expand Down Expand Up @@ -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;
}
Expand All @@ -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) {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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) =>
Expand All @@ -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'),
Expand Down
Loading
Loading