diff --git a/.circleci/config.yml b/.circleci/config.yml index c106ea68..e6be0784 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -11,7 +11,7 @@ jobs: - restore_cache: keys: - dependencies-{{ checksum "package-lock.json" }} - - run: npm install + - run: npm ci - save_cache: key: dependencies-{{ checksum "package-lock.json" }} paths: @@ -33,7 +33,7 @@ jobs: - restore_cache: keys: - dependencies-{{ checksum "package-lock.json" }} - - run: npm install + - run: npm ci - save_cache: key: dependencies-{{ checksum "package-lock.json" }} paths: diff --git a/.npmignore b/.npmignore index 15820b14..7bf7c3c0 100644 --- a/.npmignore +++ b/.npmignore @@ -1,11 +1,6 @@ *.test.js -.config -/.eslintcache -/.eslintignore -/.nvmrc -/CODE_OF_CONDUCT.md -/CONTRIBUTING.md +/.* /docs /reports -/scripts +/templates __mocks__ diff --git a/.nvmrc b/.nvmrc index 2bf5ad04..b009dfb9 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -stable +lts/* diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..8ab34729 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,27 @@ +## Changelog 🚀 + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) +and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). + +Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). + +### 0.0.0 (2018-09-25) + +- [`#5`](https://github.com/lifion/lifion-kinesis/pull/5): Update dependency npm-watch to ^0.4.0 +- [`#2`](https://github.com/lifion/lifion-kinesis/pull/2): Configure Renovate +- [`39f098d`](https://github.com/lifion/lifion-kinesis/commit/39f098de613494f6eea0c5d055b5025451e73d49): Make the tag test cases more specific +- [`562bea1`](https://github.com/lifion/lifion-kinesis/commit/562bea1e0b9c31baacbe1c013e81b3dbde692cae): Add test coverage +- [`40f156f`](https://github.com/lifion/lifion-kinesis/commit/40f156f259460b5801c5ecf0d221fc4b5c510138): Remove the check-dependencies script in favor of Renovate +- [`4f9fad8`](https://github.com/lifion/lifion-kinesis/commit/4f9fad8343a21803185627378b5aedd7cb93598d): Add test coverage for lib/utils +- [`07fd3aa`](https://github.com/lifion/lifion-kinesis/commit/07fd3aa183fd4f593d8fb73b773e9e618adb6740): Delete renovate.json +- [`ce26fc3`](https://github.com/lifion/lifion-kinesis/commit/ce26fc3a54fbce7e507fbc5b5a047ab0fa5ae11e): Change the Renovate configuration +- [`c8d578e`](https://github.com/lifion/lifion-kinesis/commit/c8d578efbd09d2434e4d4f480167818cc3945423): Add renovate.json +- [`23ac7b7`](https://github.com/lifion/lifion-kinesis/commit/23ac7b709c44d5e9a5c6f45ce469447fe62b8daf): Correct the package-lock.json file +- [`5823729`](https://github.com/lifion/lifion-kinesis/commit/582372932105d96124226726193a7fb22068de27): Add integration with CircleCI +- [`9894deb`](https://github.com/lifion/lifion-kinesis/commit/9894debffbae98b885c6bdb2f8cb73074e8edbc0): Add basic CircleCI integration +- [`3ce84f9`](https://github.com/lifion/lifion-kinesis/commit/3ce84f9258a722e6ed5feb7a5990703615fdadfa): Add management of stream consumers, refactor stream management +- [`4faac4d`](https://github.com/lifion/lifion-kinesis/commit/4faac4db09d8e7e48f0ffe3456550745371f16a3): Ensure streams are created, encrypted, and tagged +- [`0ffdc6a`](https://github.com/lifion/lifion-kinesis/commit/0ffdc6a628e02c453b0eaf2d049ae4b12371599d): Rename project to lifion-kinesis +- [`1be561c`](https://github.com/lifion/lifion-kinesis/commit/1be561ca8ed67539c6bae1a3d20310639d664be4): Initial commit diff --git a/lib/__mocks__/aws-sdk.js b/lib/__mocks__/aws-sdk.js new file mode 100644 index 00000000..9e4940b3 --- /dev/null +++ b/lib/__mocks__/aws-sdk.js @@ -0,0 +1,113 @@ +'use strict'; + +const mockData = {}; + +function resetMockData() { + mockData.Consumers = []; + mockData.Streams = []; +} + +const addTagsToStream = jest.fn(() => ({ promise: () => Promise.resolve() })); + +const createStream = jest.fn(params => { + const { StreamName } = params; + const Stream = { + StreamName, + StreamStatus: 'CREATING', + StreamARN: [ + 'arn:aws:kinesis:us-east-1', + Math.floor(Math.random() * 1e12), + `stream/${StreamName}` + ].join(':') + }; + mockData.Streams.push(Stream); + return { promise: () => Promise.resolve({}) }; +}); + +const describeStream = jest.fn(params => { + const { StreamName } = params; + const StreamDescription = mockData.Streams.find(i => i.StreamName === StreamName); + if (!StreamDescription) { + const err = new Error("The stream doesn't exists."); + err.code = 'ResourceNotFoundException'; + return { promise: () => Promise.reject(err) }; + } + return { promise: () => Promise.resolve({ StreamDescription }) }; +}); + +const listStreamConsumers = jest.fn(() => { + const { Consumers } = mockData; + return { promise: () => Promise.resolve({ Consumers }) }; +}); + +const listTagsForStream = jest.fn(params => { + const { StreamName } = params; + const { Tags = [] } = mockData.Streams.find(i => i.StreamName === StreamName); + return { promise: () => Promise.resolve({ Tags }) }; +}); + +const registerStreamConsumer = jest.fn(params => { + const { ConsumerName } = params; + const Consumer = { + ConsumerARN: [ + 'arn:aws:kinesis:us-east-1', + Math.floor(Math.random() * 1e12), + `stream/test/consumer/${ConsumerName.toLowerCase()}`, + Math.floor(Math.random() * 1e12) + ].join(':'), + ConsumerName, + ConsumerStatus: 'ACTIVE' + }; + mockData.Consumers.push(Consumer); + return { + promise: () => Promise.resolve({ Consumer: { ...Consumer, ConsumerStatus: 'CREATING' } }) + }; +}); + +const startStreamEncryption = jest.fn(() => ({ promise: () => Promise.resolve({}) })); + +const waitFor = jest.fn((state, { StreamName }) => { + const StreamDescription = mockData.Streams.find(i => i.StreamName === StreamName); + return { promise: () => Promise.resolve({ StreamDescription }) }; +}); + +const Kinesis = jest.fn(() => ({ + addTagsToStream, + createStream, + describeStream, + listStreamConsumers, + listTagsForStream, + registerStreamConsumer, + startStreamEncryption, + waitFor +})); + +function mockClear() { + addTagsToStream.mockClear(); + createStream.mockClear(); + describeStream.mockClear(); + listStreamConsumers.mockClear(); + listTagsForStream.mockClear(); + registerStreamConsumer.mockClear(); + startStreamEncryption.mockClear(); + waitFor.mockClear(); + Kinesis.mockClear(); + resetMockData(); +} + +function mockConsumers() { + return mockData.Consumers; +} + +function mockStreams() { + return mockData.Streams; +} + +resetMockData(); + +module.exports = { + Kinesis, + mockClear, + mockConsumers, + mockStreams +}; diff --git a/lib/compression.js b/lib/compression.js new file mode 100644 index 00000000..f0b824e2 --- /dev/null +++ b/lib/compression.js @@ -0,0 +1,15 @@ +'use strict'; + +const { decompressAsync } = require('lzutf8'); + +module.exports = { + 'LZ-UTF8': { + decompress: input => + new Promise((resolve, reject) => { + decompressAsync(input, { inputEncoding: 'Base64', useWebWorker: false }, (output, err) => { + if (!err) resolve(output); + else reject(err); + }); + }) + } +}; diff --git a/lib/consumer.js b/lib/consumer.js new file mode 100644 index 00000000..521a30d9 --- /dev/null +++ b/lib/consumer.js @@ -0,0 +1,71 @@ +/* eslint-disable no-await-in-loop */ + +'use strict'; + +const { wait } = require('./utils'); + +const CONSUMER_MAX_STATE_CHECKS = 18; +const CONSUMER_STATE_CHECK_DELAY = 10000; + +module.exports.activate = async ctx => { + const { + client, + consumerName: ConsumerName, + logger, + streamArn: StreamARN, + streamName: StreamName + } = ctx; + + logger.debug(`Checking if the "${ConsumerName}" consumer for "${StreamName}" exists…`); + + async function describeStreamConsumer() { + const { Consumers } = await client.listStreamConsumers({ StreamARN }).promise(); + return Consumers.find(i => i.ConsumerName === ConsumerName) || {}; + } + + const consumer = await describeStreamConsumer(ConsumerName, client, StreamARN); + let { ConsumerStatus, ConsumerARN } = consumer; + + if (ConsumerStatus === 'ACTIVE') { + logger.debug('The stream consumer exists already and is active.'); + } + + if (ConsumerStatus === 'DELETING') { + logger.debug('Waiting for the stream consumer to complete deletion…'); + let checks = 0; + while ((await describeStreamConsumer()).ConsumerStatus) { + await wait(CONSUMER_STATE_CHECK_DELAY); + checks += 1; + if (checks > CONSUMER_MAX_STATE_CHECKS) { + const errMsg = `Consumer "${ConsumerName}" exceeded the maximum wait time for deletion.`; + logger.error(errMsg); + throw new Error(errMsg); + } + } + logger.debug('The stream consumer is now gone.'); + ConsumerStatus = ''; + } + + if (!ConsumerStatus) { + logger.debug('Trying to register the consumer…'); + const { Consumer } = await client.registerStreamConsumer({ ConsumerName, StreamARN }).promise(); + ({ ConsumerStatus, ConsumerARN } = Consumer); + } + + if (ConsumerStatus === 'CREATING') { + logger.debug('Waiting until the stream consumer is active…'); + let checks = 0; + while ((await describeStreamConsumer()).ConsumerStatus !== 'ACTIVE') { + await wait(CONSUMER_STATE_CHECK_DELAY); + checks += 1; + if (checks > CONSUMER_MAX_STATE_CHECKS) { + const errMsg = `Consumer "${ConsumerName}" exceeded the maximum wait time for activation.`; + logger.error(errMsg); + throw new Error(errMsg); + } + } + logger.debug('The stream consumer is now active.'); + } + + return ConsumerARN; +}; diff --git a/lib/consumer.test.js b/lib/consumer.test.js new file mode 100644 index 00000000..d0f70b58 --- /dev/null +++ b/lib/consumer.test.js @@ -0,0 +1,149 @@ +'use strict'; + +const { Kinesis, mockClear, mockConsumers } = require('aws-sdk'); +const consumer = require('./consumer'); + +describe('lib/consumer', () => { + let client; + let logger; + let ctx; + + beforeEach(() => { + jest.useFakeTimers(); + client = new Kinesis(); + logger = { debug: jest.fn(), error: jest.fn() }; + ctx = { + client, + consumerName: 'foo', + logger, + streamArn: 'bar', + streamName: 'baz' + }; + }); + + afterEach(() => { + mockClear(); + }); + + test('the module exports the expected', () => { + expect(consumer).toEqual({ + activate: expect.any(Function) + }); + }); + + test("activate registers a consumer and return its ARN if it doesn't exists", async () => { + await expect(consumer.activate(ctx)).resolves.toMatch(/^arn:aws:kinesis/); + expect(client.registerStreamConsumer).toBeCalledWith({ + ConsumerName: 'foo', + StreamARN: 'bar' + }); + expect(logger.debug.mock.calls).toEqual([ + ['Checking if the "foo" consumer for "baz" exists…'], + ['Trying to register the consumer…'], + ['Waiting until the stream consumer is active…'], + ['The stream consumer is now active.'] + ]); + }); + + test("activate doesn't tries to register for an already active consumer", async () => { + const mockConsumer = { + ConsumerARN: 'qux', + ConsumerName: 'foo', + ConsumerStatus: 'ACTIVE' + }; + mockConsumers().push(mockConsumer); + setTimeout.mockImplementationOnce(callback => { + mockConsumer.ConsumerStatus = 'ACTIVE'; + callback(); + }); + await expect(consumer.activate(ctx)).resolves.toBe('qux'); + expect(client.registerStreamConsumer).not.toBeCalled(); + expect(logger.debug.mock.calls).toEqual([ + ['Checking if the "foo" consumer for "baz" exists…'], + ['The stream consumer exists already and is active.'] + ]); + }); + + test("activate waits for a consumer if it's in creating state", async () => { + const mockConsumer = { + ConsumerARN: 'qux', + ConsumerName: 'foo', + ConsumerStatus: 'CREATING' + }; + mockConsumers().push(mockConsumer); + setTimeout.mockImplementationOnce(callback => { + mockConsumer.ConsumerStatus = 'ACTIVE'; + callback(); + }); + await expect(consumer.activate(ctx)).resolves.toBe('qux'); + expect(client.registerStreamConsumer).not.toBeCalled(); + expect(logger.debug.mock.calls).toEqual([ + ['Checking if the "foo" consumer for "baz" exists…'], + ['Waiting until the stream consumer is active…'], + ['The stream consumer is now active.'] + ]); + }); + + test('activate throws if waiting too long for a consumer that is in creating state', async () => { + const mockConsumer = { + ConsumerARN: 'qux', + ConsumerName: 'foo', + ConsumerStatus: 'CREATING' + }; + mockConsumers().push(mockConsumer); + setTimeout.mockImplementation(callback => callback()); + await expect(consumer.activate(ctx)).rejects.toThrow( + 'Consumer "foo" exceeded the maximum wait time for activation.' + ); + expect(logger.debug.mock.calls).toEqual([ + ['Checking if the "foo" consumer for "baz" exists…'], + ['Waiting until the stream consumer is active…'] + ]); + expect(logger.error.mock.calls).toEqual([ + ['Consumer "foo" exceeded the maximum wait time for activation.'] + ]); + }); + + test("activate waits for a consumer if it's in deleting state before creating one", async () => { + const mockConsumer = { + ConsumerARN: 'qux', + ConsumerName: 'foo', + ConsumerStatus: 'DELETING' + }; + mockConsumers().push(mockConsumer); + setTimeout.mockImplementationOnce(callback => { + mockConsumers().length = 0; + callback(); + }); + await expect(consumer.activate(ctx)).resolves.toMatch(/^arn:aws:kinesis/); + expect(client.registerStreamConsumer).toBeCalledWith({ ConsumerName: 'foo', StreamARN: 'bar' }); + expect(logger.debug.mock.calls).toEqual([ + ['Checking if the "foo" consumer for "baz" exists…'], + ['Waiting for the stream consumer to complete deletion…'], + ['The stream consumer is now gone.'], + ['Trying to register the consumer…'], + ['Waiting until the stream consumer is active…'], + ['The stream consumer is now active.'] + ]); + }); + + test('activate throws if waiting too long for a consumer that is in deleting state', async () => { + const mockConsumer = { + ConsumerARN: 'qux', + ConsumerName: 'foo', + ConsumerStatus: 'DELETING' + }; + mockConsumers().push(mockConsumer); + setTimeout.mockImplementation(callback => callback()); + await expect(consumer.activate(ctx)).rejects.toThrow( + 'Consumer "foo" exceeded the maximum wait time for deletion.' + ); + expect(logger.debug.mock.calls).toEqual([ + ['Checking if the "foo" consumer for "baz" exists…'], + ['Waiting for the stream consumer to complete deletion…'] + ]); + expect(logger.error.mock.calls).toEqual([ + ['Consumer "foo" exceeded the maximum wait time for deletion.'] + ]); + }); +}); diff --git a/lib/index.js b/lib/index.js index 8b46fbba..b8835080 100644 --- a/lib/index.js +++ b/lib/index.js @@ -1,3 +1,88 @@ 'use strict'; -module.exports = {}; +const { Kinesis: AwsKinesis } = require('aws-sdk'); +const { PassThrough } = require('stream'); +const ShardSubscriber = require('./shard-subscriber'); +const consumer = require('./consumer'); +const stream = require('./stream'); +const { noop } = require('./utils'); + +const privateData = new WeakMap(); + +function internal(instance) { + if (!privateData.has(instance)) privateData.set(instance, {}); + return privateData.get(instance); +} + +class Kinesis extends PassThrough { + constructor(options = {}) { + super({ objectMode: true }); + const { + compression, + consumerName, + createStreamIfNeeded = true, + encryption, + logger = {}, + shardCount = 1, + streamName, + tags, + ...otherOptions + } = options; + + const normLogger = { + debug: typeof logger.debug === 'function' ? logger.debug.bind(logger) : noop, + error: typeof logger.error === 'function' ? logger.error.bind(logger) : noop, + warn: typeof logger.warn === 'function' ? logger.warn.bind(logger) : noop + }; + + if (!consumerName) { + const errorMsg = 'The "consumerName" option is required.'; + normLogger.error(errorMsg); + throw new TypeError(errorMsg); + } + + if (!streamName) { + const errorMsg = 'The "streamName" option is required.'; + normLogger.error(errorMsg); + throw new TypeError(errorMsg); + } + + Object.assign(internal(this), { + compression, + consumerName, + createStreamIfNeeded, + encryption, + logger: normLogger, + shardCount, + streamName, + tags, + options: otherOptions + }); + } + + async connect() { + const ctx = internal(this); + const { consumerName, encryption, tags, logger, options } = ctx; + + logger.debug('Trying to connect the client…'); + ctx.client = new AwsKinesis(options); + + ctx.streamArn = await stream.activate(ctx); + if (encryption) await stream.encrypt(ctx); + if (tags) await stream.tag(ctx); + ctx.consumerArn = await consumer.activate(ctx); + ctx.shards = (await stream.getShards(ctx)) || []; + + logger.debug(`Creating subscribers for the stream shards using "${consumerName}"…`); + ctx.shards.forEach(shard => { + const subscriber = new ShardSubscriber({ ...ctx, emitter: this, shard }); + subscriber.start(); + subscriber.on('error', err => this.emit('error', err)); + subscriber.pipe(this); + }); + + logger.debug('The client is now connected.'); + } +} + +module.exports = Kinesis; diff --git a/lib/index.test.js b/lib/index.test.js index 1b2ff63e..288135c9 100644 --- a/lib/index.test.js +++ b/lib/index.test.js @@ -1,9 +1,120 @@ 'use strict'; -const index = require('.'); +const { Kinesis: AwsKinesis } = require('aws-sdk'); +const Kinesis = require('.'); +const consumer = require('./consumer'); +const stream = require('./stream'); +const utils = require('./utils'); + +jest.mock('./consumer'); +jest.mock('./stream'); +jest.mock('./utils'); describe('lib/index', () => { + const consumerName = 'foo'; + const streamName = 'bar'; + + afterEach(() => { + AwsKinesis.mockClear(); + consumer.activate.mockClear(); + stream.activate.mockClear(); + stream.encrypt.mockClear(); + stream.tag.mockClear(); + utils.noop.mockClear(); + }); + test('the module exports the expected', () => { - expect(index).toEqual({}); + expect(Kinesis).toEqual(expect.any(Function)); + expect(Kinesis).toThrow("Class constructor Kinesis cannot be invoked without 'new'"); + }); + + test('the constructor should throw if called without a "consumerName"', () => { + expect(() => new Kinesis()).toThrow('The "consumerName" option is required.'); + }); + + test('the constructor should throw if called without a "streamName"', () => { + expect(() => new Kinesis({ consumerName })).toThrow('The "streamName" option is required.'); + }); + + test('the constructor should be able to initialize an instance', () => { + let instance; + expect(() => { + instance = new Kinesis({ consumerName, streamName }); + }).not.toThrow(); + expect(instance).toEqual(expect.any(Kinesis)); + }); + + test('connect should return a promise', async () => { + const kinesis = new Kinesis({ consumerName, streamName }); + const promise = kinesis.connect(); + expect(promise).toEqual(expect.any(Promise)); + await expect(promise).resolves.not.toBeDefined(); + }); + + test('connect should instantiate an AWS SDK Kinesis object', async () => { + const kinesis = new Kinesis({ consumerName, streamName, foo: 'bar' }); + await kinesis.connect(); + expect(AwsKinesis).toBeCalledWith({ foo: 'bar' }); + }); + + test('connect should activate a stream', async () => { + const kinesis = new Kinesis({ consumerName, streamName }); + await kinesis.connect(); + expect(stream.activate).toBeCalledWith({ + client: expect.any(Object), + compression: undefined, + consumerArn: undefined, + consumerName, + createStreamIfNeeded: true, + encryption: undefined, + logger: expect.any(Object), + options: {}, + shardCount: 1, + shardSubscribers: [], + shards: [], + streamArn: undefined, + streamName: 'bar', + tags: undefined + }); + }); + + test('connect should use a noop logger when not provided one', async () => { + const kinesis = new Kinesis({ consumerName, streamName }); + await kinesis.connect(); + const [[{ logger }]] = stream.activate.mock.calls; + const { noop } = utils; + expect(logger).toEqual({ debug: noop, error: noop }); + expect(noop.mock.calls).toEqual([ + ['Trying to connect the client…'], + ['Creating subscribers for the stream shards using "foo"…'], + ['The client is now connected.'] + ]); + }); + + test('connect should use the provided logger', async () => { + const logger = { debug: jest.fn(), error: jest.fn() }; + await new Kinesis({ consumerName, streamName, logger }).connect(); + expect(utils.noop).not.toBeCalled(); + expect(logger.debug.mock.calls).toEqual([ + ['Trying to connect the client…'], + ['Creating subscribers for the stream shards using "foo"…'], + ['The client is now connected.'] + ]); + expect(() => new Kinesis({ logger })).toThrow(); + expect(logger.error.mock.calls[0][0]).toEqual('The "consumerName" option is required.'); + }); + + test('connect should encrypt a stream if provided with the encryption options', async () => { + await new Kinesis({ consumerName, streamName, encryption: { foo: 'bar' } }).connect(); + expect(stream.encrypt).toBeCalled(); + const [[{ encryption }]] = stream.encrypt.mock.calls; + expect(encryption).toEqual({ foo: 'bar' }); + }); + + test('connect should tag a stream if provided with the tags option', async () => { + await new Kinesis({ consumerName, streamName, tags: { foo: 'bar' } }).connect(); + expect(stream.tag).toBeCalled(); + const [[{ tags }]] = stream.tag.mock.calls; + expect(tags).toEqual({ foo: 'bar' }); }); }); diff --git a/lib/records-decoder.js b/lib/records-decoder.js new file mode 100644 index 00000000..1c855a70 --- /dev/null +++ b/lib/records-decoder.js @@ -0,0 +1,77 @@ +'use strict'; + +const { Transform } = require('stream'); +const compressionLibs = require('./compression'); +const { isJson } = require('./utils'); + +const privateData = new WeakMap(); + +function internal(instance) { + if (!privateData.has(instance)) privateData.set(instance, {}); + return privateData.get(instance); +} + +class RecordsDecoder extends Transform { + constructor(options) { + super({ objectMode: true }); + const { compression } = options; + const compressionLib = compression && compressionLibs[compression]; + Object.assign(internal(this), { ...options, compressionLib }); + } + + async _transform({ headers, payload }, encoding, callback) { + const { checkpoints, compressionLib, logger, shard } = internal(this); + const msgType = headers[':message-type']; + const eventType = headers[':event-type']; + const { ShardId: shardId } = shard; + + if (msgType !== 'event') { + this.emit('error', new Error(`Unknown event stream message type "${msgType}".`)); + return; + } + + if (eventType === 'SubscribeToShardEvent') { + try { + const { + ContinuationSequenceNumber: continuationSequenceNumber, + MillisBehindLatest: millisBehindLatest, + Records + } = payload; + const records = await Promise.all( + Records.map(async record => { + const { + ApproximateArrivalTimestamp: approximateArrivalTimestamp, + Data, + EncryptionType: encryptionType, + PartitionKey: partitionKey, + SequenceNumber: sequenceNumber + } = record; + let data; + if (compressionLib) data = await compressionLib.decompress(Data); + else data = Buffer.from(Data, 'base64').toString('utf8'); + if (isJson(data)) data = JSON.parse(data); + return { + approximateArrivalTimestamp, + data, + encryptionType, + partitionKey, + sequenceNumber + }; + }) + ); + checkpoints[shardId] = continuationSequenceNumber; + this.push({ continuationSequenceNumber, millisBehindLatest, records, shardId }); + callback(); + } catch (err) { + this.emit('error', err); + } + return; + } + + logger.debug(`Event "${eventType}" emitted.`); + this.emit(eventType, payload); + callback(); + } +} + +module.exports = RecordsDecoder; diff --git a/lib/shard-subscriber.js b/lib/shard-subscriber.js new file mode 100644 index 00000000..19d0e53e --- /dev/null +++ b/lib/shard-subscriber.js @@ -0,0 +1,152 @@ +/* eslint-disable no-await-in-loop, no-loop-func */ + +'use strict'; + +const AWS = require('aws-sdk'); +const aws4 = require('aws4'); +const got = require('got'); +const { Parser } = require('lifion-aws-event-stream'); +const { PassThrough, Transform, Writable, pipeline } = require('stream'); +const { promisify } = require('util'); +const Decoder = require('./records-decoder'); +const { wait, safeJsonParse } = require('./utils'); + +const AWS_API_TARGET = 'Kinesis_20131202.SubscribeToShard'; +const AWS_EVENT_STREAM = 'application/vnd.amazon.eventstream'; +const AWS_JSON = 'application/x-amz-json-1.1'; + +const privateData = new WeakMap(); +const asyncPipeline = promisify(pipeline); + +function internal(instance) { + if (!privateData.has(instance)) privateData.set(instance, {}); + return privateData.get(instance); +} + +class ShardSubscriber extends PassThrough { + constructor(ctx) { + super({ objectMode: true }); + const { options } = ctx; + const { endpoint = 'https://kinesis.us-east-1.amazonaws.com', region } = options; + const credentialsChain = new AWS.CredentialProviderChain(); + + const signRequest = async requestOptions => { + let { accessKeyId, secretAccessKey, sessionToken } = options; + if (!accessKeyId && !secretAccessKey && !sessionToken) + ({ accessKeyId, secretAccessKey, sessionToken } = await credentialsChain.resolvePromise()); + aws4.sign(requestOptions, { accessKeyId, secretAccessKey, sessionToken }); + }; + + const httpClient = got.extend({ + baseUrl: endpoint, + headers: { 'content-type': AWS_JSON }, + hooks: { beforeRequest: [signRequest] }, + region, + throwHttpErrors: false + }); + + Object.assign(internal(this), { ...ctx, httpClient }); + } + + async start() { + const ctx = internal(this); + const { logger, consumerName, consumerArn: ConsumerARN, shard, httpClient } = ctx; + const { ShardId } = shard; + const checkpoints = {}; + const instance = this; + + let isEventStream; + let pipelineError; + let request; + let stream; + + const handleRequest = req => { + request = req; + }; + + const handleResponse = async res => { + const { headers, statusCode } = res; + if (headers['content-type'] !== AWS_EVENT_STREAM || statusCode !== 200) { + logger.error(`Subscription unsuccessful: ${statusCode}`); + isEventStream = false; + } else { + logger.debug('Subscription to shard is successful.'); + isEventStream = true; + } + }; + + logger.debug(`Starting a "${consumerName}" subscriber for shard "${ShardId}"…`); + + do { + if (isEventStream === false) { + logger.warn(`Waiting before retrying the pipeline…`); + await wait(5000); + } + + const checkpoint = checkpoints[ShardId]; + const StartingPosition = {}; + if (checkpoint) { + logger.debug('Starting from local checkpoint.'); + StartingPosition.Type = 'AFTER_SEQUENCE_NUMBER'; + StartingPosition.SequenceNumber = checkpoint; + } else { + logger.debug('Starting position: LATEST'); + StartingPosition.Type = 'LATEST'; + } + + stream = httpClient.stream('/', { + body: JSON.stringify({ ConsumerARN, ShardId, StartingPosition }), + headers: { 'X-Amz-Target': AWS_API_TARGET }, + service: 'kinesis' + }); + + stream.on('request', handleRequest); + stream.on('response', handleResponse); + + try { + await asyncPipeline([ + stream, + new Transform({ + objectMode: true, + write(chunk, encoding, callback) { + if (!isEventStream) { + const { __type, message } = safeJsonParse(chunk.toString('utf8')); + const err = new Error(message || 'Failed to subscribe to shard.'); + if (__type) err.code = __type; + err.isRetryable = true; + this.emit('error', err); + } else { + this.push(chunk); + } + callback(); + } + }), + new Parser(), + new Decoder({ ...ctx, checkpoints }), + new Writable({ + objectMode: true, + write(chunk, encoding, callback) { + instance.push(chunk); + callback(); + } + }) + ]); + } catch (err) { + if (err.isRetryable) { + const { code, message } = err; + logger.warn(`Pipeline closed with retryable error: [${code}] ${message}`); + } else { + this.emit('error', err); + logger.error('Pipeline closed with error:', err.stack); + } + pipelineError = err; + } + } while (!pipelineError || pipelineError.isRetryable); + + if (request) request.abort(); + + return this; + } +} + +module.exports = ShardSubscriber; diff --git a/lib/stream.js b/lib/stream.js new file mode 100644 index 00000000..273fd1aa --- /dev/null +++ b/lib/stream.js @@ -0,0 +1,99 @@ +'use strict'; + +const equal = require('fast-deep-equal'); + +module.exports.activate = async ctx => { + const { + client, + createStreamIfNeeded, + logger, + shardCount: ShardCount, + streamName: StreamName + } = ctx; + + logger.debug(`Checking if the stream "${StreamName}" exists…`); + + let StreamARN; + let StreamStatus; + + try { + let { StreamDescription } = await client.describeStream({ StreamName }).promise(); + ({ StreamStatus, StreamARN } = StreamDescription); + logger.debug(`The stream status is ${StreamStatus}.`); + + if (StreamStatus === 'DELETING') { + logger.debug('Waiting for the stream to complete deletion…'); + await client.waitFor('streamNotExists', { StreamName, Limit: 1 }).promise(); + StreamStatus = ''; + logger.debug('The stream is now gone.'); + } else if (StreamStatus && StreamStatus !== 'ACTIVE') { + logger.debug('Waiting for the stream to be active…'); + ({ StreamDescription } = await client.waitFor('streamExists', { StreamName }).promise()); + ({ StreamARN } = StreamDescription); + logger.debug('The stream is now active.'); + } + } catch (err) { + if (!createStreamIfNeeded || err.code !== 'ResourceNotFoundException') { + logger.error(err); + throw err; + } + } + + if (!StreamStatus) { + logger.debug('Trying to create the stream…'); + await client.createStream({ StreamName, ShardCount }).promise(); + logger.debug('Waiting for the new stream to be active…'); + const { StreamDescription } = await client.waitFor('streamExists', { StreamName }).promise(); + ({ StreamARN } = StreamDescription); + logger.debug('The new stream is now active.'); + } + + return StreamARN; +}; + +module.exports.encrypt = async ctx => { + const { + client, + encryption: { type: EncryptionType, keyId: KeyId }, + logger, + streamName: StreamName + } = ctx; + + logger.debug(`Checking if the stream "${StreamName}" is encrypted…`); + + const { StreamDescription } = await client.describeStream({ StreamName }).promise(); + + if (StreamDescription.EncryptionType === 'NONE') { + logger.debug('Trying to encrypt the stream…'); + await client.startStreamEncryption({ StreamName, EncryptionType, KeyId }).promise(); + logger.debug('Waiting for the stream to update…'); + await client.waitFor('streamExists', { StreamName }).promise(); + logger.debug('The stream is now encrypted.'); + } else { + logger.debug('The stream is encrypted.'); + } +}; + +module.exports.tag = async ctx => { + const { client, logger, streamName: StreamName, tags } = ctx; + + logger.debug(`Checking if the stream "${StreamName}" is already tagged…`); + + const { Tags } = await client.listTagsForStream({ StreamName }).promise(); + const existingTags = Tags.reduce((obj, { Key, Value }) => ({ ...obj, [Key]: Value }), {}); + const mergedTags = { ...existingTags, ...tags }; + + if (!equal(existingTags, mergedTags)) { + await client.addTagsToStream({ StreamName, Tags: mergedTags }).promise(); + logger.debug(`The stream tags have been updated.`); + } else { + logger.debug('The stream is already tagged as required.'); + } +}; + +module.exports.getShards = async ctx => { + const { logger, client, streamName: StreamName } = ctx; + logger.debug(`Retrieving the shards for the stream "${StreamName}"…`); + const { Shards } = await client.listShards({ StreamName, MaxResults: 1000 }).promise(); + return Shards; +}; diff --git a/lib/stream.test.js b/lib/stream.test.js new file mode 100644 index 00000000..3b4d490e --- /dev/null +++ b/lib/stream.test.js @@ -0,0 +1,183 @@ +'use strict'; + +const { Kinesis, mockClear, mockStreams } = require('aws-sdk'); +const stream = require('./stream'); + +describe('lib/stream', () => { + let client; + let logger; + let ctx; + + beforeEach(() => { + client = new Kinesis(); + logger = { debug: jest.fn(), error: jest.fn() }; + ctx = { + createStreamIfNeeded: true, + client, + logger, + shardCount: 1, + streamName: 'foo' + }; + }); + + afterEach(() => { + mockClear(); + }); + + test('the module exports the expected', () => { + expect(stream).toEqual({ + activate: expect.any(Function), + encrypt: expect.any(Function), + getShards: expect.any(Function), + tag: expect.any(Function) + }); + }); + + test("activate creates a stream if it's doesn't exists and auto-create is on", async () => { + await expect(stream.activate(ctx)).resolves.toMatch(/^arn:aws:kinesis/); + expect(client.createStream).toBeCalledWith({ ShardCount: 1, StreamName: 'foo' }); + expect(logger.debug.mock.calls).toEqual([ + ['Checking if the stream "foo" exists…'], + ['Trying to create the stream…'], + ['Waiting for the new stream to be active…'], + ['The new stream is now active.'] + ]); + }); + + test("activate throws if a stream doesn't exists and auto-create is off", async () => { + ctx.createStreamIfNeeded = false; + await expect(stream.activate(ctx)).rejects.toThrow("The stream doesn't exists."); + expect(client.createStream).not.toBeCalledWith(); + expect(logger.debug.mock.calls).toEqual([['Checking if the stream "foo" exists…']]); + const [[{ code, message }]] = logger.error.mock.calls; + expect(code).toBe('ResourceNotFoundException'); + expect(message).toBe("The stream doesn't exists."); + }); + + test("activate won't try to create a stream if it exists already", async () => { + const mockStream = { StreamARN: 'bar', StreamName: 'foo', StreamStatus: 'ACTIVE' }; + mockStreams().push(mockStream); + await expect(stream.activate(ctx)).resolves.toBe('bar'); + expect(client.createStream).not.toBeCalledWith(); + expect(logger.debug.mock.calls).toEqual([ + ['Checking if the stream "foo" exists…'], + ['The stream status is ACTIVE.'] + ]); + }); + + test("activate waits for a stream if it's in creating state", async () => { + const mockStream = { StreamARN: 'bar', StreamName: 'foo', StreamStatus: 'CREATING' }; + mockStreams().push(mockStream); + await expect(stream.activate(ctx)).resolves.toMatch('bar'); + expect(client.createStream).not.toBeCalled(); + expect(logger.debug.mock.calls).toEqual([ + ['Checking if the stream "foo" exists…'], + ['The stream status is CREATING.'], + ['Waiting for the stream to be active…'], + ['The stream is now active.'] + ]); + }); + + test('activate waits for a stream in deleting state before trying to create it', async () => { + const mockStream = { StreamARN: 'bar', StreamName: 'foo', StreamStatus: 'DELETING' }; + mockStreams().push(mockStream); + await expect(stream.activate(ctx)).resolves.toMatch('bar'); + expect(client.createStream).toBeCalledWith({ ShardCount: 1, StreamName: 'foo' }); + expect(logger.debug.mock.calls).toEqual([ + ['Checking if the stream "foo" exists…'], + ['The stream status is DELETING.'], + ['Waiting for the stream to complete deletion…'], + ['The stream is now gone.'], + ['Trying to create the stream…'], + ['Waiting for the new stream to be active…'], + ['The new stream is now active.'] + ]); + }); + + test('encrypt will start the stream encryption if not previously encrypted', async () => { + ctx.encryption = { type: 'baz', keyId: 'qux' }; + const mockStream = { StreamName: 'foo', StreamStatus: 'ACTIVE', EncryptionType: 'NONE' }; + mockStreams().push(mockStream); + await expect(stream.encrypt(ctx)).resolves.not.toBeDefined(); + expect(client.startStreamEncryption).toBeCalledWith({ + EncryptionType: 'baz', + KeyId: 'qux', + StreamName: 'foo' + }); + expect(logger.debug.mock.calls).toEqual([ + ['Checking if the stream "foo" is encrypted…'], + ['Trying to encrypt the stream…'], + ['Waiting for the stream to update…'], + ['The stream is now encrypted.'] + ]); + }); + + test("encrypt won't try to encrypt the stream if it's already encrypted", async () => { + ctx.encryption = { type: 'baz', keyId: 'qux' }; + const mockStream = { StreamName: 'foo', StreamStatus: 'ACTIVE', EncryptionType: 'KMS' }; + mockStreams().push(mockStream); + await expect(stream.encrypt(ctx)).resolves.not.toBeDefined(); + expect(client.startStreamEncryption).not.toBeCalled(); + expect(logger.debug.mock.calls).toEqual([ + ['Checking if the stream "foo" is encrypted…'], + ['The stream is encrypted.'] + ]); + }); + + test("tag updates the stream if it's not already tagged", async () => { + ctx.tags = { baz: 'qux', quux: 'quuz', corge: 'grault' }; + const mockStream = { + StreamName: 'foo', + StreamStatus: 'ACTIVE' + }; + mockStreams().push(mockStream); + await expect(stream.tag(ctx)).resolves.not.toBeDefined(); + expect(client.addTagsToStream).toBeCalledWith({ + StreamName: 'foo', + Tags: { baz: 'qux', corge: 'grault', quux: 'quuz' } + }); + expect(logger.debug.mock.calls).toEqual([ + ['Checking if the stream "foo" is already tagged…'], + ['The stream tags have been updated.'] + ]); + }); + + test('tag updates the stream with the previous and new tags', async () => { + ctx.tags = { corge: 'grault' }; + const mockStream = { + StreamName: 'foo', + StreamStatus: 'ACTIVE', + Tags: [{ Key: 'baz', Value: 'qux' }] + }; + mockStreams().push(mockStream); + await stream.tag(ctx); + expect(client.addTagsToStream).toBeCalledWith({ + StreamName: 'foo', + Tags: { baz: 'qux', corge: 'grault' } + }); + expect(logger.debug.mock.calls).toEqual([ + ['Checking if the stream "foo" is already tagged…'], + ['The stream tags have been updated.'] + ]); + }); + + test("tag won't update the stream if the previous and new tags are the same", async () => { + ctx.tags = { baz: 'qux', quux: 'quuz', corge: 'grault' }; + const mockStream = { + StreamName: 'foo', + StreamStatus: 'ACTIVE', + Tags: [ + { Key: 'baz', Value: 'qux' }, + { Key: 'quux', Value: 'quuz' }, + { Key: 'corge', Value: 'grault' } + ] + }; + mockStreams().push(mockStream); + await stream.tag(ctx); + expect(client.addTagsToStream).not.toBeCalled(); + expect(logger.debug.mock.calls).toEqual([ + ['Checking if the stream "foo" is already tagged…'], + ['The stream is already tagged as required.'] + ]); + }); +}); diff --git a/lib/utils.js b/lib/utils.js new file mode 100644 index 00000000..ac9df451 --- /dev/null +++ b/lib/utils.js @@ -0,0 +1,25 @@ +'use strict'; + +const isJsonRegex = /^[{[].*[}\]]$/; + +function isJson(input) { + return isJsonRegex.test(input); +} + +function noop() {} + +function safeJsonParse(input) { + try { + return JSON.parse(input); + } catch (err) { + return {}; + } +} + +function wait(ms) { + return new Promise(resolve => { + setTimeout(resolve, ms); + }); +} + +module.exports = { isJson, noop, safeJsonParse, wait }; diff --git a/lib/utils.test.js b/lib/utils.test.js new file mode 100644 index 00000000..c67171b2 --- /dev/null +++ b/lib/utils.test.js @@ -0,0 +1,35 @@ +'use strict'; + +const utils = require('./utils'); + +const { isJson, noop, wait } = utils; + +describe('lib/utils', () => { + test('the module exports the expected', () => { + expect(utils).toEqual({ + isJson: expect.any(Function), + noop: expect.any(Function), + wait: expect.any(Function) + }); + }); + + test('the isJson function returns true when called with a JSON', () => { + expect(isJson(JSON.stringify({ foo: 'bar' }))).toBe(true); + }); + + test('the isJson function returns false when called with a non-JSON string', () => { + expect(isJson('{')).toBe(false); + }); + + test('the noop function can be used to default functions in options', () => { + const { foo = noop } = {}; + expect(() => foo()).not.toThrow(); + }); + + test('the wait function can be used to delay the execution of the next statement', async () => { + const before = new Date().getTime(); + await wait(32); + const after = new Date().getTime(); + expect(after - before).toBeGreaterThanOrEqual(32); + }); +}); diff --git a/package-lock.json b/package-lock.json index faa425c2..2eca7e4b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -120,6 +120,22 @@ "any-observable": "^0.3.0" } }, + "@sindresorhus/is": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.12.0.tgz", + "integrity": "sha512-9ve22cGrAKlSRvi8Vb2JIjzcaaQg79531yQHnF+hi/kOpsSj3Om8AyR1wcHrgl0u7U3vYQ7gmF5erZzOp4+51Q==", + "requires": { + "symbol-observable": "^1.2.0" + } + }, + "@szmarczak/http-timer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.1.tgz", + "integrity": "sha512-WljfOGkmSJe8SUkl+4TPvN2ec0dpUGVyfTBQLoXJUiILs+wBSc4Kvp2N3aAWE4VwwDSLGdmD3/bufS5BgZpVSQ==", + "requires": { + "defer-to-connect": "^1.0.1" + } + }, "JSV": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/JSV/-/JSV-4.0.2.tgz", @@ -161,21 +177,21 @@ "dev": true }, "acorn-walk": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.1.0.tgz", - "integrity": "sha512-ugTb7Lq7u4GfWSqqpwE0bGyoBZNMTok/zDBXxfEG0QM50jNlGhIWjRC1pPN7bvV1anhF+bs+/gNcRw+o55Evbg==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.1.1.tgz", + "integrity": "sha512-OtUw6JUTgxA2QoqqmrmQ7F2NYqiBPi/L2jqHyFtllhOUvXYQXf0Z1CYUinIfyT4bTCGmrA7gX9FvHA81uzCoVw==", "dev": true }, "ajv": { - "version": "5.5.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", - "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", + "version": "6.5.5", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.5.5.tgz", + "integrity": "sha512-7q7gtRQDJSyuEHjuVgHoUa2VuemFiCMrfQc9Tc08XTAc4Zj/5U1buQJ0HU6i7fKjXU09SVgSmxa4sLvuvS8Iyg==", "dev": true, "requires": { - "co": "^4.6.0", - "fast-deep-equal": "^1.0.0", + "fast-deep-equal": "^2.0.1", "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.3.0" + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" } }, "ansi-align": { @@ -188,9 +204,9 @@ } }, "ansi-escape-sequences": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/ansi-escape-sequences/-/ansi-escape-sequences-4.0.0.tgz", - "integrity": "sha512-v+0wW9Wezwsyb0uF4aBVCjmSqit3Ru7PZFziGF0o2KwTvN2zWfTi3BRLq9EkJFdg3eBbyERXGTntVpBxH1J68Q==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/ansi-escape-sequences/-/ansi-escape-sequences-4.0.1.tgz", + "integrity": "sha512-G3Aona26cXv8nWIwID6MP11WSishqnyOPQjYaVJ7CfY2Xgu5sHOXM39nQg6XtyfF9++oLV6l2uFGojBb4zglGA==", "dev": true, "requires": { "array-back": "^2.0.0" @@ -590,7 +606,7 @@ }, "array-equal": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-equal/-/array-equal-1.0.0.tgz", + "resolved": "http://registry.npmjs.org/array-equal/-/array-equal-1.0.0.tgz", "integrity": "sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM=", "dev": true }, @@ -696,6 +712,22 @@ "semver": "^5.6.0" } }, + "aws-sdk": { + "version": "2.359.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.359.0.tgz", + "integrity": "sha512-Rf5Bqps00fZISnPePVRW4sEqasBOGhbGyEDeF9bv3FEiYv5Rj9Tz3vKZGkpNl8eONdVPI5xu2y3W3iE7oZvfwA==", + "requires": { + "buffer": "4.9.1", + "events": "1.1.1", + "ieee754": "1.1.8", + "jmespath": "0.15.0", + "querystring": "0.2.0", + "sax": "1.2.1", + "url": "0.10.3", + "uuid": "3.1.0", + "xml2js": "0.4.19" + } + }, "aws-sign2": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", @@ -705,8 +737,7 @@ "aws4": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", - "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==", - "dev": true + "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" }, "babel-code-frame": { "version": "6.26.0", @@ -944,14 +975,6 @@ "requires": { "core-js": "^2.4.0", "regenerator-runtime": "^0.11.0" - }, - "dependencies": { - "regenerator-runtime": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", - "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==", - "dev": true - } } }, "babel-template": { @@ -1098,6 +1121,11 @@ } } }, + "base64-js": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.0.tgz", + "integrity": "sha512-ccav/yGvoa80BQDljCxsmmQ3Xvx60/UpBIij5QN21W3wBi/hhIC9OoO+KLpu9IJTS9j4DRVJ3aDDF9cMSoa2lw==" + }, "bcrypt-pbkdf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", @@ -1114,9 +1142,9 @@ "dev": true }, "bluebird": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.2.tgz", - "integrity": "sha512-dhHTWMI7kMx5whMQntl7Vr9C6BvV10lFXDAasnqnrMYhXVCzzk6IO9Fo2L75jXHT07WrOngL1WDXOp+yYS91Yg==", + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.3.tgz", + "integrity": "sha512-/qKPUQlaW1OyR51WeCPBvRnAlnZFUJkCSG5HzGnuIqhgyJtF+T94lFnn33eiazjRm2LAHVy2guNnaq48X9SJuw==", "dev": true }, "boxen": { @@ -1187,6 +1215,16 @@ "node-int64": "^0.4.0" } }, + "buffer": { + "version": "4.9.1", + "resolved": "http://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", + "integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=", + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, "buffer-from": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", @@ -1235,6 +1273,37 @@ "mkdirp2": "^1.0.3" } }, + "cacheable-request": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-5.2.0.tgz", + "integrity": "sha512-h1n0vjpFaByTvU6PiyTKk2kx4OnuV1aVUynCUd/FiKl4icpPSceowk3rHczwFEBuZvz+E1EU4KExR0MCPeQfaQ==", + "requires": { + "clone-response": "^1.0.2", + "get-stream": "^4.0.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^3.0.0", + "lowercase-keys": "^1.0.1", + "normalize-url": "^3.1.0", + "responselike": "^1.0.2" + } + }, + "caller-callsite": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz", + "integrity": "sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ=", + "dev": true, + "requires": { + "callsites": "^2.0.0" + }, + "dependencies": { + "callsites": { + "version": "2.0.0", + "resolved": "http://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", + "integrity": "sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=", + "dev": true + } + } + }, "caller-path": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-0.1.0.tgz", @@ -1246,7 +1315,7 @@ }, "callsites": { "version": "0.2.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-0.2.0.tgz", + "resolved": "http://registry.npmjs.org/callsites/-/callsites-0.2.0.tgz", "integrity": "sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo=", "dev": true }, @@ -1567,6 +1636,14 @@ "wrap-ansi": "^2.0.0" } }, + "clone-response": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", + "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=", + "requires": { + "mimic-response": "^1.0.0" + } + }, "co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -1772,11 +1849,12 @@ "dev": true }, "cosmiconfig": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.0.6.tgz", - "integrity": "sha512-6DWfizHriCrFWURP1/qyhsiFvYdlJzbCzmtFWh744+KyWsJo5+kPzUZZaMRSSItoYc0pxFX7gEO7ZC1/gN/7AQ==", + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.0.7.tgz", + "integrity": "sha512-PcLqxTKiDmNT6pSpy4N6KtuPwb53W+2tzNvwOZw0WH9N6O0vLIBq0x8aj8Oj75ere4YcGi48bDFCL+3fRJdlNA==", "dev": true, "requires": { + "import-fresh": "^2.0.0", "is-directory": "^0.3.1", "js-yaml": "^3.9.0", "parse-json": "^4.0.0" @@ -1794,6 +1872,25 @@ } } }, + "crc": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/crc/-/crc-3.8.0.tgz", + "integrity": "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==", + "requires": { + "buffer": "^5.1.0" + }, + "dependencies": { + "buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.2.1.tgz", + "integrity": "sha512-c+Ko0loDaFfuPWiL02ls9Xd3GO3cPVmUobQ6t3rXNUk304u6hGq+8N/kFi+QEIKhzK3uwolVhLzszmfLmMLnqg==", + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4" + } + } + } + }, "create-error-class": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/create-error-class/-/create-error-class-3.0.2.tgz", @@ -1895,6 +1992,14 @@ "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", "dev": true }, + "decompress-response": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", + "integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=", + "requires": { + "mimic-response": "^1.0.0" + } + }, "dedent": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", @@ -1933,6 +2038,11 @@ } } }, + "defer-to-connect": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.0.1.tgz", + "integrity": "sha512-2e0FJesseUqQj671gvZWfUyxpnFx/5n4xleamlpCD3U6Fm5dh5qzmmLNxNhtmHF06+SYVHH8QU6FACffYTnj0Q==" + }, "define-properties": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", @@ -2100,8 +2210,7 @@ "duplexer3": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", - "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=", - "dev": true + "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=" }, "ecc-jsbn": { "version": "0.1.2", @@ -2123,7 +2232,6 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", - "dev": true, "requires": { "once": "^1.4.0" } @@ -2234,18 +2342,6 @@ "text-table": "^0.2.0" }, "dependencies": { - "ajv": { - "version": "6.5.5", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.5.5.tgz", - "integrity": "sha512-7q7gtRQDJSyuEHjuVgHoUa2VuemFiCMrfQc9Tc08XTAc4Zj/5U1buQJ0HU6i7fKjXU09SVgSmxa4sLvuvS8Iyg==", - "dev": true, - "requires": { - "fast-deep-equal": "^2.0.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, "cross-spawn": { "version": "6.0.5", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", @@ -2258,18 +2354,6 @@ "shebang-command": "^1.2.0", "which": "^1.2.9" } - }, - "fast-deep-equal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", - "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", - "dev": true - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true } } }, @@ -2610,6 +2694,11 @@ "through": "^2.3.8" } }, + "events": { + "version": "1.1.1", + "resolved": "http://registry.npmjs.org/events/-/events-1.1.1.tgz", + "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=" + }, "exec-sh": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/exec-sh/-/exec-sh-0.2.2.tgz", @@ -2646,15 +2735,6 @@ "shebang-command": "^1.2.0", "which": "^1.2.9" } - }, - "get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dev": true, - "requires": { - "pump": "^3.0.0" - } } } }, @@ -2750,10 +2830,9 @@ "dev": true }, "fast-deep-equal": { - "version": "1.1.0", - "resolved": "http://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", - "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=", - "dev": true + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" }, "fast-diff": { "version": "1.2.0", @@ -2867,21 +2946,21 @@ } }, "flat-cache": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-1.3.2.tgz", - "integrity": "sha512-KByBY8c98sLUAGpnmjEdWTrtrLZRtZdwds+kAL/ciFXTCb7AZgqKsAnVnYFQj1hxepwO8JKN/8AsRWwLq+RK0A==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-1.3.4.tgz", + "integrity": "sha512-VwyB3Lkgacfik2vhqR4uv2rvebqmDvFu4jlN/C1RzWoJEo8I7z4Q404oiqYCkq41mni8EzQnm95emU9seckwtg==", "dev": true, "requires": { "circular-json": "^0.3.1", - "del": "^3.0.0", "graceful-fs": "^4.1.2", + "rimraf": "~2.6.2", "write": "^0.2.1" } }, "flatmap-stream": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/flatmap-stream/-/flatmap-stream-0.1.1.tgz", - "integrity": "sha512-lAq4tLbm3sidmdCN8G3ExaxH7cUCtP5mgDvrYowsx84dcYkJJ4I28N7gkxA6+YlSXzaGLJYIDEi9WGfXzMiXdw==", + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/flatmap-stream/-/flatmap-stream-0.1.2.tgz", + "integrity": "sha512-ucyr6WkLXjyMuHPtOUq4l+nSAxgWi7v4QO508eQ9resnGj+lSup26oIsUI5aH8k4Qfpjsxa8dDf9UCKkS2KHzQ==", "dev": true }, "for-in": { @@ -2963,8 +3042,7 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "aproba": { "version": "1.2.0", @@ -2985,14 +3063,12 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -3007,20 +3083,17 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "core-util-is": { "version": "1.0.2", @@ -3137,8 +3210,7 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "ini": { "version": "1.3.5", @@ -3150,7 +3222,6 @@ "version": "1.0.0", "bundled": true, "dev": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -3165,7 +3236,6 @@ "version": "3.0.4", "bundled": true, "dev": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -3173,14 +3243,12 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "minipass": { "version": "2.2.4", "bundled": true, "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.1.1", "yallist": "^3.0.0" @@ -3199,7 +3267,6 @@ "version": "0.5.1", "bundled": true, "dev": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -3280,8 +3347,7 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "object-assign": { "version": "4.1.1", @@ -3293,7 +3359,6 @@ "version": "1.4.0", "bundled": true, "dev": true, - "optional": true, "requires": { "wrappy": "1" } @@ -3379,8 +3444,7 @@ "safe-buffer": { "version": "5.1.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "safer-buffer": { "version": "2.1.2", @@ -3416,7 +3480,6 @@ "version": "1.0.2", "bundled": true, "dev": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -3436,7 +3499,6 @@ "version": "3.0.1", "bundled": true, "dev": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -3480,14 +3542,12 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "yallist": { "version": "3.0.2", "bundled": true, - "dev": true, - "optional": true + "dev": true } } }, @@ -3533,10 +3593,12 @@ "dev": true }, "get-stream": { - "version": "3.0.0", - "resolved": "http://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", - "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", - "dev": true + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "requires": { + "pump": "^3.0.0" + } }, "get-value": { "version": "2.0.6", @@ -3603,7 +3665,7 @@ }, "globby": { "version": "6.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", + "resolved": "http://registry.npmjs.org/globby/-/globby-6.1.0.tgz", "integrity": "sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=", "dev": true, "requires": { @@ -3615,22 +3677,21 @@ } }, "got": { - "version": "6.7.1", - "resolved": "http://registry.npmjs.org/got/-/got-6.7.1.tgz", - "integrity": "sha1-JAzQV4WpoY5WHcG0S0HHY+8ejbA=", - "dev": true, - "requires": { - "create-error-class": "^3.0.0", + "version": "9.3.2", + "resolved": "https://registry.npmjs.org/got/-/got-9.3.2.tgz", + "integrity": "sha512-OyKOUg71IKvwb8Uj0KP6EN3+qVVvXmYsFznU1fnwUnKtDbZnwSlAi7muNlu4HhBfN9dImtlgg9e7H0g5qVdaeQ==", + "requires": { + "@sindresorhus/is": "^0.12.0", + "@szmarczak/http-timer": "^1.1.0", + "cacheable-request": "^5.1.0", + "decompress-response": "^3.3.0", "duplexer3": "^0.1.4", - "get-stream": "^3.0.0", - "is-redirect": "^1.0.0", - "is-retry-allowed": "^1.0.0", - "is-stream": "^1.0.0", - "lowercase-keys": "^1.0.0", - "safe-buffer": "^5.0.1", - "timed-out": "^4.0.0", - "unzip-response": "^2.0.1", - "url-parse-lax": "^1.0.0" + "get-stream": "^4.1.0", + "lowercase-keys": "^1.0.1", + "mimic-response": "^1.0.1", + "p-cancelable": "^1.0.0", + "to-readable-stream": "^1.0.0", + "url-parse-lax": "^3.0.0" } }, "graceful-fs": { @@ -3664,12 +3725,12 @@ "dev": true }, "har-validator": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.0.tgz", - "integrity": "sha512-+qnmNjI4OfH2ipQ9VQOw23bBd/ibtfbVdK2fYbY4acTDqKTW/YDp9McimZdDbG8iV9fZizUqQMD5xvriB146TA==", + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", + "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", "dev": true, "requires": { - "ajv": "^5.3.0", + "ajv": "^6.5.5", "har-schema": "^2.0.0" } }, @@ -3802,6 +3863,11 @@ "whatwg-encoding": "^1.0.1" } }, + "http-cache-semantics": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.0.0.tgz", + "integrity": "sha512-NtexGRtaV5z3ZUX78W9UDTOJPBdpqms6RmwQXmOhHws7CuQK3cqIoQtnmeqi1VvVD6u6eMMRL0sKE9BCZXTDWQ==" + }, "http-signature": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", @@ -3927,6 +3993,11 @@ "safer-buffer": ">= 2.1.2 < 3" } }, + "ieee754": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.8.tgz", + "integrity": "sha1-vjPUCsEO8ZJnAfbwii2G+/0a0+Q=" + }, "ignore": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", @@ -3948,6 +4019,33 @@ "minimatch": "^3.0.4" } }, + "import-fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz", + "integrity": "sha1-2BNVwVYS04bGH53dOSLUMEgipUY=", + "dev": true, + "requires": { + "caller-path": "^2.0.0", + "resolve-from": "^3.0.0" + }, + "dependencies": { + "caller-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-2.0.0.tgz", + "integrity": "sha1-Ro+DBE42mrIBD6xfBs7uFbsssfQ=", + "dev": true, + "requires": { + "caller-callsite": "^2.0.0" + } + }, + "resolve-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", + "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=", + "dev": true + } + } + }, "import-lazy": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz", @@ -4379,8 +4477,7 @@ "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" }, "isexe": { "version": "2.0.0", @@ -4707,12 +4804,16 @@ "pretty-format": "^23.6.0" } }, - "jest-junit-reporter": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/jest-junit-reporter/-/jest-junit-reporter-1.1.0.tgz", - "integrity": "sha1-iNYAbsE/gt9AxHiCyGQJic3LFDQ=", + "jest-junit": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/jest-junit/-/jest-junit-5.2.0.tgz", + "integrity": "sha512-Mdg0Qpdh1Xm/FA1B/mcLlmEmlr3XzH5pZg7MvcAwZhjHijPRd1z/UwYwkwNHmCV7o4ZOWCf77nLu7ZkhHHrtJg==", "dev": true, "requires": { + "jest-config": "^23.6.0", + "jest-validate": "^23.0.1", + "mkdirp": "^0.5.1", + "strip-ansi": "^4.0.0", "xml": "^1.0.1" } }, @@ -4902,7 +5003,7 @@ "dependencies": { "callsites": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", + "resolved": "http://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", "integrity": "sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=", "dev": true }, @@ -4946,6 +5047,11 @@ "merge-stream": "^1.0.1" } }, + "jmespath": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz", + "integrity": "sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc=" + }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -5102,15 +5208,26 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.3.tgz", "integrity": "sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==", "dev": true + }, + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", + "dev": true } } }, "jsesc": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz", + "resolved": "http://registry.npmjs.org/jsesc/-/jsesc-1.3.0.tgz", "integrity": "sha1-RsP+yMGJKxKwgz25vHYiF226s0s=", "dev": true }, + "json-buffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", + "integrity": "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=" + }, "json-parse-better-errors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", @@ -5124,9 +5241,9 @@ "dev": true }, "json-schema-traverse": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz", - "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true }, "json-stable-stringify-without-jsonify": { @@ -5169,6 +5286,14 @@ "verror": "1.10.0" } }, + "keyv": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz", + "integrity": "sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==", + "requires": { + "json-buffer": "3.0.0" + } + }, "kind-of": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", @@ -5233,6 +5358,14 @@ "type-check": "~0.3.2" } }, + "lifion-aws-event-stream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lifion-aws-event-stream/-/lifion-aws-event-stream-1.0.1.tgz", + "integrity": "sha512-lEX26ChW+G/ilJn4AxXhGYN5HsPAHILIDrlFmiwt4d3aguw1KqEnSzvlTUheYXQJeswh2D01Dyl/soM2uuTAyw==", + "requires": { + "crc": "^3.8.0" + } + }, "lint-staged": { "version": "8.0.5", "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-8.0.5.tgz", @@ -5859,8 +5992,7 @@ "lowercase-keys": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", - "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==", - "dev": true + "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==" }, "lru-cache": { "version": "4.1.3", @@ -5872,6 +6004,11 @@ "yallist": "^2.1.2" } }, + "lzutf8": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/lzutf8/-/lzutf8-0.5.5.tgz", + "integrity": "sha512-x4AdRtP0ETRe0BW8V+TfW+8rOF5t3vWU/Z52twUqDRf0OiEuXiLvXNN6Zm8biFCTI9ffXBZb++THAjfkJ559Nw==" + }, "make-dir": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", @@ -6006,6 +6143,11 @@ "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", "dev": true }, + "mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==" + }, "minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", @@ -6253,6 +6395,11 @@ "remove-trailing-separator": "^1.0.1" } }, + "normalize-url": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-3.3.0.tgz", + "integrity": "sha512-U+JJi7duF1o+u2pynbp2zXDW2/PADgC30f0GsHZtRh+HOcXHnw137TrNlyxxRvWW5fjKd3bcLHPxofWuCjaeZg==" + }, "npm-path": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/npm-path/-/npm-path-2.0.4.tgz", @@ -6438,7 +6585,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, "requires": { "wrappy": "1" } @@ -6486,7 +6632,7 @@ }, "os-homedir": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "resolved": "http://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", "dev": true }, @@ -6515,15 +6661,26 @@ "signal-exit": "^3.0.0", "strip-eof": "^1.0.0" } + }, + "get-stream": { + "version": "3.0.0", + "resolved": "http://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", + "dev": true } } }, "os-tmpdir": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "resolved": "http://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", "dev": true }, + "p-cancelable": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.0.0.tgz", + "integrity": "sha512-USgPoaC6tkTGlS831CxsVdmZmyb8tR1D+hStI84MyckLOzfJlYQUweomrwE3D8T7u5u5GVuW064LT501wHTYYA==" + }, "p-finally": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", @@ -6570,6 +6727,48 @@ "registry-auth-token": "^3.0.1", "registry-url": "^3.0.3", "semver": "^5.1.0" + }, + "dependencies": { + "get-stream": { + "version": "3.0.0", + "resolved": "http://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", + "dev": true + }, + "got": { + "version": "6.7.1", + "resolved": "http://registry.npmjs.org/got/-/got-6.7.1.tgz", + "integrity": "sha1-JAzQV4WpoY5WHcG0S0HHY+8ejbA=", + "dev": true, + "requires": { + "create-error-class": "^3.0.0", + "duplexer3": "^0.1.4", + "get-stream": "^3.0.0", + "is-redirect": "^1.0.0", + "is-retry-allowed": "^1.0.0", + "is-stream": "^1.0.0", + "lowercase-keys": "^1.0.0", + "safe-buffer": "^5.0.1", + "timed-out": "^4.0.0", + "unzip-response": "^2.0.1", + "url-parse-lax": "^1.0.0" + } + }, + "prepend-http": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", + "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=", + "dev": true + }, + "url-parse-lax": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz", + "integrity": "sha1-evjzA2Rem9eaJy56FKxovAYJ2nM=", + "dev": true, + "requires": { + "prepend-http": "^1.0.1" + } + } } }, "parse-github-url": { @@ -6628,7 +6827,7 @@ }, "path-is-absolute": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "resolved": "http://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "dev": true }, @@ -6738,10 +6937,9 @@ "dev": true }, "prepend-http": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", - "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=", - "dev": true + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", + "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=" }, "preserve": { "version": "0.2.0", @@ -6836,17 +7034,15 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, "requires": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", - "dev": true + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" }, "qs": { "version": "6.5.2", @@ -6854,6 +7050,11 @@ "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", "dev": true }, + "querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" + }, "randomatic": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/randomatic/-/randomatic-3.1.1.tgz", @@ -7432,6 +7633,14 @@ "tough-cookie": "~2.4.3", "tunnel-agent": "^0.6.0", "uuid": "^3.3.2" + }, + "dependencies": { + "uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==", + "dev": true + } } }, "request-promise-core": { @@ -7523,6 +7732,14 @@ "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", "dev": true }, + "responselike": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz", + "integrity": "sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec=", + "requires": { + "lowercase-keys": "^1.0.0" + } + }, "restore-cursor": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", @@ -7914,10 +8131,9 @@ } }, "sax": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", - "dev": true + "version": "1.2.1", + "resolved": "http://registry.npmjs.org/sax/-/sax-1.2.1.tgz", + "integrity": "sha1-e45lYZCyKOgaZq6nSEgNgozS03o=" }, "semver": { "version": "5.6.0", @@ -8293,9 +8509,9 @@ } }, "stack-utils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-1.0.1.tgz", - "integrity": "sha1-1PM6tU6OOHeLDKXP07OvsS22hiA=", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-1.0.2.tgz", + "integrity": "sha512-MTX+MeG5U994cazkjd/9KNAapsHnibjMLnfXodlkXw76JEea0UiNzrqidzo1emMwk7w5Qhc9jd4Bn9TBb1MFwA==", "dev": true }, "staged-git-files": { @@ -8452,8 +8668,7 @@ "symbol-observable": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", - "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==", - "dev": true + "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==" }, "symbol-tree": { "version": "3.2.2", @@ -8471,32 +8686,6 @@ "lodash": "^4.17.10", "slice-ansi": "1.0.0", "string-width": "^2.1.1" - }, - "dependencies": { - "ajv": { - "version": "6.5.5", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.5.5.tgz", - "integrity": "sha512-7q7gtRQDJSyuEHjuVgHoUa2VuemFiCMrfQc9Tc08XTAc4Zj/5U1buQJ0HU6i7fKjXU09SVgSmxa4sLvuvS8Iyg==", - "dev": true, - "requires": { - "fast-deep-equal": "^2.0.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "fast-deep-equal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", - "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", - "dev": true - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - } } }, "table-layout": { @@ -8547,6 +8736,12 @@ "signal-exit": "^3.0.0", "strip-eof": "^1.0.0" } + }, + "get-stream": { + "version": "3.0.0", + "resolved": "http://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", + "dev": true } } }, @@ -8648,12 +8843,12 @@ "dev": true }, "through2": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.3.tgz", - "integrity": "sha1-AARWmzfHx0ujnEPzzteNGtlBQL4=", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", "dev": true, "requires": { - "readable-stream": "^2.1.5", + "readable-stream": "~2.3.6", "xtend": "~4.0.1" } }, @@ -8693,6 +8888,11 @@ "kind-of": "^3.0.2" } }, + "to-readable-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz", + "integrity": "sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==" + }, "to-regex": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", @@ -8743,6 +8943,14 @@ "requires": { "psl": "^1.1.24", "punycode": "^1.4.1" + }, + "dependencies": { + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", + "dev": true + } } }, "tr46": { @@ -9008,13 +9216,21 @@ "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", "dev": true }, + "url": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", + "integrity": "sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ=", + "requires": { + "punycode": "1.3.2", + "querystring": "0.2.0" + } + }, "url-parse-lax": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz", - "integrity": "sha1-evjzA2Rem9eaJy56FKxovAYJ2nM=", - "dev": true, + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz", + "integrity": "sha1-FrXK/Afb42dsGxmZF3gj1lA6yww=", "requires": { - "prepend-http": "^1.0.1" + "prepend-http": "^2.0.0" } }, "urlgrey": { @@ -9046,10 +9262,9 @@ } }, "uuid": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", - "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==", - "dev": true + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.1.0.tgz", + "integrity": "sha512-DIWtzUkw04M4k3bf1IcpS2tngXEL26YUD2M0tMDUpnUrz2hgzUBlD55a4FjdLGPvfHxS6uluGWvaVEqgBcVa+g==" }, "validate-npm-package-license": { "version": "3.0.4", @@ -9236,8 +9451,7 @@ "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "write": { "version": "0.2.1", @@ -9286,6 +9500,20 @@ "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==", "dev": true }, + "xml2js": { + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz", + "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==", + "requires": { + "sax": ">=0.6.0", + "xmlbuilder": "~9.0.1" + } + }, + "xmlbuilder": { + "version": "9.0.7", + "resolved": "http://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", + "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=" + }, "xmlcreate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-1.0.2.tgz", diff --git a/package.json b/package.json index 6b3fdb79..17843219 100644 --- a/package.json +++ b/package.json @@ -17,22 +17,28 @@ "license": "MIT", "main": "lib/index.js", "engines": { - "node": ">=8.6.0" + "node": ">=10.0.0" }, "scripts": { "build-docs": "mkdir -p docs && jsdoc2md --no-gfm lib/*.js > docs/API.md && git add docs/API.md", "build-docs-watch": "npm-watch build-docs", "eslint": "eslint . --ext .js,.json --cache", - "format": "prettier --write '**/*.{eslintrc,md,js,json}' '!reports/*'", - "lint": "eslint . --ext .js,.json --cache -f checkstyle > ./reports/checkstyle.xml", - "precommit": "npm run build-docs && lint-staged", - "prelint": "rm -f ./reports/checkstyle.xml && mkdir -p ./reports", + "format": "prettier --write '**/*.{md,js,json}' '!reports/**/*.{js,json}'", + "jest": "JEST_JUNIT_OUTPUT=./reports/junit/js-test-results.xml jest", + "jest-watch": "npm run jest -- --watch --coverageReporters=html", + "lint": "eslint . --ext .js,.json --format junit -o ./reports/junit/js-lint-results.xml", "prepare": "check-engines", - "pretest": "mkdir -p ./reports", - "test": "TEST_REPORT_PATH=./reports jest && codecov", - "test-watch": "npm run test -- --watch --coverageReporters=html", + "test": "npm run jest -- --ci --runInBand && codecov", "version": "auto-changelog -p && git add CHANGELOG.md" }, + "dependencies": { + "aws-sdk": "^2.359.0", + "aws4": "^1.8.0", + "fast-deep-equal": "^2.0.1", + "got": "^9.3.2", + "lifion-aws-event-stream": "^1.0.1", + "lzutf8": "^0.5.5" + }, "devDependencies": { "auto-changelog": "^1.10.2", "chalk": "^2.4.1", @@ -42,7 +48,7 @@ "eslint-config-lifion": "^1.0.3", "husky": "^1.1.4", "jest": "^23.6.0", - "jest-junit-reporter": "^1.1.0", + "jest-junit": "^5.2.0", "jsdoc-to-markdown": "^4.0.1", "lint-staged": "^8.0.5", "npm-watch": "^0.4.0", @@ -50,17 +56,23 @@ "semver": "^5.6.0" }, "auto-changelog": { - "commitLimit": false + "commitLimit": false, + "template": "./templates/CHANGELOG.hbs" }, "eslintConfig": { "extends": "lifion" }, + "husky": { + "hooks": { + "pre-commit": "npm run build-docs && lint-staged" + } + }, "jest": { "collectCoverage": true, "collectCoverageFrom": [ "**/*.js" ], - "coverageDirectory": "../reports", + "coverageDirectory": "../reports/coverage", "coverageThreshold": { "global": { "statements": 0, @@ -69,9 +81,12 @@ "lines": 0 } }, + "reporters": [ + "default", + "jest-junit" + ], "rootDir": "lib", - "testEnvironment": "node", - "testResultsProcessor": "jest-junit-reporter" + "testEnvironment": "node" }, "lint-staged": { "*.js": [ @@ -88,10 +103,13 @@ "singleQuote": true }, "renovate": { + "branchPrefix": "feature/renovate-", + "engines": { + "enabled": false + }, "extends": [ "config:base" ], - "branchPrefix": "feature/renovate-", "rangeStrategy": "bump" }, "watch": { diff --git a/scripts/.eslintrc b/scripts/.eslintrc deleted file mode 100644 index c0c02ad5..00000000 --- a/scripts/.eslintrc +++ /dev/null @@ -1,6 +0,0 @@ -{ - "rules": { - "no-console": "off", - "node/shebang": "off" - } -} diff --git a/scripts/check-dependencies.js b/scripts/check-dependencies.js deleted file mode 100755 index 5b6ab950..00000000 --- a/scripts/check-dependencies.js +++ /dev/null @@ -1,93 +0,0 @@ -#!/usr/bin/env node - -'use strict'; - -/* eslint-disable global-require, import/no-dynamic-require, no-process-exit */ -/* eslint-disable security/detect-child-process, security/detect-non-literal-require */ - -const chalk = require('chalk'); -const path = require('path'); -const semver = require('semver'); -const { exec } = require('child_process'); -const { promisify } = require('util'); - -const { dependencies, devDependencies } = require(path.join(process.cwd(), 'package.json')); - -const execAsync = promisify(exec); -const pkgs = []; - -async function getLatestVersions(name) { - const { stdout } = await execAsync(`npm view ${name} versions --json`); - try { - return JSON.parse(stdout); - } catch (err) { - return []; - } -} - -async function getLatestVersion(name, wanted) { - const versions = await getLatestVersions(name); - const applicableVersions = versions.filter(i => semver.satisfies(i, wanted)); - applicableVersions.sort((a, b) => semver.rcompare(a, b)); - return applicableVersions[0]; -} - -function getInstalledVersion(name) { - try { - return require(path.join(process.cwd(), 'node_modules', name, 'package.json')).version; - } catch (err) { - return null; - } -} - -function pushPkgs(deps = {}, type) { - return Object.keys(deps).map(async name => { - let wanted = deps[name]; - if (!wanted.startsWith('^')) wanted = `^${wanted}`; - const installed = getInstalledVersion(name); - const latest = await getLatestVersion(name, wanted); - const wantedFixed = wanted.startsWith('^') ? wanted.substr(1) : wanted; - const shouldBeInstalled = - installed === null || wantedFixed !== installed || installed !== latest; - if (shouldBeInstalled) { - const warning = - installed !== null - ? `outdated: ${chalk.red( - wantedFixed !== installed ? wantedFixed : installed - )} → ${chalk.green(latest)}` - : chalk.red('not installed'); - console.log(`${chalk.red(name)} is ${warning}`); - } - pkgs.push({ name, wanted, installed, type, latest, shouldBeInstalled }); - }); -} - -function getPkgIds(filteredPkgs) { - return filteredPkgs.map(({ name, latest }) => `${name}@${latest}`).join(' '); -} - -async function run() { - console.log(chalk.blue('Checking NPM module versions…\n')); - await Promise.all([...pushPkgs(dependencies, 'prod'), ...pushPkgs(devDependencies, 'dev')]); - const toInstall = pkgs.filter(({ shouldBeInstalled }) => shouldBeInstalled); - if (toInstall.length > 0) { - console.log(`\n${chalk.bold('To resolve this, run:')}`); - const prodPkgs = toInstall.filter(({ type }) => type === 'prod'); - if (prodPkgs.length > 0) { - console.log(`npm i ${getPkgIds(prodPkgs)}`); - } - const devPkgs = toInstall.filter(({ type }) => type === 'dev'); - if (devPkgs.length > 0) { - console.log(`npm i -D ${getPkgIds(devPkgs)}`); - } - if (prodPkgs.length > 0 || devPkgs.length > 0) { - console.log(); - } - process.exit(1); - } else { - console.log(chalk.green('All NPM modules are up to date.')); - process.exit(0); - } -} - -run(); diff --git a/templates/CHANGELOG.hbs b/templates/CHANGELOG.hbs new file mode 100644 index 00000000..05fdc588 --- /dev/null +++ b/templates/CHANGELOG.hbs @@ -0,0 +1,23 @@ +## Changelog 🚀 + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) +and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). + +Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog). + +{{#each releases}} + ### {{title}} ({{isoDate}}) + + {{#each merges}} + - {{#if href}}[`#{{id}}`]({{href}}): {{/if}}{{message}} + {{/each}} + {{#each fixes}} + - {{commit.subject}}{{#each fixes}}{{#if href}} [`#{{id}}`]({{href}}){{/if}}{{/each}} + {{/each}} + {{#each commits}} + - {{#if href}}[`{{shorthash}}`]({{href}}): {{/if}}{{subject}} + {{/each}} + +{{/each}}