feat: Add time field to abbreviated metadata for pnpm time-based resolution (#834)

- [x] Add time field to abbreviated manifests in
`_listPackageAbbreviatedManifests` method
- [x] Fix cache update logic to populate time field when adding versions
to abbreviated manifests
- [x] Fix cache update logic to remove time field when removing versions
from abbreviated manifests
- [x] Add comprehensive test coverage for time field in abbreviated
manifests
- [x] All tests passing (30/30 in ShowPackageController.test.ts)

closes https://github.com/cnpm/cnpmcore/issues/609

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Package metadata now consistently includes time information (created,
modified, and per-version publish timestamps) in both full and
abbreviated manifests.
* Removing a version also removes its corresponding time entry to keep
metadata accurate.
* Added guards to prevent writing invalid time data, improving
stability.

* **Tests**
* Added tests to verify presence and correctness of time fields in
abbreviated manifests, including created/modified and per-version
timestamps.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: fengmk2 <156269+fengmk2@users.noreply.github.com>
Co-authored-by: MK (fengmk2) <fengmk2@gmail.com>
This commit is contained in:
Copilot
2025-10-06 15:37:26 +08:00
committed by GitHub
parent 5b1da74746
commit 2e51399db1
2 changed files with 43 additions and 0 deletions

View File

@@ -925,6 +925,11 @@ export class PackageManagerService extends AbstractService {
if (abbreviatedManifest) { if (abbreviatedManifest) {
abbreviatedManifests.versions[packageVersion.version] = abbreviatedManifests.versions[packageVersion.version] =
abbreviatedManifest; abbreviatedManifest;
// abbreviatedManifests.time is guaranteed to exist since it's initialized in _listPackageAbbreviatedManifests
if (abbreviatedManifests.time) {
abbreviatedManifests.time[packageVersion.version] =
packageVersion.publishTime;
}
} }
} }
} }
@@ -937,6 +942,8 @@ export class PackageManagerService extends AbstractService {
delete fullManifests.time[version]; delete fullManifests.time[version];
// eslint-disable-next-line typescript-eslint/no-dynamic-delete // eslint-disable-next-line typescript-eslint/no-dynamic-delete
delete abbreviatedManifests.versions[version]; delete abbreviatedManifests.versions[version];
// eslint-disable-next-line typescript-eslint/no-dynamic-delete
delete abbreviatedManifests.time?.[version];
} }
} }
@@ -1357,6 +1364,10 @@ export class PackageManagerService extends AbstractService {
modified: pkg.updatedAt, modified: pkg.updatedAt,
name: pkg.fullname, name: pkg.fullname,
versions: {}, versions: {},
time: {
created: pkg.createdAt,
modified: pkg.updatedAt,
},
}; };
for (const packageVersion of packageVersions) { for (const packageVersion of packageVersions) {
@@ -1366,6 +1377,10 @@ export class PackageManagerService extends AbstractService {
); );
if (manifest) { if (manifest) {
data.versions[packageVersion.version] = manifest; data.versions[packageVersion.version] = manifest;
// data.time is guaranteed to exist since we initialize it above
if (data.time) {
data.time[packageVersion.version] = packageVersion.publishTime;
}
} }
} }
return data; return data;

View File

@@ -384,6 +384,34 @@ describe('test/port/controller/package/ShowPackageController.test.ts', () => {
.expect(201); .expect(201);
}); });
it('should include time field in abbreviated manifests for pnpm time-based resolution', async () => {
mock(app.config.cnpmcore, 'syncMode', 'all');
const res = await app
.httpRequest()
.get(`/${name}`)
.set('Accept', 'application/vnd.npm.install-v1+json')
.expect(200)
.expect('content-type', 'application/json; charset=utf-8');
const pkg = res.body;
// Verify time field structure exists
assert.ok(pkg.time, 'time field should be present in abbreviated manifests');
assert.ok(pkg.time.created, 'time.created should be present');
assert.ok(pkg.time.modified, 'time.modified should be present');
// Verify each version has a publish time
const versions = Object.keys(pkg.versions);
for (const version of versions) {
assert.ok(pkg.time[version], `time.${version} should be present for version ${version}`);
assert.ok(pkg.time[version] instanceof Date || typeof pkg.time[version] === 'string',
`time.${version} should be a Date or string`);
}
// Verify at least the expected versions have time entries
assert.ok(pkg.time['1.0.0'], 'time.1.0.0 should be present');
assert.ok(pkg.time['2.0.0'], 'time.2.0.0 should be present');
});
it('should show one scoped package with abbreviated manifests', async () => { it('should show one scoped package with abbreviated manifests', async () => {
const res = await app const res = await app
.httpRequest() .httpRequest()