From d086073069e14320b7fa92eba4533a49a3af909d Mon Sep 17 00:00:00 2001 From: "Juan Picado @jotadeveloper" Date: Mon, 19 Feb 2018 19:29:14 +0100 Subject: [PATCH] fix(flow): fix flow definitions --- .eslintrc | 1 + .flowconfig | 13 +- flow-typed/npm/node-mocks-http_vx.x.x.js | 109 +++++++++++++ jest.config.js | 22 +-- jest.e2e.config.js | 14 +- package.json | 3 +- src/lib/config.js | 10 +- src/lib/local-storage.js | 35 ++-- src/lib/storage-utils.js | 13 +- src/lib/storage.js | 198 ++++++++++++----------- src/lib/up-storage.js | 90 ++++++----- src/lib/utils.js | 16 +- test/unit/local-storage.spec.js | 2 +- test/unit/partials/config.js | 8 +- test/unit/store.spec.js | 79 +++++++++ test/unit/up-storage.spec.js | 11 +- tools/webpack.dev.config.babel.js | 4 +- tools/webpack.prod.config.babel.js | 4 +- yarn.lock | Bin 324106 -> 327087 bytes 19 files changed, 428 insertions(+), 204 deletions(-) create mode 100644 flow-typed/npm/node-mocks-http_vx.x.x.js create mode 100644 test/unit/store.spec.js diff --git a/.eslintrc b/.eslintrc index 39a03d571..ebc189017 100644 --- a/.eslintrc +++ b/.eslintrc @@ -75,6 +75,7 @@ "no-invalid-this": 2, "new-cap": 2, "one-var": 2, + "quote-props":["error", "as-needed"], "no-console": [ 2, { diff --git a/.flowconfig b/.flowconfig index f4614a4af..ae1cda594 100644 --- a/.flowconfig +++ b/.flowconfig @@ -7,13 +7,18 @@ .*/coverage/.* .*/.vscode/.* .*/build/.* - -[include] +.*/docs/.* +.*/scripts/.* +.*/assets/.* +.*/bin/.* +.*/systemd/.* +.*/website/.* +.*/wiki/.* +.*/docs/.* +.*/tools/.* [libs] node_modules/@verdaccio/types/lib/ -[lints] - [options] suppress_comment= \\(.\\|\n\\)*\\$FlowFixMe diff --git a/flow-typed/npm/node-mocks-http_vx.x.x.js b/flow-typed/npm/node-mocks-http_vx.x.x.js new file mode 100644 index 000000000..04b2e428b --- /dev/null +++ b/flow-typed/npm/node-mocks-http_vx.x.x.js @@ -0,0 +1,109 @@ +// flow-typed signature: 0c37b93b28df38b46c7edb9bc9d278ad +// flow-typed version: <>/node-mocks-http_v1.6.7/flow_v0.64.0 + +/** + * This is an autogenerated libdef stub for: + * + * 'node-mocks-http' + * + * Fill this stub out by replacing all the `any` types. + * + * Once filled out, we encourage you to share your work with the + * community by sending a pull request to: + * https://github.com/flowtype/flow-typed + */ + +declare module 'node-mocks-http' { + declare module.exports: any; +} + +/** + * We include stubs for each file inside this npm package in case you need to + * require those files directly. Feel free to delete any files that aren't + * needed. + */ +declare module 'node-mocks-http/lib/express/mock-application' { + declare module.exports: any; +} + +declare module 'node-mocks-http/lib/express/mock-express' { + declare module.exports: any; +} + +declare module 'node-mocks-http/lib/express/mock-request' { + declare module.exports: any; +} + +declare module 'node-mocks-http/lib/express/utils/define-getter' { + declare module.exports: any; +} + +declare module 'node-mocks-http/lib/http-mock' { + declare module.exports: any; +} + +declare module 'node-mocks-http/lib/mockEventEmitter' { + declare module.exports: any; +} + +declare module 'node-mocks-http/lib/mockRequest' { + declare module.exports: any; +} + +declare module 'node-mocks-http/lib/mockResponse' { + declare module.exports: any; +} + +declare module 'node-mocks-http/lib/mockWritableStream' { + declare module.exports: any; +} + +declare module 'node-mocks-http/lib/node/_http_incoming' { + declare module.exports: any; +} + +declare module 'node-mocks-http/lib/node/_http_server' { + declare module.exports: any; +} + +declare module 'node-mocks-http/lib/node/http' { + declare module.exports: any; +} + +// Filename aliases +declare module 'node-mocks-http/lib/express/mock-application.js' { + declare module.exports: $Exports<'node-mocks-http/lib/express/mock-application'>; +} +declare module 'node-mocks-http/lib/express/mock-express.js' { + declare module.exports: $Exports<'node-mocks-http/lib/express/mock-express'>; +} +declare module 'node-mocks-http/lib/express/mock-request.js' { + declare module.exports: $Exports<'node-mocks-http/lib/express/mock-request'>; +} +declare module 'node-mocks-http/lib/express/utils/define-getter.js' { + declare module.exports: $Exports<'node-mocks-http/lib/express/utils/define-getter'>; +} +declare module 'node-mocks-http/lib/http-mock.js' { + declare module.exports: $Exports<'node-mocks-http/lib/http-mock'>; +} +declare module 'node-mocks-http/lib/mockEventEmitter.js' { + declare module.exports: $Exports<'node-mocks-http/lib/mockEventEmitter'>; +} +declare module 'node-mocks-http/lib/mockRequest.js' { + declare module.exports: $Exports<'node-mocks-http/lib/mockRequest'>; +} +declare module 'node-mocks-http/lib/mockResponse.js' { + declare module.exports: $Exports<'node-mocks-http/lib/mockResponse'>; +} +declare module 'node-mocks-http/lib/mockWritableStream.js' { + declare module.exports: $Exports<'node-mocks-http/lib/mockWritableStream'>; +} +declare module 'node-mocks-http/lib/node/_http_incoming.js' { + declare module.exports: $Exports<'node-mocks-http/lib/node/_http_incoming'>; +} +declare module 'node-mocks-http/lib/node/_http_server.js' { + declare module.exports: $Exports<'node-mocks-http/lib/node/_http_server'>; +} +declare module 'node-mocks-http/lib/node/http.js' { + declare module.exports: $Exports<'node-mocks-http/lib/node/http'>; +} diff --git a/jest.config.js b/jest.config.js index 6405c2ddd..766821435 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,31 +1,31 @@ /* eslint comma-dangle: 0 */ module.exports = { - 'name': 'verdaccio-jest', - 'verbose': true, - 'collectCoverage': true, - 'coveragePathIgnorePatterns': [ + name: 'verdaccio-jest', + verbose: true, + collectCoverage: true, + coveragePathIgnorePatterns: [ 'node_modules', 'fixtures' ], - 'testEnvironment': 'jest-environment-jsdom-global', - 'testRegex': '(/test/unit.*\\.spec|test/functional.*\\.func|/test/webui/.*\\.spec)\\.js', + testEnvironment: 'jest-environment-jsdom-global', + testRegex: '(/test/unit.*\\.spec|test/functional.*\\.func|/test/webui/.*\\.spec)\\.js', // 'testRegex': '(test/functional.*\\.func)\\.js' - 'setupFiles': [ + setupFiles: [ './test/webui/global.js' ], - 'modulePathIgnorePatterns': [ + modulePathIgnorePatterns: [ 'global.js' ], - 'testPathIgnorePatterns': [ + testPathIgnorePatterns: [ '__snapshots__' ], - 'moduleNameMapper': { + moduleNameMapper: { '\\.(scss)$': '/node_modules/identity-obj-proxy', 'github-markdown-css': '/node_modules/identity-obj-proxy', '\\.(png)$': '/node_modules/identity-obj-proxy' }, - 'transformIgnorePatterns': [ + transformIgnorePatterns: [ '/node_modules/(?!react-syntax-highlighter)' ] }; diff --git a/jest.e2e.config.js b/jest.e2e.config.js index db9d50f31..4a8d79f2b 100644 --- a/jest.e2e.config.js +++ b/jest.e2e.config.js @@ -1,11 +1,11 @@ /* eslint comma-dangle: 0 */ module.exports = { - 'name': 'verdaccio-e2e-jest', - 'verbose': true, - 'collectCoverage': false, - 'globalSetup': './test/e2e/pre-setup.js', - 'globalTeardown': './test/e2e/teardown.js', - 'testEnvironment': './test/e2e/puppeteer_environment.js', - 'testRegex': '(/test/e2e/e2e.*\\.spec)\\.js' + name: 'verdaccio-e2e-jest', + verbose: true, + collectCoverage: false, + globalSetup: './test/e2e/pre-setup.js', + globalTeardown: './test/e2e/teardown.js', + testEnvironment: './test/e2e/puppeteer_environment.js', + testRegex: '(/test/e2e/e2e.*\\.spec)\\.js' }; diff --git a/package.json b/package.json index 3e5e17744..b948aa146 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "@commitlint/cli": "6.1.0", "@commitlint/config-conventional": "6.1.0", "@commitlint/travis-cli": "6.1.0", - "@verdaccio/types": "0.3.1", + "@verdaccio/types": "1.0.0", "axios": "0.17.1", "babel-cli": "6.26.0", "babel-core": "6.26.0", @@ -105,6 +105,7 @@ "jest-environment-jsdom-global": "1.0.3", "jest-environment-node": "22.2.0", "localstorage-memory": "1.0.2", + "node-mocks-http": "1.6.7", "node-sass": "4.7.2", "normalize.css": "7.0.0", "ora": "1.4.0", diff --git a/src/lib/config.js b/src/lib/config.js index bd23f880c..0120e5737 100644 --- a/src/lib/config.js +++ b/src/lib/config.js @@ -57,11 +57,11 @@ class Config { assert(self.storage, 'CONFIG: storage path not defined'); const users = { - 'all': true, - 'anonymous': true, - 'undefined': true, - 'owner': true, - 'none': true, + all: true, + anonymous: true, + undefined: true, + owner: true, + none: true, }; const check_user_or_uplink = function(arg) { diff --git a/src/lib/local-storage.js b/src/lib/local-storage.js index 5860c411e..f526c407f 100644 --- a/src/lib/local-storage.js +++ b/src/lib/local-storage.js @@ -6,7 +6,6 @@ import Crypto from 'crypto'; import assert from 'assert'; import fs from 'fs'; import Path from 'path'; -import Stream from 'stream'; import UrlNode from 'url'; import _ from 'lodash'; // $FlowFixMe @@ -172,7 +171,7 @@ class LocalStorage implements IStorage { sha: version.dist.shasum, }; /* eslint spaced-comment: 0 */ - //$FlowFixMe + // $FlowFixMe const upLink: string = version[Symbol.for('__verdaccio_uplink')]; if (_.isNil(upLink) === false) { @@ -232,7 +231,9 @@ class LocalStorage implements IStorage { * @param {*} tag * @param {*} callback */ - addVersion(name: string, version: string, metadata: Version, + addVersion(name: string, + version: string, + metadata: Version, tag: string, callback: Callback) { this._updatePackage(name, (data, cb) => { @@ -533,7 +534,7 @@ class LocalStorage implements IStorage { * @private * @return {ReadTarball} */ - _streamSuccessReadTarBall(storage: any, filename: string) { + _streamSuccessReadTarBall(storage: any, filename: string): IReadTarball { const stream: IReadTarball = new ReadTarball(); const readTarballStream = storage.readTarball(filename); const e404 = Utils.ErrorCode.get404; @@ -588,7 +589,7 @@ class LocalStorage implements IStorage { * @return {Function} */ search(startKey: string, options: any) { - const stream = new Stream.PassThrough({objectMode: true}); + const stream = new UploadTarball({objectMode: true}); this._eachPackage((item, cb) => { fs.stat(item.path, (err, stats) => { @@ -609,21 +610,21 @@ class LocalStorage implements IStorage { if (data.versions[latest]) { const version: Version = data.versions[latest]; const pkg: any = { - 'name': version.name, - 'description': version.description, + name: version.name, + description: version.description, 'dist-tags': {latest}, - 'maintainers': version.maintainers || [version.author].filter(Boolean), - 'author': version.author, - 'repository': version.repository, - 'readmeFilename': version.readmeFilename || '', - 'homepage': version.homepage, - 'keywords': version.keywords, - 'bugs': version.bugs, - 'license': version.license, - 'time': { + maintainers: version.maintainers || [version.author].filter(Boolean), + author: version.author, + repository: version.repository, + readmeFilename: version.readmeFilename || '', + homepage: version.homepage, + keywords: version.keywords, + bugs: version.bugs, + license: version.license, + time: { modified: item.time ? new Date(item.time).toISOString() : stats.mtime, }, - 'versions': {[latest]: 'latest'}, + versions: {[latest]: 'latest'}, }; stream.push(pkg); diff --git a/src/lib/storage-utils.js b/src/lib/storage-utils.js index 57ee3f7c7..a231848ac 100644 --- a/src/lib/storage-utils.js +++ b/src/lib/storage-utils.js @@ -17,13 +17,14 @@ const DEFAULT_REVISION: string = `0-0000000000000000`; const generatePackageTemplate = function(name: string): Package { return { // standard things - 'name': name, - 'versions': {}, + name, + versions: {}, 'dist-tags': {}, - 'time': {}, - '_distfiles': {}, - '_attachments': {}, - '_uplinks': {}, + time: {}, + _distfiles: {}, + _attachments: {}, + _uplinks: {}, + _rev: '', }; }; diff --git a/src/lib/storage.js b/src/lib/storage.js index f6de14adc..6a9c5213e 100644 --- a/src/lib/storage.js +++ b/src/lib/storage.js @@ -17,6 +17,7 @@ import type { IStorage, IProxy, IStorageHandler, + Versions, ProxyList, Package, Config, @@ -27,17 +28,22 @@ import type { Logger, } from '@verdaccio/types'; -import type {IReadTarball} from '@verdaccio/streams'; +import type {IReadTarball, IUploadTarball} from '@verdaccio/streams'; const LoggerApi = require('../lib/logger'); const WHITELIST = ['_rev', 'name', 'versions', 'dist-tags', 'readme', 'time']; -const getDefaultMetadata = (name) => { - return { - 'name': name, - 'versions': {}, +const getDefaultMetadata = function(name): Package { + const pkgMetadata: Package = { + name, + versions: {}, 'dist-tags': {}, - '_uplinks': {}, + _uplinks: {}, + _distfiles: {}, + _attachments: {}, + _rev: '', }; + + return pkgMetadata; }; /** @@ -63,9 +69,9 @@ class Storage implements IStorageHandler { /** * Add a {name} package to a system - Function checks if package with the same name is available from uplinks. - If it isn't, we create package locally - Used storages: local (write) && uplinks + Function checks if package with the same name is available from uplinks. + If it isn't, we create package locally + Used storages: local (write) && uplinks * @param {*} name * @param {*} metadata * @param {*} callback @@ -97,6 +103,7 @@ class Storage implements IStorageHandler { */ const checkPackageRemote = () => { return new Promise((resolve, reject) => { + // $FlowFixMe self._syncUplinksMetadata(name, null, {}, (err, results, err_results) => { // something weird if (err && err.status !== 404) { @@ -111,7 +118,7 @@ class Storage implements IStorageHandler { // if uplink fails with a status other than 404, we report failure if (_.isNil(err_results[i][0]) === false) { if (err_results[i][0].status !== 404) { - if (_.isNil(this.config.publish) === false && + if (this.config.publish && _.isBoolean(this.config.publish.allow_offline) && this.config.publish.allow_offline) { return resolve(); @@ -159,20 +166,20 @@ class Storage implements IStorageHandler { /** * Add a new version of package {name} to a system - Used storages: local (write) + Used storages: local (write) * @param {*} name * @param {*} version * @param {*} metadata * @param {*} tag * @param {*} callback */ - addVersion(name: string, version: Version, metadata: Package, tag: string, callback: Callback) { + addVersion(name: string, version: string, metadata: Version, tag: string, callback: Callback) { this.localStorage.addVersion(name, version, metadata, tag, callback); } /** * Tags a package version with a provided tag - Used storages: local (write) + Used storages: local (write) * @param {*} name * @param {*} tag_hash * @param {*} callback @@ -183,7 +190,7 @@ class Storage implements IStorageHandler { /** * Tags a package version with a provided tag - Used storages: local (write) + Used storages: local (write) * @param {*} name * @param {*} tag_hash * @param {*} callback @@ -195,8 +202,8 @@ class Storage implements IStorageHandler { /** * Change an existing package (i.e. unpublish one version) - Function changes a package info from local storage and all uplinks with write access./ - Used storages: local (write) + Function changes a package info from local storage and all uplinks with write access./ + Used storages: local (write) * @param {*} name * @param {*} metadata * @param {*} revision @@ -208,8 +215,8 @@ class Storage implements IStorageHandler { /** * Remove a package from a system - Function removes a package from local storage - Used storages: local (write) + Function removes a package from local storage + Used storages: local (write) * @param {*} name * @param {*} callback */ @@ -220,11 +227,11 @@ class Storage implements IStorageHandler { } /** - Remove a tarball from a system - Function removes a tarball from local storage. - Tarball in question should not be linked to in any existing - versions, i.e. package version should be unpublished first. - Used storage: local (write) + Remove a tarball from a system + Function removes a tarball from local storage. + Tarball in question should not be linked to in any existing + versions, i.e. package version should be unpublished first. + Used storage: local (write) * @param {*} name * @param {*} filename * @param {*} revision @@ -236,22 +243,22 @@ class Storage implements IStorageHandler { /** * Upload a tarball for {name} package - Function is syncronous and returns a WritableStream - Used storages: local (write) + Function is syncronous and returns a WritableStream + Used storages: local (write) * @param {*} name * @param {*} filename * @return {Stream} */ - add_tarball(name: string, filename: string) { + add_tarball(name: string, filename: string): IUploadTarball { return this.localStorage.addTarball(name, filename); } /** - Get a tarball from a storage for {name} package - Function is syncronous and returns a ReadableStream - Function tries to read tarball locally, if it fails then it reads package - information in order to figure out where we can get this tarball from - Used storages: local || uplink (just one) + Get a tarball from a storage for {name} package + Function is syncronous and returns a ReadableStream + Function tries to read tarball locally, if it fails then it reads package + information in order to figure out where we can get this tarball from + Used storages: local || uplink (just one) * @param {*} name * @param {*} filename * @return {Stream} @@ -266,7 +273,8 @@ class Storage implements IStorageHandler { // information about it, so fetching package info is unnecessary // trying local first - let localStream: IReadTarball = self.localStorage.getTarball(name, filename); + // flow: should be IReadTarball + let localStream: any = self.localStorage.getTarball(name, filename); let is_open = false; localStream.on('error', (err) => { if (is_open || err.status !== 404) { @@ -276,7 +284,8 @@ class Storage implements IStorageHandler { // local reported 404 let err404 = err; localStream.abort(); - localStream = null; // gc + // $FlowFixMe + localStream = null; // we force for garbage collector self.localStorage.getPackageMetadata(name, (err, info: Package) => { if (_.isNil(err) && info._distfiles && _.isNil(info._distfiles[filename]) === false) { // information about this file exists locally @@ -285,7 +294,7 @@ class Storage implements IStorageHandler { // we know nothing about this file, trying to get information elsewhere self._syncUplinksMetadata(name, info, {}, (err, info: Package) => { if (_.isNil(err) === false) { - return readStream.emit('error', err); + return readStream.emit('error', err); } if (_.isNil(info._distfiles) || _.isNil(info._distfiles[filename])) { return readStream.emit('error', err404); @@ -367,7 +376,7 @@ class Storage implements IStorageHandler { savestream.on('error', function(err) { self.logger.warn( {err: err} - , 'error saving file: @{err.message}\n@{err.stack}' ); + , 'error saving file: @{err.message}\n@{err.stack}' ); if (savestream) { savestream.abort(); } @@ -381,11 +390,11 @@ class Storage implements IStorageHandler { } /** - Retrieve a package metadata for {name} package - Function invokes localStorage.getPackage and uplink.get_package for every - uplink with proxy_access rights against {name} and combines results - into one json object - Used storages: local && uplink (proxy_access) + Retrieve a package metadata for {name} package + Function invokes localStorage.getPackage and uplink.get_package for every + uplink with proxy_access rights against {name} and combines results + into one json object + Used storages: local && uplink (proxy_access) * @param {object} options * @property {string} options.name Package Name @@ -423,18 +432,18 @@ class Storage implements IStorageHandler { result._attachments = {}; options.callback(null, result, uplink_errors); - }); + }); }); } /** - Retrieve remote and local packages more recent than {startkey} - Function streams all packages from all uplinks first, and then - local packages. - Note that local packages could override registry ones just because - they appear in JSON last. That's a trade-off we make to avoid - memory issues. - Used storages: local && uplink (proxy_access) + Retrieve remote and local packages more recent than {startkey} + Function streams all packages from all uplinks first, and then + local packages. + Note that local packages could override registry ones just because + they appear in JSON last. That's a trade-off we make to avoid + memory issues. + Used storages: local && uplink (proxy_access) * @param {*} startkey * @param {*} options * @return {Stream} @@ -445,42 +454,42 @@ class Storage implements IStorageHandler { let stream: any = new Stream.PassThrough({objectMode: true}); async.eachSeries(Object.keys(this.uplinks), function(up_name, cb) { - // shortcut: if `local=1` is supplied, don't call uplinks - if (options.req.query.local !== undefined) { - return cb(); - } - // search by keyword for each uplink - let lstream: IUploadTarball = self.uplinks[up_name].search(options); - // join streams - lstream.pipe(stream, {end: false}); - lstream.on('error', function(err) { - self.logger.error({err: err}, 'uplink error: @{err.message}'); - cb(), cb = function() {}; - }); - lstream.on('end', function() { - cb(), cb = function() {}; - }); - - stream.abort = function() { - if (lstream.abort) { - lstream.abort(); + // shortcut: if `local=1` is supplied, don't call uplinks + if (options.req.query.local !== undefined) { + return cb(); } - cb(), cb = function() {}; - }; - }, - // executed after all series - function() { - // attach a local search results - let lstream: IReadTarball = self.localStorage.search(startkey, options); - stream.abort = function() { - lstream.abort(); - }; - lstream.pipe(stream, {end: true}); - lstream.on('error', function(err) { - self.logger.error({err: err}, 'search error: @{err.message}'); - stream.end(); + // search by keyword for each uplink + let lstream: IUploadTarball = self.uplinks[up_name].search(options); + // join streams + lstream.pipe(stream, {end: false}); + lstream.on('error', function(err) { + self.logger.error({err: err}, 'uplink error: @{err.message}'); + cb(), cb = function() {}; + }); + lstream.on('end', function() { + cb(), cb = function() {}; + }); + + stream.abort = function() { + if (lstream.abort) { + lstream.abort(); + } + cb(), cb = function() {}; + }; + }, + // executed after all series + function() { + // attach a local search results + let lstream: IReadTarball = self.localStorage.search(startkey, options); + stream.abort = function() { + lstream.abort(); + }; + lstream.pipe(stream, {end: true}); + lstream.on('error', function(err) { + self.logger.error({err: err}, 'search error: @{err.message}'); + stream.end(); + }); }); - }); return stream; } @@ -523,25 +532,18 @@ class Storage implements IStorageHandler { /** * Function fetches package metadata from uplinks and synchronizes it with local data - if package is available locally, it MUST be provided in pkginfo - returns callback(err, result, uplink_errors) - * @param {*} name - * @param {*} packageInfo - * @param {*} options - * @param {*} callback + if package is available locally, it MUST be provided in pkginfo + returns callback(err, result, uplink_errors) */ - _syncUplinksMetadata(name: string, packageInfo: Package, options: any, callback: Callback) { - let exists = false; + _syncUplinksMetadata(name: string, packageInfo: Package, options: any, callback: Callback): void { + let exists = true; const self = this; const upLinks = []; - if (_.isNil(packageInfo)) { + if (!packageInfo || packageInfo === null) { exists = false; packageInfo = getDefaultMetadata(name); - } else { - exists = true; } - for (let up in this.uplinks) { if (this.config.hasProxyTo(name, up)) { upLinks.push(this.uplinks[up]); @@ -591,7 +593,7 @@ class Storage implements IStorageHandler { // added to fix verdaccio#73 if ('time' in upLinkResponse) { - packageInfo['time'] = upLinkResponse.time; + packageInfo.time = upLinkResponse.time; } this._updateVersionsHiddenUpLink(upLinkResponse.versions, upLink); @@ -616,8 +618,8 @@ class Storage implements IStorageHandler { assert(!err && Array.isArray(upLinksErrors)); if (!exists) { return callback( Utils.ErrorCode.get404('no such package available') - , null - , upLinksErrors ); + , null + , upLinksErrors ); } self.localStorage.updateVersions(name, packageInfo, function(err, packageJsonLocal: Package) { @@ -666,7 +668,7 @@ class Storage implements IStorageHandler { /** * Function gets a local info and an info from uplinks and tries to merge it - exported for unit tests only. + exported for unit tests only. * @param {*} local * @param {*} up * @param {*} config diff --git a/src/lib/up-storage.js b/src/lib/up-storage.js index 2a66a6529..0b9ca89e1 100644 --- a/src/lib/up-storage.js +++ b/src/lib/up-storage.js @@ -13,11 +13,13 @@ import {ReadTarball} from '@verdaccio/streams'; import type { IProxy, Config, + UpLinkConf, Callback, + Headers, Logger, } from '@verdaccio/types'; -import type {IUploadTarball} from '@verdaccio/streams'; +// import type {IUploadTarball, IReadTarball} from '@verdaccio/streams'; const LoggerApi = require('./logger'); const encode = function(thing) { @@ -42,7 +44,7 @@ const setConfig = (config, key, def) => { * (same for storage.js, local-storage.js, up-storage.js) */ class ProxyStorage implements IProxy { - config: Config; + config: UpLinkConf; failed_requests: number; userAgent: string; ca: string | void; @@ -76,11 +78,11 @@ class ProxyStorage implements IProxy { this.config.url = this.config.url.replace(/\/$/, ''); - if (Number(this.config.timeout) >= 1000) { + if (this.config.timeout && Number(this.config.timeout) >= 1000) { this.logger.warn(['Too big timeout value: ' + this.config.timeout, - 'We changed time format to nginx-like one', - '(see http://nginx.org/en/docs/syntax.html)', - 'so please update your config accordingly'].join('\n')); + 'We changed time format to nginx-like one', + '(see http://nginx.org/en/docs/syntax.html)', + 'so please update your config accordingly'].join('\n')); } // a bunch of different configurable timers @@ -96,14 +98,14 @@ class ProxyStorage implements IProxy { * @param {*} cb * @return {Request} */ - request(options: any, cb: Callback) { + request(options: any, cb?: Callback) { let json; if (this._statusCheck() === false) { let streamRead = new Stream.Readable(); process.nextTick(function() { - if (_.isFunction(cb)) { + if (cb) { cb(ErrorCode.get500('uplink is offline')); } // $FlowFixMe @@ -142,6 +144,7 @@ class ProxyStorage implements IProxy { // $FlowFixMe processBody(err, body); logActivity(); + // $FlowFixMe cb(err, res, body); /** @@ -176,8 +179,8 @@ class ProxyStorage implements IProxy { function logActivity() { let message = '@{!status}, req: \'@{request.method} @{request.url}\''; message += error - ? ', error: @{!error}' - : ', bytes: @{bytes.in}/@{bytes.out}'; + ? ', error: @{!error}' + : ', bytes: @{bytes.in}/@{bytes.out}'; self.logger.warn({ err: err, request: {method: method, url: uri}, @@ -261,8 +264,9 @@ class ProxyStorage implements IProxy { * @private */ _setAuth(headers: any) { + const auth = this.config.auth; - if (_.isNil(this.config.auth) || headers['authorization']) { + if (typeof auth === 'undefined' || headers['authorization']) { return headers; } @@ -273,10 +277,12 @@ class ProxyStorage implements IProxy { // get NPM_TOKEN http://blog.npmjs.org/post/118393368555/deploying-with-npm-private-modules // or get other variable export in env let token: any = process.env.NPM_TOKEN; - if (this.config.auth.token) { - token = this.config.auth.token; - } else if (this.config.auth.token_env) { - token = process.env[this.config.auth.token_env]; + + if (auth.token) { + token = auth.token; + } else if (auth.token_env ) { + // $FlowFixMe + token = process.env[auth.token_env]; } if (_.isNil(token)) { @@ -284,8 +290,9 @@ class ProxyStorage implements IProxy { } // define type Auth allow basic and bearer - const type = this.config.auth.type; + const type = auth.type; this._setHeaderAuthorization(headers, type, token); + return headers; } @@ -321,25 +328,28 @@ class ProxyStorage implements IProxy { * Eg: * * uplinks: - npmjs: - url: https://registry.npmjs.org/ - headers: - Accept: "application/vnd.npm.install-v2+json; q=1.0" - verdaccio-staging: - url: https://mycompany.com/npm - headers: - Accept: "application/json" - authorization: "Basic YourBase64EncodedCredentials==" + npmjs: + url: https://registry.npmjs.org/ + headers: + Accept: "application/vnd.npm.install-v2+json; q=1.0" + verdaccio-staging: + url: https://mycompany.com/npm + headers: + Accept: "application/json" + authorization: "Basic YourBase64EncodedCredentials==" * @param {Object} headers * @private */ - _overrideWithUplinkConfigHeaders(headers: any) { + _overrideWithUplinkConfigHeaders(headers: Headers) { + if (!this.config.headers) { + return headers; + } + // add/override headers specified in the config + /* eslint guard-for-in: 0 */ for (let key in this.config.headers) { - if (Object.prototype.hasOwnProperty.call(this.config.headers, key)) { headers[key] = this.config.headers[key]; - } } } @@ -349,10 +359,10 @@ class ProxyStorage implements IProxy { * @return {Boolean} */ isUplinkValid(url: string) { - // $FlowFixMe - url = URL.parse(url); - // $FlowFixMe - return url.protocol === this.url.protocol && url.host === this.url.host && url.path.indexOf(this.url.path) === 0; + // $FlowFixMe + url = URL.parse(url); + // $FlowFixMe + return url.protocol === this.url.protocol && url.host === this.url.host && url.path.indexOf(this.url.path) === 0; } /** @@ -447,8 +457,8 @@ class ProxyStorage implements IProxy { * @return {Stream} */ search(options: any) { - const transformStream: IUploadTarball = new Stream.PassThrough({objectMode: true}); - const requestStream: IUploadTarball = this.request({ + const transformStream: any = new Stream.PassThrough({objectMode: true}); + const requestStream: stream$Readable = this.request({ uri: options.req.url, req: options.req, headers: { @@ -486,6 +496,8 @@ class ProxyStorage implements IProxy { }); transformStream.abort = () => { + // FIXME: this is clearly a potential issue + // $FlowFixMe requestStream.abort(); transformStream.emit('end'); }; @@ -509,8 +521,8 @@ class ProxyStorage implements IProxy { if (this.proxy === false) { headers['X-Forwarded-For'] = ( req && req.headers['x-forwarded-for'] - ? req.headers['x-forwarded-for'] + ', ' - : '' + ? req.headers['x-forwarded-for'] + ', ' + : '' ) + req.connection.remoteAddress; } } @@ -518,8 +530,8 @@ class ProxyStorage implements IProxy { // always attach Via header to avoid loops, even if we're not proxying headers['Via'] = req && req.headers['via'] - ? req.headers['via'] + ', ' - : ''; + ? req.headers['via'] + ', ' + : ''; headers['Via'] += '1.1 ' + this.server_id + ' (Verdaccio)'; } @@ -604,7 +616,7 @@ class ProxyStorage implements IProxy { if (this.proxy) { this.logger.debug({url: this.url.href, rule: noProxyItem}, 'not using proxy for @{url}, excluded by @{rule} rule'); - // $FlowFixMe + // $FlowFixMe this.proxy = false; } break; diff --git a/src/lib/utils.js b/src/lib/utils.js index 6f5743297..e504aaa95 100644 --- a/src/lib/utils.js +++ b/src/lib/utils.js @@ -278,14 +278,14 @@ function normalize_dist_tags(data) { const parseIntervalTable = { '': 1000, - 'ms': 1, - 's': 1000, - 'm': 60*1000, - 'h': 60*60*1000, - 'd': 86400000, - 'w': 7*86400000, - 'M': 30*86400000, - 'y': 365*86400000, + ms: 1, + s: 1000, + m: 60*1000, + h: 60*60*1000, + d: 86400000, + w: 7*86400000, + M: 30*86400000, + y: 365*86400000, }; /** diff --git a/test/unit/local-storage.spec.js b/test/unit/local-storage.spec.js index 0e62a3440..7eec2a183 100644 --- a/test/unit/local-storage.spec.js +++ b/test/unit/local-storage.spec.js @@ -10,7 +10,7 @@ import {readFile} from '../functional/lib/test.utils'; const readMetadata = (fileName: string = 'metadata') => readFile(`../../unit/partials/${fileName}`); -import type {IStorage} from '@verdaccio/types'; +import type {IStorage, Config} from '@verdaccio/types'; setup([]); diff --git a/test/unit/partials/config.js b/test/unit/partials/config.js index 05c964ce1..722dae174 100644 --- a/test/unit/partials/config.js +++ b/test/unit/partials/config.js @@ -1,5 +1,5 @@ const config = { - storage: __dirname + '/store/test-storage', + storage: `${__dirname}/store/test-storage`, uplinks: { 'npmjs': { 'url': 'https://registry.npmjs.org/' @@ -17,6 +17,12 @@ const config = { allow_publish: 'nobody' }, + 'react': { + allow_access: '$all', + allow_publish: '$all', + proxy: 'npmjs' + }, + 'jquery': { allow_access: '$all', allow_publish: '$all', diff --git a/test/unit/store.spec.js b/test/unit/store.spec.js new file mode 100644 index 000000000..f970c1de8 --- /dev/null +++ b/test/unit/store.spec.js @@ -0,0 +1,79 @@ +// @flow + +import _ from 'lodash'; +import httpMocks from 'node-mocks-http'; +// $FlowFixMe +import configExample from './partials/config'; +import AppConfig from '../../src/lib/config'; +import Storage from '../../src/lib/storage'; +import {setup} from '../../src/lib/logger'; + +import type {IStorageHandler, Config} from '@verdaccio/types'; + +setup(configExample.logs); + +const generateStorage = function(): IStorageHandler { + const storageConfig = _.clone(configExample); + const storage = `./unit/partials/store/test-storage-store.spec`; + storageConfig.self_path = __dirname; + storageConfig.storage = storage; + const config: Config = new AppConfig(storageConfig); + + return new Storage(config); +} + +describe('StorageTest', () => { + + jest.setTimeout(1000000); + + beforeAll((done)=> { + const storage: IStorageHandler = generateStorage(); + var request = httpMocks.createRequest({ + method: 'GET', + url: '/react', + params: {} + }); + + storage.getPackage({ + name: 'react', + req: request, + callback: () => { + const stream = storage.get_tarball('react', 'react-16.1.0.tgz'); + stream.on('content-length', function(content) { + if (content) { + expect(content).toBeTruthy(); + done(); + } + }); + }, + }); + }); + + test('should be defined', () => { + const storage: IStorageHandler = generateStorage(); + + expect(storage).toBeDefined(); + }); + + test('should fetch from uplink react metadata from nmpjs', (done) => { + const storage: IStorageHandler = generateStorage(); + + // $FlowFixMe + storage._syncUplinksMetadata('react', null, {}, (err, metadata, errors) => { + expect(metadata).toBeInstanceOf(Object); + done(); + }); + }); + + test('should fails on fetch from uplink metadata from nmpjs', (done) => { + const storage: IStorageHandler = generateStorage(); + + // $FlowFixMe + storage._syncUplinksMetadata('@verdaccio/404', null, {}, (err, metadata, errors) => { + expect(errors).toBeInstanceOf(Array); + expect(errors[0][0].statusCode).toBe(404); + expect(errors[0][0].message).toMatch(/package doesn't exist on uplink/); + done(); + }); + }); +}); diff --git a/test/unit/up-storage.spec.js b/test/unit/up-storage.spec.js index 7ee13b400..f3b3849de 100644 --- a/test/unit/up-storage.spec.js +++ b/test/unit/up-storage.spec.js @@ -6,13 +6,20 @@ import _ from 'lodash'; import configExample from './partials/config'; import {setup} from '../../src/lib/logger'; +import type {UpLinkConf, Config} from '@verdaccio/types'; + setup([]); describe('UpStorge', () => { - const uplinkDefault = { - url: 'https://registry.npmjs.org/' + const uplinkDefault: UpLinkConf = { + url: 'https://registry.npmjs.org/', + fail_timeout: '5m', + max_fails: 2, + maxage: '2m', + timeout: '1m', }; + let generateProxy = (config: UpLinkConf = uplinkDefault) => { const appConfig: Config = new AppConfig(configExample); diff --git a/tools/webpack.dev.config.babel.js b/tools/webpack.dev.config.babel.js index eaf871257..ef063a0e9 100644 --- a/tools/webpack.dev.config.babel.js +++ b/tools/webpack.dev.config.babel.js @@ -24,9 +24,9 @@ export default { plugins: [ new webpack.DefinePlugin({ - '__DEBUG__': true, + __DEBUG__: true, 'process.env.NODE_ENV': '"development"', - '__APP_VERSION__': `"${getPackageVersion()}"`, + __APP_VERSION__: `"${getPackageVersion()}"`, }), new HTMLWebpackPlugin({ title: 'Verdaccio', diff --git a/tools/webpack.prod.config.babel.js b/tools/webpack.prod.config.babel.js index 8d84a951c..9107562f9 100644 --- a/tools/webpack.prod.config.babel.js +++ b/tools/webpack.prod.config.babel.js @@ -18,9 +18,9 @@ const prodConf = { plugins: [ new webpack.DefinePlugin({ - '__DEBUG__': false, + __DEBUG__: false, 'process.env.NODE_ENV': '"production"', - '__APP_VERSION__': `"${getPackageVersion()}"`, + __APP_VERSION__: `"${getPackageVersion()}"`, }), new webpack.optimize.UglifyJsPlugin({ sourceMap: true, diff --git a/yarn.lock b/yarn.lock index 0325be487f06d16bab7513bfce2b1fad8e4c59bc..4df7554d985146498b15263755361340b32e62ee 100644 GIT binary patch delta 10056 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 C=D6Sh delta 7817 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�ic)2H~{$F#l}r7|RaTkpAFd9-nZmVb#qur&9bB z-$lQEr_w?PcX>Uu?Lpo|CqK=n;8b-|fyfjogozdgCJZ3cIh&)SRU$PcfYBQ3ql7gJ z)l?d+wKi&^+n(mLF(Fs(;8VyQVN-AU9!!|aj_?Jv?R$Luz@34>un&FU5tLBrz`-b! zxGNk?*jiC+Y@A?T`iBD{4ue?cJi}*~$sS4b_TtOddr*$AKf{~p_yK-Wbx&_+{T?@d zkB^(vZ^~#5_2(m)vsl29RqK3%MF2An8y46E)vA3sUr%~%bo609Yv8Rm{AGS#d#0vg z8up1!LVCNE!^DD+?16-Hn|ZOfrz2DRkNQxh!sehM&|J-1ol^VckC= zNOXj~3-B33UZ)C6K@jDBDh=d^^5Kra+q8L^lj3iV@Czr6?rQ(snsr%sZ@%FC03uBl zkNuiY7+Y+4pEvWO=L7!ekm8n#7mTFs7lKgVALC6Jcryg`p}L^hAr53%F5b%KCa@W* z0e`Y)pl3l%BrgA&H>ya+dcPk*6V_p6*gA)G72iJDJHm=;grvmONpxg}cN!D4@Kx-uvrhF!RY$)yY?#}; z@xryeNlA*g%=czgm%cT!tzmhrv+H^&}t|S0E7H z;4fp-p>7ONJj)v^`jv9YC|DJ&%1W!m!Xtqq4bf!*tPL#x*ajn*T9Upn%bUpp+BVBO zjgBG$J<#UOrYW7sWK@$pa9JW-Qj>t(M0FyAr4^(e0VXVzm`!9D1W}!xVA73XqPV`( zJ97wa+lv+SdEaZM|JL4w#wz4T7OGIeA3+t24>m^iT?+0*>7`a#iOOpPB#xhPrbXqYi%4ofZhz#2y!pY zRE}NFIOsocJ$fyIwQE9bB$2I&zm*HUdChQ7e8{DLS?GPUIDDpe&7e{1dN;1g*SJf{ zX+h@{uP*b(chVgjfl#Mx^2T)6C8CcBM3D?0X5=u(WAt$1!W=tJs?pdMAE;#Azu2-?q4~|m7f=i+x4DcMF z^b=4G$Y@9`U{Dt0i4N8ctBWrj@&0j4BO}G|7rlEqJvRgSJ$e+KR@di=36m(x!NO=v z^ewy~SlOikFMlrX8qst8xrhxHida69&RZN+Q9^8V05zxYO_lX~9uIz*X-#>NoFxQawm znjB^${vuB!34Vs(2&9CSB3)wWh1UThx4r9~O1Hh|=|;9*eyh?}y!oCtBBXUUdR?b) zuG_9DOnr{IV}UqK7P4c7)5G%C#x?1<3KQHJ7{ z90-5@>dG+MeNAOZLDyCwphk1Ai&}1|l#;n|o-BqH4n2=&qKOctvP)t0VfBW)rrOOr!3tOps;!)E|}N zv1<9)7Jp;`Pk6c!r?_QliR3bMFyw@ZP&JZZVY>F7%E$^Dql2z`l#MHP-&1L6DV}?z Ia_p@C1(eQDsQ>@~