Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
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
25 changes: 16 additions & 9 deletions app/core/service/PackageVersionService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,16 +114,23 @@ export class PackageVersionService {
version = versionMatchTag;
} else {
const range = new Range(fetchSpec);
const paddingSemVer = new SqlRange(range);
if (paddingSemVer.containPreRelease) {
const versions = await this.packageVersionRepository.findSatisfyVersionsWithPrerelease(
scope,
name,
paddingSemVer,
);
version = semver.maxSatisfying(versions, range);
// Prefer latest tag version if it satisfies the range
// e.g., latest=4.1.0, newest=4.2.0, range=^4 should return 4.1.0
const latestVersion = await this.packageVersionRepository.findVersionByTag(scope, name, 'latest');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

类似的 latest-4, latest-1 这种,需要做优先级处理吗?
如果同时存在 latest-4 和 latest,并且它们指向的版本不一致,以那个 tag 为最高优先级判断?

if (latestVersion && semver.satisfies(latestVersion, range)) {
version = latestVersion;
Comment on lines +119 to +121
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new logic to prefer the latest tag applies to ALL range queries, including the wildcard *. This will change the behavior of wildcard queries from returning the maximum satisfying version to returning the latest tag version when it satisfies the range.

This is a breaking change that will affect the existing test at test/core/service/PackageVersionService.test.ts lines 119-124, which expects getVersion(npa('mock_package@*')) to return '1.1.0' (the max version) when the latest tag points to '1.0.0'.

Consider adding special handling for the wildcard * range to preserve backward compatibility, or update all related tests to reflect this new behavior. The wildcard * is typically expected to return the highest version, not necessarily the latest tag.

Copilot uses AI. Check for mistakes.
} else {
version = await this.packageVersionRepository.findMaxSatisfyVersion(scope, name, paddingSemVer);
const paddingSemVer = new SqlRange(range);
if (paddingSemVer.containPreRelease) {
const versions = await this.packageVersionRepository.findSatisfyVersionsWithPrerelease(
scope,
name,
paddingSemVer,
);
version = semver.maxSatisfying(versions, range);
} else {
version = await this.packageVersionRepository.findMaxSatisfyVersion(scope, name, paddingSemVer);
}
}
Comment on lines +119 to 134
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

这个改动正确地实现了优先返回 latest 标签版本的功能。

不过,这个实现为每个版本范围(range)的解析都增加了一次数据库查询(findVersionByTag)。在某些情况下(例如,当 fetchSpec 本身也是一个标签时),这可能导致在一次版本解析的流程中有多达三次数据库查询。

考虑到版本范围解析是一个高频操作,这可能会对服务的整体性能产生影响。建议考虑对 latest 标签对应的版本号进行缓存,以减少数据库的压力。例如,可以为 findVersionByTag(scope, name, 'latest') 的结果添加一个较短时间的缓存(比如 1-5 分钟)。

}
}
Expand Down
71 changes: 71 additions & 0 deletions test/port/controller/package/ShowPackageVersionController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,77 @@ describe('test/port/controller/package/ShowPackageVersionController.test.ts', ()
assert.equal(res.body.error, `[NOT_FOUND] ${pkg.name}@beta-not-exists not found`);
});

it('should prefer latest tag version when it satisfies semver range', async () => {
// Scenario: latest=4.1.0, newest=4.2.0, request ^4 should return 4.1.0
mock(app.config.cnpmcore, 'allowPublishNonScopePackage', true);

// Publish 4.1.0
const pkg410 = await TestUtil.getFullPackage({
name: 'foo-prefer-latest',
version: '4.1.0',
versionObject: {
description: 'version 4.1.0',
},
});
await app
.httpRequest()
.put(`/${pkg410.name}`)
.set('authorization', publisher.authorization)
.set('user-agent', publisher.ua)
.send(pkg410)
.expect(201);

// Publish 4.2.0 without updating latest tag
const pkg420 = await TestUtil.getFullPackage({
name: 'foo-prefer-latest',
version: '4.2.0',
versionObject: {
description: 'version 4.2.0',
},
});
// Without dist-tags, latest tag won't be updated
delete pkg420['dist-tags'];
await app
.httpRequest()
.put(`/${pkg420.name}`)
.set('authorization', publisher.authorization)
.set('user-agent', publisher.ua)
.send(pkg420)
.expect(201);

// Verify latest tag is still 4.1.0
let res = await app.httpRequest().get(`/${pkg410.name}/latest`).expect(200);
Comment on lines +318 to +319
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test setup is incomplete. After publishing version 4.2.0, the test should verify that the package has both versions available before making assertions about which version is returned. Consider adding an assertion to confirm that both versions exist in the system after the second publish operation completes.

Suggested change
// Verify latest tag is still 4.1.0
let res = await app.httpRequest().get(`/${pkg410.name}/latest`).expect(200);
// Verify both versions exist after second publish
let res = await app.httpRequest().get(`/${pkg410.name}/4.1.0`).expect(200);
assert.equal(res.body.version, '4.1.0');
res = await app.httpRequest().get(`/${pkg410.name}/4.2.0`).expect(200);
assert.equal(res.body.version, '4.2.0');
// Verify latest tag is still 4.1.0
res = await app.httpRequest().get(`/${pkg410.name}/latest`).expect(200);

Copilot uses AI. Check for mistakes.
assert.equal(res.body.version, '4.1.0');

// Request ^4 should return latest tag version 4.1.0, not max version 4.2.0
res = await app.httpRequest().get(`/${pkg410.name}/^4`).expect(200);
assert.equal(res.body.version, '4.1.0');

res = await app.httpRequest().get(`/${pkg410.name}/%5E4`).expect(200);
assert.equal(res.body.version, '4.1.0');

// Request ^4.2.0 should return 4.2.0 (latest 4.1.0 doesn't satisfy ^4.2.0)
res = await app.httpRequest().get(`/${pkg410.name}/^4.2.0`).expect(200);
assert.equal(res.body.version, '4.2.0');

// Request >=4 should also return latest 4.1.0
res = await app.httpRequest().get(`/${pkg410.name}/>=4`).expect(200);
assert.equal(res.body.version, '4.1.0');

// After updating latest tag to 4.2.0, ^4 should return 4.2.0
await app
.httpRequest()
.put(`/-/package/${pkg410.name}/dist-tags/latest`)
.set('authorization', publisher.authorization)
.set('user-agent', publisher.ua)
.set('content-type', 'application/json')
.send(JSON.stringify('4.2.0'))
.expect(200);

res = await app.httpRequest().get(`/${pkg410.name}/^4`).expect(200);
assert.equal(res.body.version, '4.2.0');
});
Comment on lines 279 to 349
Copy link

Copilot AI Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test scenario is well-designed and covers multiple edge cases including URL-encoded version ranges. However, consider adding a test case for the wildcard * range to ensure it behaves as expected with the new logic. The wildcard is also parsed as a range and will be affected by the change to prefer the latest tag.

Copilot uses AI. Check for mistakes.

it('should 404 when version not exists', async () => {
const pkg = await TestUtil.getFullPackage({
name: '@cnpm/foo',
Expand Down
Loading