diff --git a/.changeset/tricky-taxis-watch.md b/.changeset/tricky-taxis-watch.md new file mode 100644 index 000000000..9ea856562 --- /dev/null +++ b/.changeset/tricky-taxis-watch.md @@ -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 diff --git a/packages/api/src/package.ts b/packages/api/src/package.ts index 91405d1a2..33d8d3305 100644 --- a/packages/api/src/package.ts +++ b/packages/api/src/package.ts @@ -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, }); diff --git a/packages/api/src/publish.ts b/packages/api/src/publish.ts index 88461ca09..183c3b796 100644 --- a/packages/api/src/publish.ts +++ b/packages/api/src/publish.ts @@ -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 * diff --git a/packages/api/test/integration/package.spec.ts b/packages/api/test/integration/package.spec.ts index e17c6fe94..205f8ce2f 100644 --- a/packages/api/test/integration/package.spec.ts +++ b/packages/api/test/integration/package.spec.ts @@ -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(); + } + ); }); diff --git a/packages/core/types/src/manifest.ts b/packages/core/types/src/manifest.ts index cc72daa6d..5a53d286c 100644 --- a/packages/core/types/src/manifest.ts +++ b/packages/core/types/src/manifest.ts @@ -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 & { + modified: string; + versions: AbbreviatedVersions; +}; + export interface PublishManifest { /** * The `_attachments` object has different usages: diff --git a/packages/plugins/local-storage/src/local-database.ts b/packages/plugins/local-storage/src/local-database.ts index 7ef7612cc..975b9288a 100644 --- a/packages/plugins/local-storage/src/local-database.ts +++ b/packages/plugins/local-storage/src/local-database.ts @@ -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); } diff --git a/packages/plugins/local-storage/src/local-fs.ts b/packages/plugins/local-storage/src/local-fs.ts index 950c377b8..c675e6299 100644 --- a/packages/plugins/local-storage/src/local-fs.ts +++ b/packages/plugins/local-storage/src/local-fs.ts @@ -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) { diff --git a/packages/server/fastify/src/endpoints/manifest.ts b/packages/server/fastify/src/endpoints/manifest.ts index d3089540a..c55faa29f 100644 --- a/packages/server/fastify/src/endpoints/manifest.ts +++ b/packages/server/fastify/src/endpoints/manifest.ts @@ -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; }); diff --git a/packages/server/fastify/src/routes/web/api/readme.ts b/packages/server/fastify/src/routes/web/api/readme.ts index 21a02a5b4..d5fa62804 100644 --- a/packages/server/fastify/src/routes/web/api/readme.ts +++ b/packages/server/fastify/src/routes/web/api/readme.ts @@ -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); diff --git a/packages/store/jest.config.js b/packages/store/jest.config.js index 15fc12297..535c138d4 100644 --- a/packages/store/jest.config.js +++ b/packages/store/jest.config.js @@ -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, }, }, }); diff --git a/packages/store/src/lib/storage-utils.ts b/packages/store/src/lib/storage-utils.ts index 54ea06e48..f7c322416 100644 --- a/packages/store/src/lib/storage-utils.ts +++ b/packages/store/src/lib/storage-utils.ts @@ -256,7 +256,7 @@ export function hasInvalidPublishBody(manifest: Pick { + 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 { + public async getPackageByOptions( + options: IGetPackageOptionsNext + ): Promise { // 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; } diff --git a/packages/store/src/type.ts b/packages/store/src/type.ts index 3f509a265..d062770b0 100644 --- a/packages/store/src/type.ts +++ b/packages/store/src/type.ts @@ -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 diff --git a/packages/store/test/storage.spec.ts b/packages/store/test/storage.spec.ts index d1bf1b48e..14ba578de 100644 --- a/packages/store/test/storage.spec.ts +++ b/packages/store/test/storage.spec.ts @@ -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(); + }); }); }); }); diff --git a/packages/tools/helpers/src/generatePackageMetadata.ts b/packages/tools/helpers/src/generatePackageMetadata.ts index a207a327a..b27d03b7b 100644 --- a/packages/tools/helpers/src/generatePackageMetadata.ts +++ b/packages/tools/helpers/src/generatePackageMetadata.ts @@ -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`]: { diff --git a/packages/verdaccio/test/unit/whoami.spec.js b/packages/verdaccio/test/unit/whoami.spec.ts similarity index 100% rename from packages/verdaccio/test/unit/whoami.spec.js rename to packages/verdaccio/test/unit/whoami.spec.ts diff --git a/packages/web/src/api/package.ts b/packages/web/src/api/package.ts index 9db5d8a41..c009db977 100644 --- a/packages/web/src/api/package.ts +++ b/packages/web/src/api/package.ts @@ -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}' ); diff --git a/packages/web/src/api/readme.ts b/packages/web/src/api/readme.ts index d1628377d..f0fdd5e15 100644 --- a/packages/web/src/api/readme.ts +++ b/packages/web/src/api/readme.ts @@ -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 {