diff --git a/src/utils/version.ts b/src/utils/version.ts index 686b29e..54f1262 100644 --- a/src/utils/version.ts +++ b/src/utils/version.ts @@ -30,13 +30,52 @@ async function resolveInstalledVersion(cwd: string): Promise { return parseVersion(packageInfo?.version ?? '') } -export async function resolveFallBack(dataPath = getDataPath()): Promise { +async function getAvailableVersions(dataPath: string): Promise { const entries = await readdir(dataPath, { withFileTypes: true }) - const versions = entries + + return entries .filter(entry => entry.isFile()) .map(entry => entry.name.match(VERSION_FILE_RE)?.[1]) .filter((version): version is string => Boolean(version && semver.valid(version))) .sort(semver.rcompare) +} + +async function resolveAvailableVersion(resolvedVersion: ResolvedVersion, dataPath: string): Promise { + if (!resolvedVersion.version) { + return resolvedVersion + } + + const requestedVersion = semver.parse(resolvedVersion.version) + if (!requestedVersion) { + return resolvedVersion + } + + const versions = await getAvailableVersions(dataPath) + + const exactVersion = versions.find(version => semver.eq(version, requestedVersion)) + if (exactVersion) { + return parseVersion(exactVersion) + } + + const latestMinorVersion = versions.find((version) => { + return semver.major(version) === requestedVersion.major + && semver.minor(version) === requestedVersion.minor + }) + if (latestMinorVersion) { + return parseVersion(latestMinorVersion) + } + + // v{major}.json is the latest-version alias for that major. Returning its + // concrete semantic version keeps loadVersionMetaData pointed at a versioned file. + const latestMajorVersion = versions.find((version) => { + return semver.major(version) === requestedVersion.major + }) + + return latestMajorVersion ? parseVersion(latestMajorVersion) : resolvedVersion +} + +export async function resolveFallBack(dataPath = getDataPath()): Promise { + const versions = await getAvailableVersions(dataPath) const resolvedVersion = versions[0] && parseVersion(versions[0]) @@ -47,23 +86,19 @@ export async function resolveFallBack(dataPath = getDataPath()): Promise { +export async function resolveVersion(config: ResolvedConfig, dataPath = getDataPath()): Promise { const { version, cwd } = config if (version) { const resolvedVersion = parseVersion(version) - if (!resolvedVersion) { - throw new TypeError(`Invalid antdv-next version: ${version}`) - } - - return resolvedVersion + return await resolveAvailableVersion(resolvedVersion, dataPath) } const pkgVersion = await resolveInstalledVersion(cwd) if (pkgVersion.version) { - return pkgVersion + return await resolveAvailableVersion(pkgVersion, dataPath) } - return await resolveFallBack() + return await resolveFallBack(dataPath) } diff --git a/test/version.test.ts b/test/version.test.ts index cae3ec5..9e8ab44 100644 --- a/test/version.test.ts +++ b/test/version.test.ts @@ -23,6 +23,12 @@ function createConfig(version = '', cwd = '/project'): ResolvedConfig { beforeEach(async () => { mockedGetPackageInfo.mockReset() await mkdir(dataPath, { recursive: true }) + await Promise.all([ + writeFile(join(dataPath, 'v1.2.3.json'), '{}'), + writeFile(join(dataPath, 'v1.5.1.json'), '{}'), + writeFile(join(dataPath, 'v1.5.2.json'), '{}'), + writeFile(join(dataPath, 'v1.json'), '{}'), + ]) }) afterEach(async () => { @@ -31,15 +37,31 @@ afterEach(async () => { describe('resolveVersion', () => { it('resolves and normalizes an explicitly provided version', async () => { - await expect(resolveVersion(createConfig(' v1.2.3 '))).resolves.toEqual({ + await expect(resolveVersion(createConfig(' v1.2.3 '), dataPath)).resolves.toEqual({ version: '1.2.3', majorVersion: 'v1', }) expect(mockedGetPackageInfo).not.toHaveBeenCalled() }) + it('resolves a missing patch to the greatest version in the same minor', async () => { + await expect(resolveVersion(createConfig('v1.5.4'), dataPath)).resolves.toEqual({ + version: '1.5.2', + majorVersion: 'v1', + }) + expect(mockedGetPackageInfo).not.toHaveBeenCalled() + }) + + it('resolves a missing minor to the latest version in the same major', async () => { + await expect(resolveVersion(createConfig('v1.6.0'), dataPath)).resolves.toEqual({ + version: '1.5.2', + majorVersion: 'v1', + }) + expect(mockedGetPackageInfo).not.toHaveBeenCalled() + }) + it('returns an empty sentinel when an explicit version cannot be coerced', async () => { - await expect(resolveVersion(createConfig('latest'))).resolves.toEqual({ + await expect(resolveVersion(createConfig('latest'), dataPath)).resolves.toEqual({ version: '', majorVersion: 'v0', }) @@ -55,7 +77,7 @@ describe('resolveVersion', () => { packageJson: {}, }) - await expect(resolveVersion(createConfig())).resolves.toEqual({ + await expect(resolveVersion(createConfig(), dataPath)).resolves.toEqual({ version: '2.4.1', majorVersion: 'v2', }) @@ -82,6 +104,8 @@ describe('resolveFallBack', () => { }) it('throws when the data directory has no valid version files', async () => { + await rm(dataPath, { recursive: true }) + await mkdir(dataPath) await writeFile(join(dataPath, 'version.json'), '{}') await expect(resolveFallBack(dataPath)).rejects.toThrow(