1
0
mirror of https://github.com/verdaccio/verdaccio.git synced 2024-12-20 17:05:52 +01:00

feat: implement abbreviated manifest (#3322)

* feat: implement abbreviated manifest

* chore: add time field

* chore: add abbreviated version

* chore: fix missing time

* chore: fix merge time issue

* Update jest.config.js

* add tests

* chore: add tests

* chore: add missing fields
This commit is contained in:
Juan Picado 2022-08-27 12:52:23 +02:00 committed by GitHub
parent f1527f5f20
commit 37274e4c8d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 486 additions and 70 deletions

@ -0,0 +1,28 @@
---
'@verdaccio/api': minor
'@verdaccio/types': minor
'@verdaccio/local-storage': minor
'@verdaccio/server-fastify': minor
'@verdaccio/store': minor
'@verdaccio/test-helper': minor
'@verdaccio/web': minor
---
feat: implement abbreviated manifest
Enable abbreviated manifest data by adding the header:
```
curl -H "Accept: application/vnd.npm.install-v1+json" https://registry.npmjs.org/verdaccio
```
It returns a filtered manifest, additionally includes the [time](https://github.com/pnpm/rfcs/pull/2) field by request.
Current support for packages managers:
- npm: yes
- pnpm: yes
- yarn classic: yes
- yarn modern (+2.x): [no](https://github.com/yarnpkg/berry/pull/3981#issuecomment-1076566096)
https://github.com/npm/registry/blob/master/docs/responses/package-metadata.md#abbreviated-metadata-format

@ -25,6 +25,7 @@ export default function (route: Router, auth: IAuth, storage: Storage): void {
const name = req.params.package;
let version = req.params.version;
const write = req.query.write === 'true';
const abbreviated = req.get('Accept') === Storage.ABBREVIATED_HEADER;
const requestOptions = {
protocol: req.protocol,
headers: req.headers as any,
@ -38,6 +39,7 @@ export default function (route: Router, auth: IAuth, storage: Storage): void {
const manifest = await storage.getPackageByOptions({
name,
uplinksLook: true,
abbreviated,
version,
requestOptions,
});

@ -56,7 +56,7 @@ const debug = buildDebug('verdaccio:api:publish');
*
* There are two possible flows:
*
* - Remove all pacakges (entirely)
* - Remove all packages (entirely)
* eg: npm unpublish package-name@* --force
* eg: npm unpublish package-name --force
*

@ -1,6 +1,7 @@
import supertest from 'supertest';
import { HEADERS, HEADER_TYPE, HTTP_STATUS } from '@verdaccio/core';
import { DIST_TAGS, HEADERS, HEADER_TYPE, HTTP_STATUS } from '@verdaccio/core';
import { Storage } from '@verdaccio/store';
import { initializeServer, publishVersion } from './_helper';
@ -45,4 +46,24 @@ describe('package', () => {
expect(response.body.name).toEqual(pkg);
}
);
test.each([['foo-abbreviated'], ['@scope/foo-abbreviated']])(
'should return abbreviated local manifest',
async (pkg) => {
await publishVersion(app, pkg, '1.0.0');
const response = await supertest(app)
.get(`/${pkg}`)
.set(HEADERS.ACCEPT, HEADERS.JSON)
.set(HEADERS.ACCEPT, Storage.ABBREVIATED_HEADER)
.expect(HEADER_TYPE.CONTENT_TYPE, HEADERS.JSON_CHARSET)
.expect(HTTP_STATUS.OK);
expect(response.body.name).toEqual(pkg);
expect(response.body.time).toBeDefined();
expect(response.body.modified).toBeDefined();
expect(response.body[DIST_TAGS]).toEqual({ latest: '1.0.0' });
expect(response.body.readme).not.toBeDefined();
expect(response.body._rev).not.toBeDefined();
expect(response.body.users).not.toBeDefined();
}
);
});

@ -92,7 +92,6 @@ export interface Tags {
export interface Version {
name: string;
version: string;
devDependencies?: string;
directories?: any;
dist: Dist;
author: string | Author;
@ -113,7 +112,11 @@ export interface Version {
scripts?: any;
homepage?: string;
etag?: string;
dependencies: any;
dependencies?: Dependencies;
peerDependencies?: Dependencies;
devDependencies?: Dependencies;
optionalDependencies?: Dependencies;
bundleDependencies?: Dependencies;
keywords?: string | string[];
nodeVersion?: string;
_id: string;
@ -121,6 +124,17 @@ export interface Version {
_npmUser: Author;
_hasShrinkwrap?: boolean;
deprecated?: string;
funding?: { type: string; url: string };
engines?: Engines;
hasInstallScript?: boolean;
}
export interface Dependencies {
[key: string]: string;
}
export interface Engines {
[key: string]: string;
}
export interface Versions {
@ -194,6 +208,32 @@ export interface Manifest extends FullRemoteManifest, PublishManifest {
*/
_rev: string;
}
export type AbbreviatedVersion = Pick<
Version,
| 'name'
| 'version'
| 'description'
| 'dependencies'
| 'devDependencies'
| 'bin'
| 'dist'
| 'engines'
| 'funding'
| 'peerDependencies'
>;
export interface AbbreviatedVersions {
[key: string]: AbbreviatedVersion;
}
/**
*
*/
export type AbbreviatedManifest = Pick<Manifest, 'name' | 'dist-tags' | 'time'> & {
modified: string;
versions: AbbreviatedVersions;
};
export interface PublishManifest {
/**
* The `_attachments` object has different usages:

@ -40,7 +40,7 @@ class LocalDatabase extends TokenActions implements IPluginStorage {
debug('config path %o', config.configPath);
this.path = _dbGenPath(DB_NAME, config);
this.storages = this._getCustomPackageLocalStorages();
this.logger.debug({ path: this.path }, 'local storage path @{path}');
this.logger.info({ path: this.path }, 'local storage path @{path}');
debug('plugin storage path %o', this.path);
}

@ -285,7 +285,7 @@ export default class LocalFS implements ILocalFSPackageManager {
});
// if upload is aborted, we clean up the temporal file
signal.addEventListener(
signal?.addEventListener(
'abort',
async () => {
if (opened) {

@ -16,6 +16,8 @@ async function manifestRoute(fastify: FastifyInstance) {
const { name } = request.params;
const storage = fastify.storage;
debug('pkg name %s ', name);
// @ts-ignore
const abbreviated = request.headers['accept'] === Storage.ABBREVIATED_HEADER;
const data = await storage?.getPackageByOptions({
name,
// @ts-ignore
@ -25,6 +27,7 @@ async function manifestRoute(fastify: FastifyInstance) {
headers: request.headers as any,
host: request.hostname,
},
abbreviated,
});
return data;
});

@ -3,6 +3,7 @@ import { FastifyInstance } from 'fastify';
import _ from 'lodash';
import sanitizyReadme from '@verdaccio/readme';
import { Manifest } from '@verdaccio/types';
const debug = buildDebug('verdaccio:fastify:web:readme');
export const NOT_README_FOUND = 'ERROR: No README data found!';
@ -12,7 +13,7 @@ async function readmeRoute(fastify: FastifyInstance) {
// @ts-ignore
const { version, packageName } = request.params;
debug('readme name %s version: %s', packageName, version);
const manifest = await fastify.storage?.getPackageByOptions({
const manifest = (await fastify.storage?.getPackageByOptions({
name: packageName,
// remove on refactor getPackageByOptions
// @ts-ignore
@ -24,7 +25,7 @@ async function readmeRoute(fastify: FastifyInstance) {
headers: request.headers as any,
host: request.hostname,
},
});
})) as Manifest;
try {
const parsedReadme = parseReadme(manifest.name, manifest.readme as string);
reply.code(fastify.statusCode.OK).send(parsedReadme);
@ -37,7 +38,7 @@ async function readmeRoute(fastify: FastifyInstance) {
// @ts-ignore
const { version, packageName } = request.params;
debug('readme name %s version: %s', packageName, version);
const manifest = await fastify.storage?.getPackageByOptions({
const manifest = (await fastify.storage?.getPackageByOptions({
name: packageName,
// remove on refactor getPackageByOptions
// @ts-ignore
@ -49,7 +50,7 @@ async function readmeRoute(fastify: FastifyInstance) {
headers: request.headers as any,
host: request.hostname,
},
});
})) as Manifest;
try {
const parsedReadme = parseReadme(manifest.name, manifest.readme as string);
reply.code(fastify.statusCode.OK).send(parsedReadme);

@ -4,9 +4,9 @@ module.exports = Object.assign({}, config, {
coverageThreshold: {
global: {
// FIXME: increase to 90
branches: 51,
functions: 75,
lines: 64,
branches: 55,
functions: 81,
lines: 71,
},
},
});

@ -256,7 +256,7 @@ export function hasInvalidPublishBody(manifest: Pick<Manifest, '_attachments' |
*/
export function mergeVersions(cacheManifest: Manifest, remoteManifest: Manifest): Manifest {
let _cacheManifest = { ...cacheManifest };
const { versions } = remoteManifest;
const { versions, time } = remoteManifest;
// copy new versions to a cache
// NOTE: if a certain version was updated, we can't refresh it reliably
for (const i in versions) {
@ -265,6 +265,12 @@ export function mergeVersions(cacheManifest: Manifest, remoteManifest: Manifest)
}
}
for (const i in time) {
if (typeof cacheManifest.time[i] === 'undefined') {
_cacheManifest.time[i] = time[i];
}
}
for (const distTag in remoteManifest[DIST_TAGS]) {
if (_cacheManifest[DIST_TAGS][distTag] !== remoteManifest[DIST_TAGS][distTag]) {
if (

@ -25,6 +25,8 @@ import {
convertDistVersionToLocalTarballsUrl,
} from '@verdaccio/tarball';
import {
AbbreviatedManifest,
AbbreviatedVersions,
Author,
Config,
DistFile,
@ -91,6 +93,9 @@ class Storage {
debug('uplinks available %o', Object.keys(this.uplinks));
}
static ABBREVIATED_HEADER =
'application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8, */*';
/**
* Change an existing package (i.e. unpublish one version)
Function changes a package info from local storage and all uplinks with write access./
@ -523,17 +528,66 @@ class Storage {
return convertedManifest;
}
private convertAbbreviatedManifest(manifest: Manifest): AbbreviatedManifest {
const abbreviatedVersions = Object.keys(manifest.versions).reduce(
(acc: AbbreviatedVersions, version: string) => {
const _version = manifest.versions[version];
// This should be align with this document
// https://github.com/npm/registry/blob/master/docs/responses/package-metadata.md#abbreviated-version-object
const _version_abbreviated = {
name: _version.name,
version: _version.version,
description: _version.description,
deprecated: _version.deprecated,
bin: _version.bin,
dist: _version.dist,
engines: _version.engines,
funding: _version.funding,
directories: _version.directories,
dependencies: _version.dependencies,
devDependencies: _version.devDependencies,
peerDependencies: _version.peerDependencies,
optionalDependencies: _version.optionalDependencies,
bundleDependencies: _version.bundleDependencies,
// npm cli specifics
_hasShrinkwrap: _version._hasShrinkwrap,
hasInstallScript: _version.hasInstallScript,
};
acc[version] = _version_abbreviated;
return acc;
},
{}
);
const convertedManifest = {
name: manifest['name'],
[DIST_TAGS]: manifest[DIST_TAGS],
versions: abbreviatedVersions,
modified: manifest.time.modified,
// NOTE: special case for pnpm https://github.com/pnpm/rfcs/pull/2
time: manifest.time,
};
return convertedManifest;
}
/**
* Return a manifest or version based on the options.
* @param options {Object}
* @returns A package manifest or specific version
*/
public async getPackageByOptions(options: IGetPackageOptionsNext): Promise<Manifest | Version> {
public async getPackageByOptions(
options: IGetPackageOptionsNext
): Promise<Manifest | AbbreviatedManifest | Version> {
// if no version we return the whole manifest
if (_.isNil(options.version) === false) {
return this.getPackageByVersion(options);
} else {
return this.getPackageManifest(options);
const manifest = await this.getPackageManifest(options);
if (options.abbreviated === true) {
debug('abbreviated manifest');
return this.convertAbbreviatedManifest(manifest);
}
return manifest;
}
}
@ -596,7 +650,7 @@ class Storage {
}
/**
* Initialize the storage asyncronously.
* Initialize the storage asynchronously.
* @param config Config
* @param filters IPluginFilters
* @returns Storage instance
@ -1613,7 +1667,7 @@ class Storage {
* @param options options
* @returns Returns a promise that resolves with the merged manifest.
*/
public async mergeCacheRemoteMetadata(
private async mergeCacheRemoteMetadata(
uplink: IProxy,
cachedManifest: Manifest,
options: ISyncUplinksOptions
@ -1642,7 +1696,7 @@ class Storage {
);
try {
_cacheManifest = validatioUtils.normalizeMetadata(remoteManifest, _cacheManifest.name);
_cacheManifest = validatioUtils.normalizeMetadata(_cacheManifest, _cacheManifest.name);
} catch (err: any) {
this.logger.error(
{
@ -1657,7 +1711,7 @@ class Storage {
// merge time field cache and remote
_cacheManifest = mergeUplinkTimeIntoLocalNext(_cacheManifest, remoteManifest);
// update the _uplinks field in the cache
_cacheManifest = updateVersionsHiddenUpLinkNext(cachedManifest, uplink);
_cacheManifest = updateVersionsHiddenUpLinkNext(_cacheManifest, uplink);
try {
// merge versions from remote into the cache
_cacheManifest = mergeVersions(_cacheManifest, remoteManifest);
@ -1667,7 +1721,7 @@ class Storage {
{
err: err,
},
'package.json mergin has failed @{!err?.message}\n@{err.stack}'
'package.json merge has failed @{!err?.message}\n@{err.stack}'
);
throw err;
}

@ -33,6 +33,12 @@ export type IGetPackageOptionsNext = {
* internally indicates to avoid any cache layer.
*/
byPassCache?: boolean;
/**
* Reduce the package metadata to the minimum required to get the package.
* https://github.com/npm/registry/blob/c0b573593fb5d6e0268de7d6612addd7059cb779/docs/responses/package-metadata.md#package-metadata
*/
abbreviated?: boolean;
};
// @deprecate remove this type

@ -11,16 +11,17 @@ import { API_ERROR, DIST_TAGS, HEADERS, HEADER_TYPE, errorUtils, fileUtils } fro
import { setup } from '@verdaccio/logger';
import {
addNewVersion,
generateLocalPackageMetadata,
generatePackageMetadata,
generateRemotePackageMetadata,
} from '@verdaccio/test-helper';
import { Manifest, Version } from '@verdaccio/types';
import { AbbreviatedManifest, Manifest, Version } from '@verdaccio/types';
import { Storage } from '../src';
import manifestFooRemoteNpmjs from './fixtures/manifests/foo-npmjs.json';
import { configExample } from './helpers';
function generateRamdonStorage() {
function generateRandomStorage() {
const tempStorage = pseudoRandomBytes(5).toString('hex');
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), '/verdaccio-test'));
@ -29,7 +30,7 @@ function generateRamdonStorage() {
setup({ type: 'stdout', format: 'pretty', level: 'trace' });
const domain = 'http://localhost:4873';
const domain = 'https://registry.npmjs.org';
const fakeHost = 'localhost:4873';
const fooManifest = generatePackageMetadata('foo', '1.0.0');
@ -40,23 +41,6 @@ describe('storage', () => {
jest.clearAllMocks();
});
// describe('add packages', () => {
// test('add package item', async () => {
// nock(domain).get('/foo').reply(404);
// const config = new Config(
// configExample({
// storage: generateRamdonStorage(),
// })
// );
// const storage = new Storage(config);
// await storage.init(config);
// await storage.addPackage('foo', fooManifest, (err) => {
// expect(err).toBeNull();
// });
// });
// });
describe('updateManifest', () => {
test('create private package', async () => {
const mockDate = '2018-01-14T11:17:40.712Z';
@ -71,7 +55,7 @@ describe('storage', () => {
configExample(
{
...getDefaultConfig(),
storage: generateRamdonStorage(),
storage: generateRandomStorage(),
},
'./fixtures/config/updateManifest-1.yaml',
__dirname
@ -114,7 +98,7 @@ describe('storage', () => {
const config = new Config(
configExample(
{
storage: generateRamdonStorage(),
storage: generateRandomStorage(),
},
'./fixtures/config/updateManifest-1.yaml',
__dirname
@ -155,7 +139,7 @@ describe('storage', () => {
},
};
const pkgName = 'upstream';
// const storage = generateRamdonStorage();
// const storage = generateRandomStorage();
const config = new Config(
configExample(
{
@ -246,7 +230,7 @@ describe('storage', () => {
const config = new Config(
configExample(
{
storage: generateRamdonStorage(),
storage: generateRandomStorage(),
},
'./fixtures/config/getTarballNext-getupstream.yaml',
__dirname
@ -276,7 +260,7 @@ describe('storage', () => {
const config = new Config(
configExample({
...getDefaultConfig(),
storage: generateRamdonStorage(),
storage: generateRandomStorage(),
})
);
const storage = new Storage(config);
@ -313,7 +297,7 @@ describe('storage', () => {
const config = new Config(
configExample(
{
storage: generateRamdonStorage(),
storage: generateRandomStorage(),
},
'./fixtures/config/getTarballNext-getupstream.yaml',
__dirname
@ -357,7 +341,7 @@ describe('storage', () => {
const config = new Config(
configExample(
{
storage: generateRamdonStorage(),
storage: generateRandomStorage(),
},
'./fixtures/config/getTarballNext-getupstream.yaml',
__dirname
@ -417,7 +401,7 @@ describe('storage', () => {
.replyWithFile(201, path.join(__dirname, 'fixtures/tarball.tgz'), {
[HEADER_TYPE.CONTENT_LENGTH]: 277,
});
const storagePath = generateRamdonStorage();
const storagePath = generateRandomStorage();
const config = new Config(
configExample(
{
@ -474,7 +458,7 @@ describe('storage', () => {
const config = new Config(
configExample(
{
storage: generateRamdonStorage(),
storage: generateRandomStorage(),
},
'./fixtures/config/getTarballNext-getupstream.yaml',
__dirname
@ -532,7 +516,7 @@ describe('storage', () => {
const config = new Config(
configExample(
{
storage: generateRamdonStorage(),
storage: generateRandomStorage(),
},
'./fixtures/config/syncDoubleUplinksMetadata.yaml',
__dirname
@ -562,7 +546,7 @@ describe('storage', () => {
const config = new Config(
configExample(
{
storage: generateRamdonStorage(),
storage: generateRandomStorage(),
},
'./fixtures/config/syncSingleUplinksMetadata.yaml',
__dirname
@ -583,7 +567,7 @@ describe('storage', () => {
const config = new Config(
configExample(
{
storage: generateRamdonStorage(),
storage: generateRandomStorage(),
},
'./fixtures/config/syncSingleUplinksMetadata.yaml',
__dirname
@ -600,12 +584,12 @@ describe('storage', () => {
describe('success scenarios', () => {
test('should handle one proxy success', async () => {
const fooManifest = generatePackageMetadata('foo', '8.0.0');
const fooManifest = generateLocalPackageMetadata('foo', '8.0.0');
nock('https://registry.verdaccio.org').get('/foo').reply(201, manifestFooRemoteNpmjs);
const config = new Config(
configExample(
{
storage: generateRamdonStorage(),
storage: generateRandomStorage(),
},
'./fixtures/config/syncSingleUplinksMetadata.yaml',
__dirname
@ -617,7 +601,28 @@ describe('storage', () => {
const [response] = await storage.syncUplinksMetadataNext(fooManifest.name, fooManifest);
expect(response).not.toBeNull();
expect((response as Manifest).name).toEqual(fooManifest.name);
expect(Object.keys((response as Manifest).versions)).toEqual([
'8.0.0',
'1.0.0',
'0.0.3',
'0.0.4',
'0.0.5',
'0.0.6',
'0.0.7',
]);
expect(Object.keys((response as Manifest).time)).toEqual([
'modified',
'created',
'8.0.0',
'1.0.0',
'0.0.3',
'0.0.4',
'0.0.5',
'0.0.6',
'0.0.7',
]);
expect((response as Manifest)[DIST_TAGS].latest).toEqual('8.0.0');
expect((response as Manifest).time['8.0.0']).toBeDefined();
});
test('should handle one proxy success with no local cache manifest', async () => {
@ -625,7 +630,7 @@ describe('storage', () => {
const config = new Config(
configExample(
{
storage: generateRamdonStorage(),
storage: generateRandomStorage(),
},
'./fixtures/config/syncSingleUplinksMetadata.yaml',
__dirname
@ -647,7 +652,7 @@ describe('storage', () => {
const config = new Config(
configExample(
{
storage: generateRamdonStorage(),
storage: generateRandomStorage(),
},
'./fixtures/config/syncNoUplinksMetadata.yaml',
__dirname
@ -670,7 +675,7 @@ describe('storage', () => {
const config = new Config(
configExample(
{
storage: generateRamdonStorage(),
storage: generateRandomStorage(),
},
'./fixtures/config/syncSingleUplinksMetadata.yaml',
__dirname
@ -689,7 +694,184 @@ describe('storage', () => {
});
});
// TODO: getPackageNext should replace getPackage eventually
describe('getLocalDatabaseNext', () => {
test('should return 0 local packages', async () => {
const config = new Config(
configExample({
...getDefaultConfig(),
storage: generateRandomStorage(),
})
);
const storage = new Storage(config);
await storage.init(config);
await expect(storage.getLocalDatabaseNext()).resolves.toHaveLength(0);
});
test('should return 1 local packages', async () => {
const config = new Config(
configExample({
...getDefaultConfig(),
storage: generateRandomStorage(),
})
);
const req = httpMocks.createRequest({
method: 'GET',
connection: { remoteAddress: fakeHost },
headers: {
host: 'host',
},
url: '/',
});
const storage = new Storage(config);
await storage.init(config);
const manifest = generatePackageMetadata('foo');
const ac = new AbortController();
await storage.updateManifest(manifest, {
signal: ac.signal,
name: 'foo',
uplinksLook: false,
requestOptions: {
headers: req.headers as any,
protocol: req.protocol,
host: req.get('host') as string,
},
});
const response = await storage.getLocalDatabaseNext();
expect(response).toHaveLength(1);
expect(response[0]).toEqual(expect.objectContaining({ name: 'foo', version: '1.0.0' }));
});
});
describe('tokens', () => {
describe('saveToken', () => {
test('should retrieve tokens created', async () => {
const config = new Config(
configExample({
...getDefaultConfig(),
storage: generateRandomStorage(),
})
);
const storage = new Storage(config);
await storage.init(config);
await storage.saveToken({
user: 'foo',
token: 'secret',
key: 'key',
created: 'created',
readonly: true,
});
const tokens = await storage.readTokens({ user: 'foo' });
expect(tokens).toEqual([
{ user: 'foo', token: 'secret', key: 'key', readonly: true, created: 'created' },
]);
});
test('should delete a token created', async () => {
const config = new Config(
configExample({
...getDefaultConfig(),
storage: generateRandomStorage(),
})
);
const storage = new Storage(config);
await storage.init(config);
await storage.saveToken({
user: 'foo',
token: 'secret',
key: 'key',
created: 'created',
readonly: true,
});
const tokens = await storage.readTokens({ user: 'foo' });
expect(tokens).toHaveLength(1);
await storage.deleteToken('foo', 'key');
const tokens2 = await storage.readTokens({ user: 'foo' });
expect(tokens2).toHaveLength(0);
});
});
});
describe('removeTarball', () => {
test('should fail on remove tarball of package does not exist', async () => {
const config = new Config(
configExample({
...getDefaultConfig(),
storage: generateRandomStorage(),
})
);
const storage = new Storage(config);
await storage.init(config);
await expect(storage.removeTarball('foo', 'foo-1.0.0.tgz', 'rev')).rejects.toThrow(
API_ERROR.NO_PACKAGE
);
});
});
describe('removePackage', () => {
test('should remove entirely a package', async () => {
const config = new Config(
configExample({
...getDefaultConfig(),
storage: generateRandomStorage(),
})
);
const req = httpMocks.createRequest({
method: 'GET',
connection: { remoteAddress: fakeHost },
headers: {
host: fakeHost,
[HEADERS.FORWARDED_PROTO]: 'http',
},
url: '/',
});
const storage = new Storage(config);
await storage.init(config);
const manifest = generatePackageMetadata('foo');
const ac = new AbortController();
// 1. publish a package
await storage.updateManifest(manifest, {
signal: ac.signal,
name: 'foo',
uplinksLook: false,
requestOptions: {
headers: req.headers as any,
protocol: req.protocol,
host: req.get('host') as string,
},
});
// 2. request package (should be available in the local cache)
const manifest1 = (await storage.getPackageByOptions({
name: 'foo',
uplinksLook: false,
requestOptions: {
headers: req.headers as any,
protocol: req.protocol,
host: req.get('host') as string,
},
})) as Manifest;
const _rev = manifest1._rev;
// 3. remove the tarball
await expect(
storage.removeTarball(manifest1.name, 'foo-1.0.0.tgz', _rev)
).resolves.toBeDefined();
// 4. remove the package
await storage.removePackage(manifest1.name, _rev);
// 5. fails if package does not exist anymore in storage
await expect(
storage.getPackageByOptions({
name: 'foo',
uplinksLook: false,
requestOptions: {
headers: req.headers as any,
protocol: req.protocol,
host: req.get('host') as string,
},
})
).rejects.toThrowError('package does not exist on uplink: foo');
});
});
describe('get packages getPackageByOptions()', () => {
describe('with uplinks', () => {
test('should get 201 and merge from uplink', async () => {
@ -697,7 +879,7 @@ describe('storage', () => {
const config = new Config(
configExample({
...getDefaultConfig(),
storage: generateRamdonStorage(),
storage: generateRandomStorage(),
})
);
const req = httpMocks.createRequest({
@ -729,7 +911,7 @@ describe('storage', () => {
const config = new Config(
configExample({
...getDefaultConfig(),
storage: generateRamdonStorage(),
storage: generateRandomStorage(),
})
);
const req = httpMocks.createRequest({
@ -762,7 +944,7 @@ describe('storage', () => {
const config = new Config(
configExample({
...getDefaultConfig(),
storage: generateRamdonStorage(),
storage: generateRandomStorage(),
})
);
const req = httpMocks.createRequest({
@ -795,7 +977,7 @@ describe('storage', () => {
const config = new Config(
configExample({
...getDefaultConfig(),
storage: generateRamdonStorage(),
storage: generateRandomStorage(),
})
);
const req = httpMocks.createRequest({
@ -835,7 +1017,7 @@ describe('storage', () => {
url: domain,
},
},
storage: generateRamdonStorage(),
storage: generateRandomStorage(),
})
);
const req = httpMocks.createRequest({
@ -875,7 +1057,7 @@ describe('storage', () => {
url: domain,
},
},
storage: generateRamdonStorage(),
storage: generateRandomStorage(),
})
);
const req = httpMocks.createRequest({
@ -902,6 +1084,72 @@ describe('storage', () => {
})
).rejects.toThrow(errorUtils.getServiceUnavailable('ETIMEDOUT'));
});
test('should fetch abbreviated version of manifest ', async () => {
const fooManifest = generateLocalPackageMetadata('foo', '1.0.0');
nock(domain).get('/foo').reply(201, fooManifest);
const config = new Config(
configExample({
...getDefaultConfig(),
storage: generateRandomStorage(),
})
);
const req = httpMocks.createRequest({
method: 'GET',
connection: { remoteAddress: fakeHost },
headers: {
host: fakeHost,
[HEADERS.FORWARDED_PROTO]: 'http',
},
url: '/',
});
const storage = new Storage(config);
await storage.init(config);
const manifest = (await storage.getPackageByOptions({
name: 'foo',
uplinksLook: true,
requestOptions: {
headers: req.headers as any,
protocol: req.protocol,
host: req.get('host') as string,
},
abbreviated: true,
})) as AbbreviatedManifest;
const { versions, name } = manifest;
expect(name).toEqual('foo');
expect(Object.keys(versions)).toEqual(['1.0.0']);
expect(manifest[DIST_TAGS]).toEqual({ latest: '1.0.0' });
const version = versions['1.0.0'];
expect(Object.keys(version)).toEqual([
'name',
'version',
'description',
'deprecated',
'bin',
'dist',
'engines',
'funding',
'directories',
'dependencies',
'devDependencies',
'peerDependencies',
'optionalDependencies',
'bundleDependencies',
'_hasShrinkwrap',
'hasInstallScript',
]);
expect(manifest.modified).toBeDefined();
// special case for pnpm/rfcs/pull/2
expect(manifest.time).toBeDefined();
// fields must not have
// @ts-expect-error
expect(manifest.readme).not.toBeDefined();
// @ts-expect-error
expect(manifest._attachments).not.toBeDefined();
// @ts-expect-error
expect(manifest._rev).not.toBeDefined();
});
});
});
});

@ -1,4 +1,4 @@
import { Manifest } from '@verdaccio/types';
import { GenericBody, Manifest } from '@verdaccio/types';
export interface DistTags {
[key: string]: string;
@ -73,7 +73,8 @@ export function addNewVersion(
export function generateLocalPackageMetadata(
pkgName: string,
version = '1.0.0',
domain: string = 'http://localhost:5555'
domain: string = 'http://localhost:5555',
time?: GenericBody
): Manifest {
// @ts-ignore
return {
@ -115,6 +116,11 @@ export function generateLocalPackageMetadata(
},
},
},
time: time ?? {
modified: new Date().toISOString(),
created: new Date().toISOString(),
[version]: new Date().toISOString(),
},
readme: '# test',
_attachments: {
[`${getTarball(pkgName)}-${version}.tgz`]: {

@ -75,7 +75,7 @@ function addPackageWebApi(storage: Storage, auth: IAuth, config: Config): Router
}
} catch (err: any) {
debug('process packages error %o', err);
logger.logger.error(
logger.error(
{ name: pkg.name, error: err },
'permission process for @{name} has failed: @{error}'
);

@ -45,11 +45,12 @@ function addReadmeWebApi(storage: Storage, auth: IAuth): Router {
remoteAddress: req.socket.remoteAddress,
};
try {
const manifest = await storage.getPackageByOptions({
const manifest = (await storage.getPackageByOptions({
name,
uplinksLook: true,
abbreviated: false,
requestOptions,
});
})) as Manifest;
debug('readme pkg %o', manifest?.name);
res.set(HEADER_TYPE.CONTENT_TYPE, HEADERS.TEXT_PLAIN_UTF8);
try {