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:
parent
f1527f5f20
commit
37274e4c8d
28
.changeset/tricky-taxis-watch.md
Normal file
28
.changeset/tricky-taxis-watch.md
Normal file
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user