diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 000000000..5bc8a6877 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,5073 @@ +{ + "requires": true, + "lockfileVersion": 1, + "dependencies": { + "@lerna/add": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@lerna/add/-/add-3.2.0.tgz", + "integrity": "sha512-qGA7agAWcKlrXZR3FwFJXTr26Q2rqjOVMNhtm8uyawImqfdKp4WJXuGdioiWOSW20jMvzLIFhWZh5lCh0UyMBw==", + "requires": { + "@lerna/bootstrap": "^3.2.0", + "@lerna/command": "^3.1.3", + "@lerna/filter-options": "^3.1.2", + "@lerna/npm-conf": "^3.0.0", + "@lerna/validation-error": "^3.0.0", + "dedent": "^0.7.0", + "npm-package-arg": "^6.0.0", + "p-map": "^1.2.0", + "pacote": "^9.1.0", + "semver": "^5.5.0" + } + }, + "@lerna/batch-packages": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@lerna/batch-packages/-/batch-packages-3.1.2.tgz", + "integrity": "sha512-HAkpptrYeUVlBYbLScXgeCgk6BsNVXxDd53HVWgzzTWpXV4MHpbpeKrByyt7viXlNhW0w73jJbipb/QlFsHIhQ==", + "requires": { + "@lerna/package-graph": "^3.1.2", + "@lerna/validation-error": "^3.0.0", + "npmlog": "^4.1.2" + } + }, + "@lerna/bootstrap": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@lerna/bootstrap/-/bootstrap-3.2.0.tgz", + "integrity": "sha512-xh6dPpdzsAEWF7lqASaym5AThkmP3ArR7Q+P/tiPWCT+OT7QT5QI2IQAz1aAYEBQL3ACzpE6kq+VOGi0m+9bxw==", + "requires": { + "@lerna/batch-packages": "^3.1.2", + "@lerna/command": "^3.1.3", + "@lerna/filter-options": "^3.1.2", + "@lerna/has-npm-version": "^3.0.4", + "@lerna/npm-conf": "^3.0.0", + "@lerna/npm-install": "^3.0.0", + "@lerna/rimraf-dir": "^3.0.0", + "@lerna/run-lifecycle": "^3.2.0", + "@lerna/run-parallel-batches": "^3.0.0", + "@lerna/symlink-binary": "^3.1.4", + "@lerna/symlink-dependencies": "^3.1.4", + "@lerna/validation-error": "^3.0.0", + "dedent": "^0.7.0", + "get-port": "^3.2.0", + "multimatch": "^2.1.0", + "npm-package-arg": "^6.0.0", + "npmlog": "^4.1.2", + "p-finally": "^1.0.0", + "p-map": "^1.2.0", + "p-map-series": "^1.0.0", + "p-waterfall": "^1.0.0", + "read-package-tree": "^5.1.6", + "semver": "^5.5.0" + } + }, + "@lerna/changed": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@lerna/changed/-/changed-3.2.0.tgz", + "integrity": "sha512-R+vGzzXPN5s5lJT0v1zSTLw43O2ek2yekqCqvw7p9UFqgqYSbxUsyWXMdhku/mOIFWTc6DzrsOi+U7CX3TXmHg==", + "requires": { + "@lerna/collect-updates": "^3.1.0", + "@lerna/command": "^3.1.3", + "@lerna/listable": "^3.0.0", + "@lerna/output": "^3.0.0", + "@lerna/version": "^3.2.0" + } + }, + "@lerna/check-working-tree": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@lerna/check-working-tree/-/check-working-tree-3.1.0.tgz", + "integrity": "sha512-ruy6s44IUcaPEa4JlDDDk6nbacMESUPRSb+dohzLJxfhXx1wFnEVF6L91TGxFP+C0lt5V6zd8rnJxkW/uZzNAA==", + "requires": { + "@lerna/describe-ref": "^3.1.0", + "@lerna/validation-error": "^3.0.0" + } + }, + "@lerna/child-process": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@lerna/child-process/-/child-process-3.0.0.tgz", + "integrity": "sha512-8vHRDkpGhzSaMsXgyXVgY80mUSC5WSkDmhWWA3bnB/n5FBK1gK8EKQUpHTk14SckwvUgEJzBd35gR5/XKGOgmQ==", + "requires": { + "chalk": "^2.3.1", + "execa": "^0.10.0", + "strong-log-transformer": "^1.0.6" + } + }, + "@lerna/clean": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@lerna/clean/-/clean-3.1.3.tgz", + "integrity": "sha512-XVdcIOjhudXlk5pTXjrpsnNLqeVi2rBu2oWzPH2GHrxWGBZBW8thGIFhQf09da/RbRT3uzBWXpUv+sbL2vbX3g==", + "requires": { + "@lerna/command": "^3.1.3", + "@lerna/filter-options": "^3.1.2", + "@lerna/prompt": "^3.0.0", + "@lerna/rimraf-dir": "^3.0.0", + "p-map": "^1.2.0", + "p-map-series": "^1.0.0", + "p-waterfall": "^1.0.0" + } + }, + "@lerna/cli": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@lerna/cli/-/cli-3.2.0.tgz", + "integrity": "sha512-JdbLyTxHqxUlrkI+Ke+ltXbtyA+MPu9zR6kg/n8Fl6uaez/2fZWtReXzYi8MgLxfUFa7+1OHWJv4eAMZlByJ+Q==", + "requires": { + "@lerna/global-options": "^3.1.3", + "dedent": "^0.7.0", + "npmlog": "^4.1.2", + "yargs": "^12.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" + }, + "cliui": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", + "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==", + "requires": { + "string-width": "^2.1.1", + "strip-ansi": "^4.0.0", + "wrap-ansi": "^2.0.0" + } + }, + "decamelize": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-2.0.0.tgz", + "integrity": "sha512-Ikpp5scV3MSYxY39ymh45ZLEecsTdv/Xj2CaQfI8RLMuwi7XvjX9H/fhraiSuU+C5w5NTDu4ZU72xNiZnurBPg==", + "requires": { + "xregexp": "4.0.0" + } + }, + "find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "requires": { + "locate-path": "^3.0.0" + } + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" + }, + "locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "p-limit": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.0.0.tgz", + "integrity": "sha512-fl5s52lI5ahKCernzzIyAP0QAZbGIovtVHGwpcu1Jr/EpzLVDI2myISHwGqK7m8uQFugVWSrbxH7XnhGtvEc+A==", + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-try": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.0.0.tgz", + "integrity": "sha512-hMp0onDKIajHfIkdRk3P4CdCmErkYAxxDtP3Wx/4nZ3aGlau2VKh3mZpcuFkH27WQkL/3WBCPOktzA9ZOAnMQQ==" + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "requires": { + "ansi-regex": "^3.0.0" + } + }, + "yargs": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.1.tgz", + "integrity": "sha512-B0vRAp1hRX4jgIOWFtjfNjd9OA9RWYZ6tqGA9/I/IrTMsxmKvtWy+ersM+jzpQqbC3YfLzeABPdeTgcJ9eu1qQ==", + "requires": { + "cliui": "^4.0.0", + "decamelize": "^2.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^1.0.1", + "os-locale": "^2.0.0", + "require-directory": "^2.1.1", + "require-main-filename": "^1.0.1", + "set-blocking": "^2.0.0", + "string-width": "^2.0.0", + "which-module": "^2.0.0", + "y18n": "^3.2.1 || ^4.0.0", + "yargs-parser": "^10.1.0" + } + } + } + }, + "@lerna/collect-updates": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@lerna/collect-updates/-/collect-updates-3.1.0.tgz", + "integrity": "sha512-zHxHRZOteTqcW9mAyLrmoWEKpfxgA3c6LJj4nauB2pM3MKyKNhg0gqiy2RHFu7EGivPki4Q1624I301iAXtUVA==", + "requires": { + "@lerna/child-process": "^3.0.0", + "@lerna/describe-ref": "^3.1.0", + "minimatch": "^3.0.4", + "npmlog": "^4.1.2", + "slash": "^1.0.0" + } + }, + "@lerna/command": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@lerna/command/-/command-3.1.3.tgz", + "integrity": "sha512-ptaFNsfcTpxnSkaNrGgW3fRbWWVSVDUx4BpkjUUnRTgy9mwoykQWgQB3inhgTYHwW6e4PzO79F2hovfUMzHuzg==", + "requires": { + "@lerna/child-process": "^3.0.0", + "@lerna/package-graph": "^3.1.2", + "@lerna/project": "^3.0.0", + "@lerna/validation-error": "^3.0.0", + "@lerna/write-log-file": "^3.0.0", + "dedent": "^0.7.0", + "execa": "^0.10.0", + "is-ci": "^1.0.10", + "lodash": "^4.17.5", + "npmlog": "^4.1.2" + } + }, + "@lerna/conventional-commits": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@lerna/conventional-commits/-/conventional-commits-3.0.2.tgz", + "integrity": "sha512-Cxd1eWXn3usADKXIUvYmTERx2+1N7oJD4Whz+FVu8kTfufsfTO7fYOan1RVkg86ukZbNDyS+iOxZ8DJ2JspS9g==", + "requires": { + "@lerna/validation-error": "^3.0.0", + "conventional-changelog-angular": "^1.6.6", + "conventional-changelog-core": "^2.0.5", + "conventional-recommended-bump": "^2.0.6", + "dedent": "^0.7.0", + "fs-extra": "^6.0.1", + "get-stream": "^3.0.0", + "npm-package-arg": "^6.0.0", + "npmlog": "^4.1.2", + "semver": "^5.5.0" + } + }, + "@lerna/create": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@lerna/create/-/create-3.1.3.tgz", + "integrity": "sha512-CmXKCBc6AE3F9O6mMg4Y76cQ8eTCy3ksqDFKxVQMdYDtHKnTrH1s0l8sn3J1AiylqVDnvxhb3Rjyj+OWyzmFPQ==", + "requires": { + "@lerna/child-process": "^3.0.0", + "@lerna/command": "^3.1.3", + "@lerna/npm-conf": "^3.0.0", + "@lerna/validation-error": "^3.0.0", + "camelcase": "^4.1.0", + "dedent": "^0.7.0", + "fs-extra": "^6.0.1", + "globby": "^8.0.1", + "init-package-json": "^1.10.3", + "npm-package-arg": "^6.0.0", + "pify": "^3.0.0", + "semver": "^5.5.0", + "slash": "^1.0.0", + "validate-npm-package-license": "^3.0.3", + "validate-npm-package-name": "^3.0.0", + "whatwg-url": "^6.5.0" + }, + "dependencies": { + "camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=" + } + } + }, + "@lerna/create-symlink": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@lerna/create-symlink/-/create-symlink-3.0.0.tgz", + "integrity": "sha512-Q9qAzGGqQtVzHWrCz+Md4SH0tW99DrgFJ68cnFqilOO6H3Y/y/H0gwHICqM9YxRwLs6GJdkzoqJATFShM7PKJA==", + "requires": { + "cmd-shim": "^2.0.2", + "fs-extra": "^6.0.1", + "npmlog": "^4.1.2" + } + }, + "@lerna/describe-ref": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@lerna/describe-ref/-/describe-ref-3.1.0.tgz", + "integrity": "sha512-0a7WFKDSmdEwwmEj+ZfhI7SkkG1CDcVhfW8VhPqr6gnCMY+ryt6iV/rR7ygb0eCDg8wEe9eQsiwbnrbXDLjIDw==", + "requires": { + "@lerna/child-process": "^3.0.0", + "npmlog": "^4.1.2" + } + }, + "@lerna/diff": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@lerna/diff/-/diff-3.1.3.tgz", + "integrity": "sha512-30G74DxdQC4dR3U0yqh5mjcioLDUmSy1ntntdF3khvKV9oiMVzCSOO0oOlSwIdmohke+bQ//oF+oyl0Dy1TUTQ==", + "requires": { + "@lerna/child-process": "^3.0.0", + "@lerna/command": "^3.1.3", + "@lerna/validation-error": "^3.0.0", + "npmlog": "^4.1.2" + } + }, + "@lerna/exec": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@lerna/exec/-/exec-3.1.3.tgz", + "integrity": "sha512-r0yQj9Rza5a42Shts8rXYGU1/Va8hO2atk/TEZgrA1EcTwgZyAuXsuML5UWbC/eLgwEjVDmc3MUSj8O1JijBMQ==", + "requires": { + "@lerna/batch-packages": "^3.1.2", + "@lerna/child-process": "^3.0.0", + "@lerna/command": "^3.1.3", + "@lerna/filter-options": "^3.1.2", + "@lerna/run-parallel-batches": "^3.0.0", + "@lerna/validation-error": "^3.0.0" + } + }, + "@lerna/filter-options": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@lerna/filter-options/-/filter-options-3.1.2.tgz", + "integrity": "sha512-smbvSGK/eU+7PDKO4jbJ7XYO2XTfhnwPeOTuwSm1mlWS5dUGasYkhAuFzouFh60aZBvmW0e6APe0XYeofQNcCg==", + "requires": { + "@lerna/collect-updates": "^3.1.0", + "@lerna/filter-packages": "^3.0.0", + "dedent": "^0.7.0" + } + }, + "@lerna/filter-packages": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@lerna/filter-packages/-/filter-packages-3.0.0.tgz", + "integrity": "sha512-zwbY1J4uRjWRZ/FgYbtVkq7I3Nduwsg2V2HwLKSzwV2vPglfGqgovYOVkND6/xqe2BHwDX4IyA2+e7OJmLaLSA==", + "requires": { + "@lerna/validation-error": "^3.0.0", + "multimatch": "^2.1.0", + "npmlog": "^4.1.2" + } + }, + "@lerna/get-npm-exec-opts": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@lerna/get-npm-exec-opts/-/get-npm-exec-opts-3.0.0.tgz", + "integrity": "sha512-arcYUm+4xS8J3Palhl+5rRJXnZnFHsLFKHBxznkPIxjwGQeAEw7df38uHdVjEQ+HNeFmHnBgSqfbxl1VIw5DHg==", + "requires": { + "npmlog": "^4.1.2" + } + }, + "@lerna/global-options": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@lerna/global-options/-/global-options-3.1.3.tgz", + "integrity": "sha512-LVeZU/Zgc0XkHdGMRYn+EmHfDmmYNwYRv3ta59iCVFXLVp7FRFWF7oB1ss/WRa9x/pYU0o6L8as/5DomLUGASA==" + }, + "@lerna/has-npm-version": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@lerna/has-npm-version/-/has-npm-version-3.0.4.tgz", + "integrity": "sha512-RisEWZBROi8corPb/PUIQqT+xGPLeriJ/n6VCeO6GROCO1fyYBX7kgFmVpFOytufWFkI04qBgLaUs+CEc8Yspg==", + "requires": { + "@lerna/child-process": "^3.0.0", + "semver": "^5.5.0" + } + }, + "@lerna/import": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@lerna/import/-/import-3.1.3.tgz", + "integrity": "sha512-+XV/EHXEHbyMmprz8zQa0VF0TZ+txRIrcF3Q/PsuZ4isVxawIbP1CmgE0yn0/1XSNJwGKsuPfGauRtnjUi2LJw==", + "requires": { + "@lerna/child-process": "^3.0.0", + "@lerna/command": "^3.1.3", + "@lerna/prompt": "^3.0.0", + "@lerna/validation-error": "^3.0.0", + "dedent": "^0.7.0", + "fs-extra": "^6.0.1", + "p-map-series": "^1.0.0" + } + }, + "@lerna/init": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@lerna/init/-/init-3.1.3.tgz", + "integrity": "sha512-c418p6fAfJ+b/tidB8/O/ABGLX7a5y5uJSWxH2/Mp7i5da/RD27XJ6E6818hGAbUbVQw04+XuXHtrWYlWLEJCw==", + "requires": { + "@lerna/child-process": "^3.0.0", + "@lerna/command": "^3.1.3", + "fs-extra": "^6.0.1", + "p-map": "^1.2.0", + "write-json-file": "^2.3.0" + } + }, + "@lerna/link": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@lerna/link/-/link-3.1.4.tgz", + "integrity": "sha512-AAl4ctKtE6975zxdrsr16CAh/K4y0ZjjY9XDdD+zMxsSPELvRVG6M36O1A72AmKz5Nhh1l82bPrw7w54TbQ8hA==", + "requires": { + "@lerna/command": "^3.1.3", + "@lerna/package-graph": "^3.1.2", + "@lerna/symlink-dependencies": "^3.1.4", + "p-map": "^1.2.0", + "slash": "^1.0.0" + } + }, + "@lerna/list": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@lerna/list/-/list-3.1.3.tgz", + "integrity": "sha512-/ncX5Kj1ddLgZuBkjaoluJgcmAAm/L4/AymVNBgVrw6dOad0C0RB6oIcRAbxDennEQ25wSOFmuXRZHYHY9VYyg==", + "requires": { + "@lerna/command": "^3.1.3", + "@lerna/filter-options": "^3.1.2", + "@lerna/listable": "^3.0.0", + "@lerna/output": "^3.0.0" + } + }, + "@lerna/listable": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@lerna/listable/-/listable-3.0.0.tgz", + "integrity": "sha512-HX/9hyx1HLg2kpiKXIUc1EimlkK1T58aKQ7ovO7rQdTx9ForpefoMzyLnHE1n4XrUtEszcSWJIICJ/F898M6Ag==", + "requires": { + "chalk": "^2.3.1", + "columnify": "^1.5.4" + } + }, + "@lerna/log-packed": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@lerna/log-packed/-/log-packed-3.0.4.tgz", + "integrity": "sha512-vVQHgMagE2wnbxhNY9nFkdu+Cx2TsyWalkJfkxbNzmo6gOCrDsxCBDj9vTEV8Q+4aWx0C0Bsc0sB2Eb8y/+ofA==", + "requires": { + "byte-size": "^4.0.3", + "columnify": "^1.5.4", + "has-unicode": "^2.0.1", + "npmlog": "^4.1.2" + } + }, + "@lerna/npm-conf": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@lerna/npm-conf/-/npm-conf-3.0.0.tgz", + "integrity": "sha512-xXG7qt349t+xzaHTQELmIDjbq8Q49HOMR8Nx/gTDBkMl02Fno91LXFnA4A7ErPiyUSGqNSfLw+zgij0hgpeN7w==", + "requires": { + "config-chain": "^1.1.11", + "pify": "^3.0.0" + } + }, + "@lerna/npm-dist-tag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@lerna/npm-dist-tag/-/npm-dist-tag-3.0.0.tgz", + "integrity": "sha512-ZOcfcsNJlCoVHvLOROdCTvqD3keG3TJ78Cu8sALsz8n0kEz2ga7tNy5wbQ67SGyY7+jq4YiBv5BwXjV+56Sv+A==", + "requires": { + "@lerna/child-process": "^3.0.0", + "@lerna/get-npm-exec-opts": "^3.0.0", + "npmlog": "^4.1.2" + } + }, + "@lerna/npm-install": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@lerna/npm-install/-/npm-install-3.0.0.tgz", + "integrity": "sha512-e0sspVUfzEKhqsRIxzWqZ/uMBHzZSzOa4HCeORErEZu+dmDoI145XYhqvCVn7EvbAb407FV2H9GVeoP0JeG8GQ==", + "requires": { + "@lerna/child-process": "^3.0.0", + "@lerna/get-npm-exec-opts": "^3.0.0", + "fs-extra": "^6.0.1", + "npm-package-arg": "^6.0.0", + "npmlog": "^4.1.2", + "signal-exit": "^3.0.2", + "write-pkg": "^3.1.0" + } + }, + "@lerna/npm-publish": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@lerna/npm-publish/-/npm-publish-3.2.0.tgz", + "integrity": "sha512-x13EGrjZk9w8gCQAE44aKbeO1xhLizLJ4tKjzZmQqKEaUCugF4UU8ZRGshPMRFBdsHTEWh05dkKx2oPMoaf0dw==", + "requires": { + "@lerna/child-process": "^3.0.0", + "@lerna/get-npm-exec-opts": "^3.0.0", + "@lerna/has-npm-version": "^3.0.4", + "@lerna/log-packed": "^3.0.4", + "fs-extra": "^6.0.1", + "npmlog": "^4.1.2", + "p-map": "^1.2.0" + } + }, + "@lerna/npm-run-script": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@lerna/npm-run-script/-/npm-run-script-3.0.0.tgz", + "integrity": "sha512-Y1H4Myer1S7an33FDK0eqyR+95PujUePC/xJZKq/H50SaQNwBw7KMlxXxy6kXVEcQhmvQsER4Bw3msgqwwGYIw==", + "requires": { + "@lerna/child-process": "^3.0.0", + "@lerna/get-npm-exec-opts": "^3.0.0", + "npmlog": "^4.1.2" + } + }, + "@lerna/output": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@lerna/output/-/output-3.0.0.tgz", + "integrity": "sha512-EFxnSbO0zDEVKkTKpoCUAFcZjc3gn3DwPlyTDxbeqPU7neCfxP4rA4+0a6pcOfTlRS5kLBRMx79F2TRCaMM3DA==", + "requires": { + "npmlog": "^4.1.2" + } + }, + "@lerna/package": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@lerna/package/-/package-3.0.0.tgz", + "integrity": "sha512-djzEJxzn212wS8d9znBnlXkeRlPL7GqeAYBykAmsuq51YGvaQK67Umh5ejdO0uxexF/4r7yRwgrlRHpQs8Rfqg==", + "requires": { + "npm-package-arg": "^6.0.0", + "write-pkg": "^3.1.0" + } + }, + "@lerna/package-graph": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@lerna/package-graph/-/package-graph-3.1.2.tgz", + "integrity": "sha512-9wIWb49I1IJmyjPdEVZQ13IAi9biGfH/OZHOC04U2zXGA0GLiY+B3CAx6FQvqkZ8xEGfqzmXnv3LvZ0bQfc1aQ==", + "requires": { + "@lerna/validation-error": "^3.0.0", + "npm-package-arg": "^6.0.0", + "semver": "^5.5.0" + } + }, + "@lerna/project": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@lerna/project/-/project-3.0.0.tgz", + "integrity": "sha512-XhDFVfqj79jG2Speggd15RpYaE8uiR25UKcQBDmumbmqvTS7xf2cvl2pq2UTvDafaJ0YwFF3xkxQZeZnFMwdkw==", + "requires": { + "@lerna/package": "^3.0.0", + "@lerna/validation-error": "^3.0.0", + "cosmiconfig": "^5.0.2", + "dedent": "^0.7.0", + "dot-prop": "^4.2.0", + "glob-parent": "^3.1.0", + "globby": "^8.0.1", + "load-json-file": "^4.0.0", + "npmlog": "^4.1.2", + "p-map": "^1.2.0", + "resolve-from": "^4.0.0", + "write-json-file": "^2.3.0" + } + }, + "@lerna/prompt": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@lerna/prompt/-/prompt-3.0.0.tgz", + "integrity": "sha512-EzvNexDTh//GlpOz68zRo16NdOIqWqiiXMs9tIxpELQubH+kUGKvBSiBrZ2Zyrfd8pQhIf+8qARtkCG+G7wzQQ==", + "requires": { + "inquirer": "^5.1.0", + "npmlog": "^4.1.2" + } + }, + "@lerna/publish": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@lerna/publish/-/publish-3.2.1.tgz", + "integrity": "sha512-SnSBstK/G9qLt5rS56pihNacgsu3UgxXiCexWb57GGEp2eDguQ7rFzxVs4JMQQWmVG97EMJQxfFV54tW2sqtIw==", + "requires": { + "@lerna/batch-packages": "^3.1.2", + "@lerna/check-working-tree": "^3.1.0", + "@lerna/child-process": "^3.0.0", + "@lerna/collect-updates": "^3.1.0", + "@lerna/command": "^3.1.3", + "@lerna/describe-ref": "^3.1.0", + "@lerna/get-npm-exec-opts": "^3.0.0", + "@lerna/npm-dist-tag": "^3.0.0", + "@lerna/npm-publish": "^3.2.0", + "@lerna/output": "^3.0.0", + "@lerna/prompt": "^3.0.0", + "@lerna/run-lifecycle": "^3.2.0", + "@lerna/run-parallel-batches": "^3.0.0", + "@lerna/validation-error": "^3.0.0", + "@lerna/version": "^3.2.0", + "fs-extra": "^6.0.1", + "npm-package-arg": "^6.0.0", + "npmlog": "^4.1.2", + "p-finally": "^1.0.0", + "p-map": "^1.2.0", + "p-pipe": "^1.2.0", + "p-reduce": "^1.0.0", + "semver": "^5.5.0" + } + }, + "@lerna/resolve-symlink": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@lerna/resolve-symlink/-/resolve-symlink-3.0.0.tgz", + "integrity": "sha512-MqjW9e+QVXts5IK5dk1XnYx7JKb+g+tQkOnnpAxYWHjahf3rGJ7Ru8maWh8KoPE+nIHAekk4WcjpiA9nLKvkFQ==", + "requires": { + "fs-extra": "^6.0.1", + "npmlog": "^4.1.2", + "read-cmd-shim": "^1.0.1" + } + }, + "@lerna/rimraf-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@lerna/rimraf-dir/-/rimraf-dir-3.0.0.tgz", + "integrity": "sha512-epvh/RGWSOYdrNgrizMcRq9VyCHkeY0LpIE4074r4ouKdYNhBT0LlpT0yMLvQgQKJkKRlqcfhJHvZeGHhXQyGg==", + "requires": { + "@lerna/child-process": "^3.0.0", + "npmlog": "^4.1.2", + "path-exists": "^3.0.0", + "rimraf": "^2.6.2" + } + }, + "@lerna/run": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@lerna/run/-/run-3.1.3.tgz", + "integrity": "sha512-O26WdR+sQFSG2Fpc67nw+m8oVq3R+H6jsscKuB6VJafU+V4/hPURSbuFZIcmnD9MLmzAIhlQiCf0Fy6s/1MPPA==", + "requires": { + "@lerna/batch-packages": "^3.1.2", + "@lerna/command": "^3.1.3", + "@lerna/filter-options": "^3.1.2", + "@lerna/npm-run-script": "^3.0.0", + "@lerna/output": "^3.0.0", + "@lerna/run-parallel-batches": "^3.0.0", + "@lerna/validation-error": "^3.0.0", + "p-map": "^1.2.0" + } + }, + "@lerna/run-lifecycle": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@lerna/run-lifecycle/-/run-lifecycle-3.2.0.tgz", + "integrity": "sha512-kGGdHJRyeZF+VTtal1DBptg6qwIsOLg3pKtmRm1rCMNN7j4kgrA9L07ZoRar8LjQXvfuheB1LSKHd5d04pr4Tg==", + "requires": { + "@lerna/npm-conf": "^3.0.0", + "npm-lifecycle": "^2.0.0", + "npmlog": "^4.1.2" + } + }, + "@lerna/run-parallel-batches": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@lerna/run-parallel-batches/-/run-parallel-batches-3.0.0.tgz", + "integrity": "sha512-Mj1ravlXF7AkkewKd9YFq9BtVrsStNrvVLedD/b2wIVbNqcxp8lS68vehXVOzoL/VWNEDotvqCQtyDBilCodGw==", + "requires": { + "p-map": "^1.2.0", + "p-map-series": "^1.0.0" + } + }, + "@lerna/symlink-binary": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@lerna/symlink-binary/-/symlink-binary-3.1.4.tgz", + "integrity": "sha512-uQ8pYxzygahshXJAeC/vY4eSi+kFM0S2Pi15hJsJI3W7Ec6ysSYU1lXemb6kqoIqkTDJZWnfKXEq/3FLE+JYhg==", + "requires": { + "@lerna/create-symlink": "^3.0.0", + "@lerna/package": "^3.0.0", + "fs-extra": "^6.0.1", + "p-map": "^1.2.0", + "read-pkg": "^3.0.0" + } + }, + "@lerna/symlink-dependencies": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@lerna/symlink-dependencies/-/symlink-dependencies-3.1.4.tgz", + "integrity": "sha512-iModwb0Xh0N0t55C6S4K2mzLdu1zXVsBc0qubUY1x0RSul92z8NeAe1aM5JzwMzuSoMA/LRiD1lNMWMRBf4JVg==", + "requires": { + "@lerna/create-symlink": "^3.0.0", + "@lerna/resolve-symlink": "^3.0.0", + "@lerna/symlink-binary": "^3.1.4", + "fs-extra": "^6.0.1", + "p-finally": "^1.0.0", + "p-map": "^1.2.0", + "p-map-series": "^1.0.0" + } + }, + "@lerna/validation-error": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@lerna/validation-error/-/validation-error-3.0.0.tgz", + "integrity": "sha512-5wjkd2PszV0kWvH+EOKZJWlHEqCTTKrWsvfHnHhcUaKBe/NagPZFWs+0xlsDPZ3DJt5FNfbAPAnEBQ05zLirFA==", + "requires": { + "npmlog": "^4.1.2" + } + }, + "@lerna/version": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@lerna/version/-/version-3.2.0.tgz", + "integrity": "sha512-1AVDMpeecSMiG1cacduE+f2KO0mC7F/9MvWsHtp+rjkpficMcsVme7IMtycuvu/F07wY4Xr9ioFKYTwTcybbIA==", + "requires": { + "@lerna/batch-packages": "^3.1.2", + "@lerna/check-working-tree": "^3.1.0", + "@lerna/child-process": "^3.0.0", + "@lerna/collect-updates": "^3.1.0", + "@lerna/command": "^3.1.3", + "@lerna/conventional-commits": "^3.0.2", + "@lerna/output": "^3.0.0", + "@lerna/prompt": "^3.0.0", + "@lerna/run-lifecycle": "^3.2.0", + "@lerna/validation-error": "^3.0.0", + "chalk": "^2.3.1", + "dedent": "^0.7.0", + "minimatch": "^3.0.4", + "npmlog": "^4.1.2", + "p-map": "^1.2.0", + "p-pipe": "^1.2.0", + "p-reduce": "^1.0.0", + "p-waterfall": "^1.0.0", + "semver": "^5.5.0", + "slash": "^1.0.0", + "temp-write": "^3.4.0" + } + }, + "@lerna/write-log-file": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@lerna/write-log-file/-/write-log-file-3.0.0.tgz", + "integrity": "sha512-SfbPp29lMeEVOb/M16lJwn4nnx5y+TwCdd7Uom9umd7KcZP0NOvpnX0PHehdonl7TyHZ1Xx2maklYuCLbQrd/A==", + "requires": { + "npmlog": "^4.1.2", + "write-file-atomic": "^2.3.0" + } + }, + "@mrmlnc/readdir-enhanced": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz", + "integrity": "sha512-bPHp6Ji8b41szTOcaP63VlnbbO5Ny6dwAATtY6JTjh5N2OLrb5Qk/Th5cRkRQhkWCt+EJsYrNB0MiL+Gpn6e3g==", + "requires": { + "call-me-maybe": "^1.0.1", + "glob-to-regexp": "^0.3.0" + } + }, + "@nodelib/fs.stat": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-1.1.1.tgz", + "integrity": "sha512-KU/VDjC5RwtDUZiz3d+DHXJF2lp5hB9dn552TXIyptj8SH1vXmR40mG0JgGq03IlYsOgGfcv8xrLpSQ0YUMQdA==" + }, + "JSONStream": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.4.tgz", + "integrity": "sha512-Y7vfi3I5oMOYIr+WxV8NZxDSwcbNgzdKYsTNInmycOq9bUYwGg9ryu57Wg5NLmCjqdFPNUmpMBo3kSJN9tCbXg==", + "requires": { + "jsonparse": "^1.2.0", + "through": ">=2.2.7 <3" + } + }, + "abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" + }, + "agent-base": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.2.1.tgz", + "integrity": "sha512-JVwXMr9nHYTUXsBFKUqhJwvlcYU/blreOEUkhNR2eXZIvwd+c+o5V4MgDPKWnMS/56awN3TRzIP+KoPn+roQtg==", + "requires": { + "es6-promisify": "^5.0.0" + } + }, + "agentkeepalive": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-3.5.1.tgz", + "integrity": "sha512-Cte/sTY9/XcygXjJ0q58v//SnEQ7ViWExKyJpLJlLqomDbQyMLh6Is4KuWJ/wmxzhiwkGRple7Gqv1zf6Syz5w==", + "requires": { + "humanize-ms": "^1.2.1" + } + }, + "ajv": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz", + "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=", + "requires": { + "co": "^4.6.0", + "fast-deep-equal": "^1.0.0", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.3.0" + } + }, + "align-text": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", + "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", + "requires": { + "kind-of": "^3.0.2", + "longest": "^1.0.1", + "repeat-string": "^1.5.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "amdefine": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", + "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=" + }, + "ansi-escapes": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.1.0.tgz", + "integrity": "sha512-UgAb8H9D41AQnu/PbWlCofQVcnV4Gs2bBJi9eZPxfU/hgglFh3SMDMENRIqdr7H6XFnXdoknctFByVsCOotTVw==" + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "^1.9.0" + } + }, + "aproba": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" + }, + "are-we-there-yet": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", + "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + } + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=" + }, + "arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==" + }, + "arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=" + }, + "array-differ": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-differ/-/array-differ-1.0.0.tgz", + "integrity": "sha1-7/UuN1gknTO+QCuLuOVkuytdQDE=" + }, + "array-find-index": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", + "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=" + }, + "array-ify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-ify/-/array-ify-1.0.0.tgz", + "integrity": "sha1-nlKHYrSpBmrRY6aWKjZEGOlibs4=" + }, + "array-union": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", + "requires": { + "array-uniq": "^1.0.1" + } + }, + "array-uniq": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", + "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=" + }, + "array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=" + }, + "arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=" + }, + "asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=" + }, + "asn1": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "requires": { + "safer-buffer": "~2.1.0" + } + }, + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" + }, + "assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=" + }, + "async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=" + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, + "atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==" + }, + "aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" + }, + "aws4": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", + "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "base": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", + "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "requires": { + "cache-base": "^1.0.1", + "class-utils": "^0.3.5", + "component-emitter": "^1.2.1", + "define-property": "^1.0.0", + "isobject": "^3.0.1", + "mixin-deep": "^1.2.0", + "pascalcase": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "requires": { + "tweetnacl": "^0.14.3" + } + }, + "block-stream": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", + "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=", + "requires": { + "inherits": "~2.0.0" + } + }, + "bluebird": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.1.tgz", + "integrity": "sha512-MKiLiV+I1AA596t9w1sQJ8jkiSr5+ZKi0WKrYGUn6d1Fx+Ij4tIj+m2WMQSGczs5jZVxV339chE8iwk6F64wjA==" + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "requires": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" + }, + "builtin-modules": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", + "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=" + }, + "builtins": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/builtins/-/builtins-1.0.3.tgz", + "integrity": "sha1-y5T662HIaWRR2zZTThQi+U8K7og=" + }, + "byline": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/byline/-/byline-5.0.0.tgz", + "integrity": "sha1-dBxSFkaOrcRXsDQQEYrXfejB3bE=" + }, + "byte-size": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/byte-size/-/byte-size-4.0.3.tgz", + "integrity": "sha512-JGC3EV2bCzJH/ENSh3afyJrH4vwxbHTuO5ljLoI5+2iJOcEpMgP8T782jH9b5qGxf2mSUIp1lfGnfKNrRHpvVg==" + }, + "cacache": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-11.2.0.tgz", + "integrity": "sha512-IFWl6lfK6wSeYCHUXh+N1lY72UDrpyrYQJNIVQf48paDuWbv5RbAtJYf/4gUQFObTCHZwdZ5sI8Iw7nqwP6nlQ==", + "requires": { + "bluebird": "^3.5.1", + "chownr": "^1.0.1", + "figgy-pudding": "^3.1.0", + "glob": "^7.1.2", + "graceful-fs": "^4.1.11", + "lru-cache": "^4.1.3", + "mississippi": "^3.0.0", + "mkdirp": "^0.5.1", + "move-concurrently": "^1.0.1", + "promise-inflight": "^1.0.1", + "rimraf": "^2.6.2", + "ssri": "^6.0.0", + "unique-filename": "^1.1.0", + "y18n": "^4.0.0" + } + }, + "cache-base": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", + "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "requires": { + "collection-visit": "^1.0.0", + "component-emitter": "^1.2.1", + "get-value": "^2.0.6", + "has-value": "^1.0.0", + "isobject": "^3.0.1", + "set-value": "^2.0.0", + "to-object-path": "^0.3.0", + "union-value": "^1.0.0", + "unset-value": "^1.0.0" + } + }, + "call-me-maybe": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.1.tgz", + "integrity": "sha1-JtII6onje1y95gJQoV8DHBak1ms=" + }, + "camelcase": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", + "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=" + }, + "camelcase-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-4.2.0.tgz", + "integrity": "sha1-oqpfsa9oh1glnDLBQUJteJI7m3c=", + "requires": { + "camelcase": "^4.1.0", + "map-obj": "^2.0.0", + "quick-lru": "^1.0.0" + }, + "dependencies": { + "camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=" + } + } + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" + }, + "center-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", + "integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=", + "requires": { + "align-text": "^0.1.3", + "lazy-cache": "^1.0.3" + } + }, + "chalk": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz", + "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==", + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "chardet": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.4.2.tgz", + "integrity": "sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I=" + }, + "chownr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.0.1.tgz", + "integrity": "sha1-4qdQQqlVGQi+vSW4Uj1fl2nXkYE=" + }, + "ci-info": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.4.0.tgz", + "integrity": "sha512-Oqmw2pVfCl8sCL+1QgMywPfdxPJPkC51y4usw0iiE2S9qnEOAqXy8bwl1CpMpnoU39g4iKJTz6QZj+28FvOnjQ==" + }, + "class-utils": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", + "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "requires": { + "arr-union": "^3.1.0", + "define-property": "^0.2.5", + "isobject": "^3.0.0", + "static-extend": "^0.1.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "cli-cursor": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", + "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", + "requires": { + "restore-cursor": "^2.0.0" + } + }, + "cli-width": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", + "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=" + }, + "cliui": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", + "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=", + "requires": { + "center-align": "^0.1.1", + "right-align": "^0.1.1", + "wordwrap": "0.0.2" + }, + "dependencies": { + "wordwrap": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", + "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=" + } + } + }, + "clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=" + }, + "cmd-shim": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/cmd-shim/-/cmd-shim-2.0.2.tgz", + "integrity": "sha1-b8vamUg6j9FdfTChlspp1oii79s=", + "requires": { + "graceful-fs": "^4.1.2", + "mkdirp": "~0.5.0" + } + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" + }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" + }, + "collection-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", + "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", + "requires": { + "map-visit": "^1.0.0", + "object-visit": "^1.0.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, + "columnify": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/columnify/-/columnify-1.5.4.tgz", + "integrity": "sha1-Rzfd8ce2mop8NAVweC6UfuyOeLs=", + "requires": { + "strip-ansi": "^3.0.0", + "wcwidth": "^1.0.0" + } + }, + "combined-stream": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", + "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "compare-func": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/compare-func/-/compare-func-1.3.2.tgz", + "integrity": "sha1-md0LpFfh+bxyKxLAjsM+6rMfpkg=", + "requires": { + "array-ify": "^1.0.0", + "dot-prop": "^3.0.0" + }, + "dependencies": { + "dot-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-3.0.0.tgz", + "integrity": "sha1-G3CK8JSknJoOfbyteQq6U52sEXc=", + "requires": { + "is-obj": "^1.0.0" + } + } + } + }, + "component-emitter": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", + "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=" + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "config-chain": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.11.tgz", + "integrity": "sha1-q6CXR9++TD5w52am5BWG4YWfxvI=", + "requires": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, + "console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" + }, + "conventional-changelog-angular": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-1.6.6.tgz", + "integrity": "sha512-suQnFSqCxRwyBxY68pYTsFkG0taIdinHLNEAX5ivtw8bCRnIgnpvcHmlR/yjUyZIrNPYAoXlY1WiEKWgSE4BNg==", + "requires": { + "compare-func": "^1.3.1", + "q": "^1.5.1" + } + }, + "conventional-changelog-core": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/conventional-changelog-core/-/conventional-changelog-core-2.0.11.tgz", + "integrity": "sha512-HvTE6RlqeEZ/NFPtQeFLsIDOLrGP3bXYr7lFLMhCVsbduF1MXIe8OODkwMFyo1i9ku9NWBwVnVn0jDmIFXjDRg==", + "requires": { + "conventional-changelog-writer": "^3.0.9", + "conventional-commits-parser": "^2.1.7", + "dateformat": "^3.0.0", + "get-pkg-repo": "^1.0.0", + "git-raw-commits": "^1.3.6", + "git-remote-origin-url": "^2.0.0", + "git-semver-tags": "^1.3.6", + "lodash": "^4.2.1", + "normalize-package-data": "^2.3.5", + "q": "^1.5.1", + "read-pkg": "^1.1.0", + "read-pkg-up": "^1.0.1", + "through2": "^2.0.0" + }, + "dependencies": { + "load-json-file": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0", + "strip-bom": "^2.0.0" + } + }, + "parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "requires": { + "error-ex": "^1.2.0" + } + }, + "path-type": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", + "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", + "requires": { + "graceful-fs": "^4.1.2", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" + }, + "read-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", + "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", + "requires": { + "load-json-file": "^1.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^1.0.0" + } + }, + "strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "requires": { + "is-utf8": "^0.2.0" + } + } + } + }, + "conventional-changelog-preset-loader": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/conventional-changelog-preset-loader/-/conventional-changelog-preset-loader-1.1.8.tgz", + "integrity": "sha512-MkksM4G4YdrMlT2MbTsV2F6LXu/hZR0Tc/yenRrDIKRwBl/SP7ER4ZDlglqJsCzLJi4UonBc52Bkm5hzrOVCcw==" + }, + "conventional-changelog-writer": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/conventional-changelog-writer/-/conventional-changelog-writer-3.0.9.tgz", + "integrity": "sha512-n9KbsxlJxRQsUnK6wIBRnARacvNnN4C/nxnxCkH+B/R1JS2Fa+DiP1dU4I59mEDEjgnFaN2+9wr1P1s7GYB5/Q==", + "requires": { + "compare-func": "^1.3.1", + "conventional-commits-filter": "^1.1.6", + "dateformat": "^3.0.0", + "handlebars": "^4.0.2", + "json-stringify-safe": "^5.0.1", + "lodash": "^4.2.1", + "meow": "^4.0.0", + "semver": "^5.5.0", + "split": "^1.0.0", + "through2": "^2.0.0" + } + }, + "conventional-commits-filter": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/conventional-commits-filter/-/conventional-commits-filter-1.1.6.tgz", + "integrity": "sha512-KcDgtCRKJCQhyk6VLT7zR+ZOyCnerfemE/CsR3iQpzRRFbLEs0Y6rwk3mpDvtOh04X223z+1xyJ582Stfct/0Q==", + "requires": { + "is-subset": "^0.1.1", + "modify-values": "^1.0.0" + } + }, + "conventional-commits-parser": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-2.1.7.tgz", + "integrity": "sha512-BoMaddIEJ6B4QVMSDu9IkVImlGOSGA1I2BQyOZHeLQ6qVOJLcLKn97+fL6dGbzWEiqDzfH4OkcveULmeq2MHFQ==", + "requires": { + "JSONStream": "^1.0.4", + "is-text-path": "^1.0.0", + "lodash": "^4.2.1", + "meow": "^4.0.0", + "split2": "^2.0.0", + "through2": "^2.0.0", + "trim-off-newlines": "^1.0.0" + } + }, + "conventional-recommended-bump": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/conventional-recommended-bump/-/conventional-recommended-bump-2.0.9.tgz", + "integrity": "sha512-YE6/o+648qkX3fTNvfBsvPW3tSnbZ6ec3gF0aBahCPgyoVHU2Mw0nUAZ1h1UN65GazpORngrgRC8QCltNYHPpQ==", + "requires": { + "concat-stream": "^1.6.0", + "conventional-changelog-preset-loader": "^1.1.8", + "conventional-commits-filter": "^1.1.6", + "conventional-commits-parser": "^2.1.7", + "git-raw-commits": "^1.3.6", + "git-semver-tags": "^1.3.6", + "meow": "^4.0.0", + "q": "^1.5.1" + } + }, + "copy-concurrently": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/copy-concurrently/-/copy-concurrently-1.0.5.tgz", + "integrity": "sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A==", + "requires": { + "aproba": "^1.1.1", + "fs-write-stream-atomic": "^1.0.8", + "iferr": "^0.1.5", + "mkdirp": "^0.5.1", + "rimraf": "^2.5.4", + "run-queue": "^1.0.0" + } + }, + "copy-descriptor": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", + "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=" + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "cosmiconfig": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.0.6.tgz", + "integrity": "sha512-6DWfizHriCrFWURP1/qyhsiFvYdlJzbCzmtFWh744+KyWsJo5+kPzUZZaMRSSItoYc0pxFX7gEO7ZC1/gN/7AQ==", + "requires": { + "is-directory": "^0.3.1", + "js-yaml": "^3.9.0", + "parse-json": "^4.0.0" + } + }, + "cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "currently-unhandled": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", + "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=", + "requires": { + "array-find-index": "^1.0.1" + } + }, + "cyclist": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-0.2.2.tgz", + "integrity": "sha1-GzN5LhHpFKL9bW7WRHRkRE5fpkA=" + }, + "dargs": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/dargs/-/dargs-4.1.0.tgz", + "integrity": "sha1-A6nbtLXC8Tm/FK5T8LiipqhvThc=", + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "dateformat": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz", + "integrity": "sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==" + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "debuglog": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/debuglog/-/debuglog-1.0.1.tgz", + "integrity": "sha1-qiT/uaw9+aI1GDfPstJ5NgzXhJI=" + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" + }, + "decamelize-keys": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.0.tgz", + "integrity": "sha1-0XGoeTMlKAfrPLYdwcFEXQeN8tk=", + "requires": { + "decamelize": "^1.1.0", + "map-obj": "^1.0.0" + }, + "dependencies": { + "map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=" + } + } + }, + "decode-uri-component": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", + "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=" + }, + "dedent": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", + "integrity": "sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw=" + }, + "defaults": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz", + "integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=", + "requires": { + "clone": "^1.0.2" + } + }, + "define-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "requires": { + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" + }, + "dependencies": { + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + }, + "delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" + }, + "detect-indent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-5.0.0.tgz", + "integrity": "sha1-OHHMCmoALow+Wzz38zYmRnXwa50=" + }, + "dezalgo": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.3.tgz", + "integrity": "sha1-f3Qt4Gb8dIvI24IFad3c5Jvw1FY=", + "requires": { + "asap": "^2.0.0", + "wrappy": "1" + } + }, + "dir-glob": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-2.0.0.tgz", + "integrity": "sha512-37qirFDz8cA5fimp9feo43fSuRo2gHwaIn6dXL8Ber1dGwUosDrGZeCCXq57WnIqE4aQ+u3eQZzsk1yOzhdwag==", + "requires": { + "arrify": "^1.0.1", + "path-type": "^3.0.0" + } + }, + "dot-prop": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.0.tgz", + "integrity": "sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ==", + "requires": { + "is-obj": "^1.0.0" + } + }, + "duplexer": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.1.tgz", + "integrity": "sha1-rOb/gIwc5mtX0ev5eXessCM0z8E=" + }, + "duplexify": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.6.0.tgz", + "integrity": "sha512-fO3Di4tBKJpYTFHAxTU00BcfWMY9w24r/x21a6rZRbsD/ToUgGxsMbiGRmB7uVAXeGKXD9MwiLZa5E97EVgIRQ==", + "requires": { + "end-of-stream": "^1.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.0.0", + "stream-shift": "^1.0.0" + } + }, + "ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "requires": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "encoding": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz", + "integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=", + "requires": { + "iconv-lite": "~0.4.13" + } + }, + "end-of-stream": { + "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==", + "requires": { + "once": "^1.4.0" + } + }, + "err-code": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-1.1.2.tgz", + "integrity": "sha1-BuARbTAo9q70gGhJ6w6mp0iuaWA=" + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "es6-promise": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.4.tgz", + "integrity": "sha512-/NdNZVJg+uZgtm9eS3O6lrOLYmQag2DjdEXuPaHlZ6RuVqgqaVZfgYCepEIKsLqwdQArOPtC3XzRLqGGfT8KQQ==" + }, + "es6-promisify": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", + "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=", + "requires": { + "es6-promise": "^4.0.3" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" + }, + "execa": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-0.10.0.tgz", + "integrity": "sha512-7XOMnz8Ynx1gGo/3hyV9loYNPWM94jG3+3T3Y8tsfSstFmETmENCMU/A/zj8Lyaj1lkgEepKepvd6240tBRvlw==", + "requires": { + "cross-spawn": "^6.0.0", + "get-stream": "^3.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + }, + "expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "requires": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "requires": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "external-editor": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-2.2.0.tgz", + "integrity": "sha512-bSn6gvGxKt+b7+6TKEv1ZycHleA7aHhRHyAqJyp5pbUFuYYNIzpZnQDk7AsYckyWdEnTeAnay0aCy2aV6iTk9A==", + "requires": { + "chardet": "^0.4.0", + "iconv-lite": "^0.4.17", + "tmp": "^0.0.33" + } + }, + "extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "requires": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" + }, + "fast-deep-equal": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz", + "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ=" + }, + "fast-glob": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-2.2.2.tgz", + "integrity": "sha512-TR6zxCKftDQnUAPvkrCWdBgDq/gbqx8A3ApnBrR5rMvpp6+KMJI0Igw7fkWPgeVK0uhRXTXdvO3O+YP0CaUX2g==", + "requires": { + "@mrmlnc/readdir-enhanced": "^2.2.1", + "@nodelib/fs.stat": "^1.0.1", + "glob-parent": "^3.1.0", + "is-glob": "^4.0.0", + "merge2": "^1.2.1", + "micromatch": "^3.1.10" + }, + "dependencies": { + "is-glob": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.0.tgz", + "integrity": "sha1-lSHHaEXMJhCoUgPd8ICpWML/q8A=", + "requires": { + "is-extglob": "^2.1.1" + } + } + } + }, + "fast-json-stable-stringify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" + }, + "figgy-pudding": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.1.tgz", + "integrity": "sha512-vNKxJHTEKNThjfrdJwHc7brvM6eVevuO5nTj6ez8ZQ1qbXTvGthucRF7S4vf2cr71QVnT70V34v0S1DyQsti0w==" + }, + "figures": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", + "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", + "requires": { + "escape-string-regexp": "^1.0.5" + } + }, + "fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "requires": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "requires": { + "locate-path": "^2.0.0" + } + }, + "flush-write-stream": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.0.3.tgz", + "integrity": "sha512-calZMC10u0FMUqoiunI2AiGIIUtUIvifNwkHhNupZH4cbNnW1Itkoh/Nf5HFYmDrwWPjrUxpkZT0KhuCq0jmGw==", + "requires": { + "inherits": "^2.0.1", + "readable-stream": "^2.0.4" + } + }, + "for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=" + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" + }, + "form-data": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz", + "integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "1.0.6", + "mime-types": "^2.1.12" + } + }, + "fragment-cache": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", + "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", + "requires": { + "map-cache": "^0.2.2" + } + }, + "from2": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", + "integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=", + "requires": { + "inherits": "^2.0.1", + "readable-stream": "^2.0.0" + } + }, + "fs-extra": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-6.0.1.tgz", + "integrity": "sha512-GnyIkKhhzXZUWFCaJzvyDLEEgDkPfb4/TPvJCJVuS8MWZgoSsErf++QpiAlDnKFcqhRlm+tIOcencCjyJE6ZCA==", + "requires": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "fs-minipass": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.5.tgz", + "integrity": "sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ==", + "requires": { + "minipass": "^2.2.1" + } + }, + "fs-write-stream-atomic": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz", + "integrity": "sha1-tH31NJPvkR33VzHnCp3tAYnbQMk=", + "requires": { + "graceful-fs": "^4.1.2", + "iferr": "^0.1.5", + "imurmurhash": "^0.1.4", + "readable-stream": "1 || 2" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "fstream": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz", + "integrity": "sha1-XB+x8RdHcRTwYyoOtLcbPLD9MXE=", + "requires": { + "graceful-fs": "^4.1.2", + "inherits": "~2.0.0", + "mkdirp": ">=0.5 0", + "rimraf": "2" + } + }, + "gauge": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", + "requires": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + } + }, + "genfun": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/genfun/-/genfun-4.0.1.tgz", + "integrity": "sha1-7RAEHy5KfxsKOEZtF6XD4n3x38E=" + }, + "get-caller-file": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", + "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==" + }, + "get-pkg-repo": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/get-pkg-repo/-/get-pkg-repo-1.4.0.tgz", + "integrity": "sha1-xztInAbYDMVTbCyFP54FIyBWly0=", + "requires": { + "hosted-git-info": "^2.1.4", + "meow": "^3.3.0", + "normalize-package-data": "^2.3.0", + "parse-github-repo-url": "^1.3.0", + "through2": "^2.0.0" + }, + "dependencies": { + "camelcase": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", + "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=" + }, + "camelcase-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", + "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", + "requires": { + "camelcase": "^2.0.0", + "map-obj": "^1.0.0" + } + }, + "indent-string": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", + "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=", + "requires": { + "repeating": "^2.0.0" + } + }, + "map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=" + }, + "meow": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", + "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", + "requires": { + "camelcase-keys": "^2.0.0", + "decamelize": "^1.1.2", + "loud-rejection": "^1.0.0", + "map-obj": "^1.0.1", + "minimist": "^1.1.3", + "normalize-package-data": "^2.3.4", + "object-assign": "^4.0.1", + "read-pkg-up": "^1.0.1", + "redent": "^1.0.0", + "trim-newlines": "^1.0.0" + } + }, + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + }, + "redent": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz", + "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=", + "requires": { + "indent-string": "^2.1.0", + "strip-indent": "^1.0.1" + } + }, + "strip-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz", + "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=", + "requires": { + "get-stdin": "^4.0.1" + } + }, + "trim-newlines": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz", + "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=" + } + } + }, + "get-port": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-3.2.0.tgz", + "integrity": "sha1-3Xzn3hh8Bsi/NTeWrHHgmfCYDrw=" + }, + "get-stdin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", + "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=" + }, + "get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=" + }, + "get-value": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", + "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=" + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "requires": { + "assert-plus": "^1.0.0" + } + }, + "git-raw-commits": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/git-raw-commits/-/git-raw-commits-1.3.6.tgz", + "integrity": "sha512-svsK26tQ8vEKnMshTDatSIQSMDdz8CxIIqKsvPqbtV23Etmw6VNaFAitu8zwZ0VrOne7FztwPyRLxK7/DIUTQg==", + "requires": { + "dargs": "^4.0.1", + "lodash.template": "^4.0.2", + "meow": "^4.0.0", + "split2": "^2.0.0", + "through2": "^2.0.0" + } + }, + "git-remote-origin-url": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/git-remote-origin-url/-/git-remote-origin-url-2.0.0.tgz", + "integrity": "sha1-UoJlna4hBxRaERJhEq0yFuxfpl8=", + "requires": { + "gitconfiglocal": "^1.0.0", + "pify": "^2.3.0" + }, + "dependencies": { + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" + } + } + }, + "git-semver-tags": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/git-semver-tags/-/git-semver-tags-1.3.6.tgz", + "integrity": "sha512-2jHlJnln4D/ECk9FxGEBh3k44wgYdWjWDtMmJPaecjoRmxKo3Y1Lh8GMYuOPu04CHw86NTAODchYjC5pnpMQig==", + "requires": { + "meow": "^4.0.0", + "semver": "^5.5.0" + } + }, + "gitconfiglocal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/gitconfiglocal/-/gitconfiglocal-1.0.0.tgz", + "integrity": "sha1-QdBF84UaXqiPA/JMocYXgRRGS5s=", + "requires": { + "ini": "^1.3.2" + } + }, + "glob": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz", + "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "requires": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + } + }, + "glob-to-regexp": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz", + "integrity": "sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs=" + }, + "globby": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/globby/-/globby-8.0.1.tgz", + "integrity": "sha512-oMrYrJERnKBLXNLVTqhm3vPEdJ/b2ZE28xN4YARiix1NOIOBPEpOUnm844K1iu/BkphCaf2WNFwMszv8Soi1pw==", + "requires": { + "array-union": "^1.0.1", + "dir-glob": "^2.0.0", + "fast-glob": "^2.0.2", + "glob": "^7.1.2", + "ignore": "^3.3.5", + "pify": "^3.0.0", + "slash": "^1.0.0" + } + }, + "graceful-fs": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=" + }, + "handlebars": { + "version": "4.0.11", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.0.11.tgz", + "integrity": "sha1-Ywo13+ApS8KB7a5v/F0yn8eYLcw=", + "requires": { + "async": "^1.4.0", + "optimist": "^0.6.1", + "source-map": "^0.4.4", + "uglify-js": "^2.6" + }, + "dependencies": { + "source-map": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz", + "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=", + "requires": { + "amdefine": ">=0.0.4" + } + } + } + }, + "har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" + }, + "har-validator": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.0.tgz", + "integrity": "sha512-+qnmNjI4OfH2ipQ9VQOw23bBd/ibtfbVdK2fYbY4acTDqKTW/YDp9McimZdDbG8iV9fZizUqQMD5xvriB146TA==", + "requires": { + "ajv": "^5.3.0", + "har-schema": "^2.0.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + }, + "has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" + }, + "has-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", + "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", + "requires": { + "get-value": "^2.0.6", + "has-values": "^1.0.0", + "isobject": "^3.0.0" + } + }, + "has-values": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", + "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", + "requires": { + "is-number": "^3.0.0", + "kind-of": "^4.0.0" + }, + "dependencies": { + "kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "hosted-git-info": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.7.1.tgz", + "integrity": "sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==" + }, + "http-cache-semantics": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz", + "integrity": "sha512-5ai2iksyV8ZXmnZhHH4rWPoxxistEexSi5936zIQ1bnNTW5VnA85B6P/VpXiRM017IgRvb2kKo1a//y+0wSp3w==" + }, + "http-proxy-agent": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-2.1.0.tgz", + "integrity": "sha512-qwHbBLV7WviBl0rQsOzH6o5lwyOIvwp/BdFnvVxXORldu5TmjFfjzBcWUWS5kWAZhmv+JtiDhSuQCp4sBfbIgg==", + "requires": { + "agent-base": "4", + "debug": "3.1.0" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + } + } + }, + "http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "requires": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + } + }, + "https-proxy-agent": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.1.tgz", + "integrity": "sha512-HPCTS1LW51bcyMYbxUIOO4HEOlQ1/1qRaFWcyxvwaqUS9TY88aoEuHUY33kuAh1YhVVaDQhLZsnPd+XNARWZlQ==", + "requires": { + "agent-base": "^4.1.0", + "debug": "^3.1.0" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + } + } + }, + "humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha1-xG4xWaKT9riW2ikxbYtv6Lt5u+0=", + "requires": { + "ms": "^2.0.0" + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "iferr": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/iferr/-/iferr-0.1.5.tgz", + "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=" + }, + "ignore": { + "version": "3.3.10", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.10.tgz", + "integrity": "sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==" + }, + "ignore-walk": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.1.tgz", + "integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==", + "requires": { + "minimatch": "^3.0.4" + } + }, + "import-local": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-1.0.0.tgz", + "integrity": "sha512-vAaZHieK9qjGo58agRBg+bhHX3hoTZU/Oa3GESWLz7t1U62fk63aHuDJJEteXoDeTCcPmUT+z38gkHPZkkmpmQ==", + "requires": { + "pkg-dir": "^2.0.0", + "resolve-cwd": "^2.0.0" + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=" + }, + "indent-string": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-3.2.0.tgz", + "integrity": "sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok=" + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "ini": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" + }, + "init-package-json": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/init-package-json/-/init-package-json-1.10.3.tgz", + "integrity": "sha512-zKSiXKhQveNteyhcj1CoOP8tqp1QuxPIPBl8Bid99DGLFqA1p87M6lNgfjJHSBoWJJlidGOv5rWjyYKEB3g2Jw==", + "requires": { + "glob": "^7.1.1", + "npm-package-arg": "^4.0.0 || ^5.0.0 || ^6.0.0", + "promzard": "^0.3.0", + "read": "~1.0.1", + "read-package-json": "1 || 2", + "semver": "2.x || 3.x || 4 || 5", + "validate-npm-package-license": "^3.0.1", + "validate-npm-package-name": "^3.0.0" + } + }, + "inquirer": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-5.2.0.tgz", + "integrity": "sha512-E9BmnJbAKLPGonz0HeWHtbKf+EeSP93paWO3ZYoUpq/aowXvYGjjCSuashhXPpzbArIjBbji39THkxTz9ZeEUQ==", + "requires": { + "ansi-escapes": "^3.0.0", + "chalk": "^2.0.0", + "cli-cursor": "^2.1.0", + "cli-width": "^2.0.0", + "external-editor": "^2.1.0", + "figures": "^2.0.0", + "lodash": "^4.3.0", + "mute-stream": "0.0.7", + "run-async": "^2.2.0", + "rxjs": "^5.5.2", + "string-width": "^2.1.0", + "strip-ansi": "^4.0.0", + "through": "^2.3.6" + }, + "dependencies": { + "ansi-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", + "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" + }, + "string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "requires": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + } + }, + "strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "requires": { + "ansi-regex": "^3.0.0" + } + } + } + }, + "invert-kv": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", + "integrity": "sha1-EEqOSqym09jNFXqO+L+rLXo//bY=" + }, + "ip": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", + "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=" + }, + "is-accessor-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", + "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + }, + "is-builtin-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz", + "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=", + "requires": { + "builtin-modules": "^1.0.0" + } + }, + "is-ci": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-1.2.0.tgz", + "integrity": "sha512-plgvKjQtalH2P3Gytb7L61Lmz95g2DlpzFiQyRSFew8WoJKxtKRzrZMeyRN2supblm3Psc8OQGy7Xjb6XG11jw==", + "requires": { + "ci-info": "^1.3.0" + } + }, + "is-data-descriptor": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", + "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-descriptor": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", + "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "requires": { + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" + }, + "dependencies": { + "kind-of": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", + "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" + } + } + }, + "is-directory": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", + "integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=" + }, + "is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=" + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" + }, + "is-finite": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", + "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "requires": { + "is-extglob": "^2.1.0" + } + }, + "is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "is-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", + "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=" + }, + "is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=" + }, + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "requires": { + "isobject": "^3.0.1" + } + }, + "is-promise": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", + "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=" + }, + "is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" + }, + "is-subset": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-subset/-/is-subset-0.1.1.tgz", + "integrity": "sha1-ilkRfZMt4d4A8kX83TnOQ/HpOaY=" + }, + "is-text-path": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-text-path/-/is-text-path-1.0.1.tgz", + "integrity": "sha1-Thqg+1G/vLPpJogAE5cgLBd1tm4=", + "requires": { + "text-extensions": "^1.0.0" + } + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" + }, + "is-utf8": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", + "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=" + }, + "is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==" + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" + }, + "js-yaml": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" + }, + "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", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==" + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" + }, + "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=" + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" + }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "requires": { + "graceful-fs": "^4.1.6" + } + }, + "jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=" + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + } + }, + "kind-of": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", + "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" + }, + "lazy-cache": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", + "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=" + }, + "lcid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", + "integrity": "sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU=", + "requires": { + "invert-kv": "^1.0.0" + } + }, + "lerna": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/lerna/-/lerna-3.2.1.tgz", + "integrity": "sha512-nHa/TgRLOHlBm+NfeW62ffVO7hY7wJxnu6IJmZA3lrSmRlqrXZk2BPvnq0FSaCinVYjW0w0XeSNZdRKR//HAwQ==", + "requires": { + "@lerna/add": "^3.2.0", + "@lerna/bootstrap": "^3.2.0", + "@lerna/changed": "^3.2.0", + "@lerna/clean": "^3.1.3", + "@lerna/cli": "^3.2.0", + "@lerna/create": "^3.1.3", + "@lerna/diff": "^3.1.3", + "@lerna/exec": "^3.1.3", + "@lerna/import": "^3.1.3", + "@lerna/init": "^3.1.3", + "@lerna/link": "^3.1.4", + "@lerna/list": "^3.1.3", + "@lerna/publish": "^3.2.1", + "@lerna/run": "^3.1.3", + "@lerna/version": "^3.2.0", + "import-local": "^1.0.0", + "npmlog": "^4.1.2" + } + }, + "load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + } + }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "requires": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + } + }, + "lodash": { + "version": "4.17.10", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.10.tgz", + "integrity": "sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg==" + }, + "lodash._reinterpolate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", + "integrity": "sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=" + }, + "lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=" + }, + "lodash.template": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.5.0.tgz", + "integrity": "sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A==", + "requires": { + "lodash._reinterpolate": "^3.0.0", + "lodash.templatesettings": "^4.0.0" + } + }, + "lodash.templatesettings": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-4.1.0.tgz", + "integrity": "sha1-K01OlbpEDZFf8IvImeRVNmZxMxY=", + "requires": { + "lodash._reinterpolate": "~3.0.0" + } + }, + "longest": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", + "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=" + }, + "loud-rejection": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", + "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=", + "requires": { + "currently-unhandled": "^0.4.1", + "signal-exit": "^3.0.0" + } + }, + "lru-cache": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.3.tgz", + "integrity": "sha512-fFEhvcgzuIoJVUF8fYr5KR0YqxD238zgObTps31YdADwPPAp82a4M8TrckkWyx7ekNlf9aBcVn81cFwwXngrJA==", + "requires": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "make-dir": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", + "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", + "requires": { + "pify": "^3.0.0" + } + }, + "make-fetch-happen": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-4.0.1.tgz", + "integrity": "sha512-7R5ivfy9ilRJ1EMKIOziwrns9fGeAD4bAha8EB7BIiBBLHm2KeTUGCrICFt2rbHfzheTLynv50GnNTK1zDTrcQ==", + "requires": { + "agentkeepalive": "^3.4.1", + "cacache": "^11.0.1", + "http-cache-semantics": "^3.8.1", + "http-proxy-agent": "^2.1.0", + "https-proxy-agent": "^2.2.1", + "lru-cache": "^4.1.2", + "mississippi": "^3.0.0", + "node-fetch-npm": "^2.0.2", + "promise-retry": "^1.1.1", + "socks-proxy-agent": "^4.0.0", + "ssri": "^6.0.0" + } + }, + "map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=" + }, + "map-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-2.0.0.tgz", + "integrity": "sha1-plzSkIepJZi4eRJXpSPgISIqwfk=" + }, + "map-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", + "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", + "requires": { + "object-visit": "^1.0.0" + } + }, + "mem": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/mem/-/mem-1.1.0.tgz", + "integrity": "sha1-Xt1StIXKHZAP5kiVUFOZoN+kX3Y=", + "requires": { + "mimic-fn": "^1.0.0" + } + }, + "meow": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/meow/-/meow-4.0.1.tgz", + "integrity": "sha512-xcSBHD5Z86zaOc+781KrupuHAzeGXSLtiAOmBsiLDiPSaYSB6hdew2ng9EBAnZ62jagG9MHAOdxpDi/lWBFJ/A==", + "requires": { + "camelcase-keys": "^4.0.0", + "decamelize-keys": "^1.0.0", + "loud-rejection": "^1.0.0", + "minimist": "^1.1.3", + "minimist-options": "^3.0.1", + "normalize-package-data": "^2.3.4", + "read-pkg-up": "^3.0.0", + "redent": "^2.0.0", + "trim-newlines": "^2.0.0" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" + }, + "read-pkg-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-3.0.0.tgz", + "integrity": "sha1-PtSWaF26D4/hGNBpHcUfSh/5bwc=", + "requires": { + "find-up": "^2.0.0", + "read-pkg": "^3.0.0" + } + } + } + }, + "merge2": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.2.2.tgz", + "integrity": "sha512-bgM8twH86rWni21thii6WCMQMRMmwqqdW3sGWi9IipnVAszdLXRjwDwAnyrVXo6DuP3AjRMMttZKUB48QWIFGg==" + }, + "micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + } + }, + "mime-db": { + "version": "1.36.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.36.0.tgz", + "integrity": "sha512-L+xvyD9MkoYMXb1jAmzI/lWYAxAMCPvIBSWur0PZ5nOf5euahRLVqH//FKW9mWp2lkqUgYiXPgkzfMUFi4zVDw==" + }, + "mime-types": { + "version": "2.1.20", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.20.tgz", + "integrity": "sha512-HrkrPaP9vGuWbLK1B1FfgAkbqNjIuy4eHlIYnFi7kamZyLLrGlo2mpcx0bBmNpKqBtYtAfGbodDddIgddSJC2A==", + "requires": { + "mime-db": "~1.36.0" + } + }, + "mimic-fn": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", + "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==" + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.1.0.tgz", + "integrity": "sha1-md9lelJXTCHJBXSX33QnkLK0wN4=" + }, + "minimist-options": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-3.0.2.tgz", + "integrity": "sha512-FyBrT/d0d4+uiZRbqznPXqw3IpZZG3gl3wKWiX784FycUKVwBt0uLBFkQrtE4tZOrgo78nZp2jnKz3L65T5LdQ==", + "requires": { + "arrify": "^1.0.1", + "is-plain-obj": "^1.1.0" + } + }, + "minipass": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.3.4.tgz", + "integrity": "sha512-mlouk1OHlaUE8Odt1drMtG1bAJA4ZA6B/ehysgV0LUIrDHdKgo1KorZq3pK0b/7Z7LJIQ12MNM6aC+Tn6lUZ5w==", + "requires": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + }, + "dependencies": { + "yallist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.2.tgz", + "integrity": "sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k=" + } + } + }, + "mississippi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mississippi/-/mississippi-3.0.0.tgz", + "integrity": "sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA==", + "requires": { + "concat-stream": "^1.5.0", + "duplexify": "^3.4.2", + "end-of-stream": "^1.1.0", + "flush-write-stream": "^1.0.0", + "from2": "^2.1.0", + "parallel-transform": "^1.1.0", + "pump": "^3.0.0", + "pumpify": "^1.3.3", + "stream-each": "^1.1.0", + "through2": "^2.0.0" + } + }, + "mixin-deep": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz", + "integrity": "sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==", + "requires": { + "for-in": "^1.0.2", + "is-extendable": "^1.0.1" + }, + "dependencies": { + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "requires": { + "is-plain-object": "^2.0.4" + } + } + } + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "requires": { + "minimist": "0.0.8" + }, + "dependencies": { + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + } + } + }, + "modify-values": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/modify-values/-/modify-values-1.0.1.tgz", + "integrity": "sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw==" + }, + "moment": { + "version": "2.22.2", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.22.2.tgz", + "integrity": "sha1-PCV/mDn8DpP/UxSWMiOeuQeD/2Y=" + }, + "move-concurrently": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", + "integrity": "sha1-viwAX9oy4LKa8fBdfEszIUxwH5I=", + "requires": { + "aproba": "^1.1.1", + "copy-concurrently": "^1.0.0", + "fs-write-stream-atomic": "^1.0.8", + "mkdirp": "^0.5.1", + "rimraf": "^2.5.4", + "run-queue": "^1.0.3" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "multimatch": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/multimatch/-/multimatch-2.1.0.tgz", + "integrity": "sha1-nHkGoi+0wCkZ4vX3UWG0zb1LKis=", + "requires": { + "array-differ": "^1.0.0", + "array-union": "^1.0.1", + "arrify": "^1.0.0", + "minimatch": "^3.0.0" + } + }, + "mute-stream": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", + "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=" + }, + "nanomatch": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", + "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", + "requires": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "fragment-cache": "^0.2.1", + "is-windows": "^1.0.2", + "kind-of": "^6.0.2", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + } + }, + "nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==" + }, + "node-fetch-npm": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/node-fetch-npm/-/node-fetch-npm-2.0.2.tgz", + "integrity": "sha512-nJIxm1QmAj4v3nfCvEeCrYSoVwXyxLnaPBK5W1W5DGEJwjlKuC2VEUycGw5oxk+4zZahRrB84PUJJgEmhFTDFw==", + "requires": { + "encoding": "^0.1.11", + "json-parse-better-errors": "^1.0.0", + "safe-buffer": "^5.1.1" + } + }, + "node-gyp": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-3.8.0.tgz", + "integrity": "sha512-3g8lYefrRRzvGeSowdJKAKyks8oUpLEd/DyPV4eMhVlhJ0aNaZqIrNUIPuEWWTAoPqyFkfGrM67MC69baqn6vA==", + "requires": { + "fstream": "^1.0.0", + "glob": "^7.0.3", + "graceful-fs": "^4.1.2", + "mkdirp": "^0.5.0", + "nopt": "2 || 3", + "npmlog": "0 || 1 || 2 || 3 || 4", + "osenv": "0", + "request": "^2.87.0", + "rimraf": "2", + "semver": "~5.3.0", + "tar": "^2.0.0", + "which": "1" + }, + "dependencies": { + "semver": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", + "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=" + } + } + }, + "nopt": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", + "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", + "requires": { + "abbrev": "1" + } + }, + "normalize-package-data": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz", + "integrity": "sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==", + "requires": { + "hosted-git-info": "^2.1.4", + "is-builtin-module": "^1.0.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "npm-bundled": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.0.5.tgz", + "integrity": "sha512-m/e6jgWu8/v5niCUKQi9qQl8QdeEduFA96xHDDzFGqly0OOjI7c+60KM/2sppfnUU9JJagf+zs+yGhqSOFj71g==" + }, + "npm-lifecycle": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/npm-lifecycle/-/npm-lifecycle-2.1.0.tgz", + "integrity": "sha512-QbBfLlGBKsktwBZLj6AviHC6Q9Y3R/AY4a2PYSIRhSKSS0/CxRyD/PfxEX6tPeOCXQgMSNdwGeECacstgptc+g==", + "requires": { + "byline": "^5.0.0", + "graceful-fs": "^4.1.11", + "node-gyp": "^3.8.0", + "resolve-from": "^4.0.0", + "slide": "^1.1.6", + "uid-number": "0.0.6", + "umask": "^1.1.0", + "which": "^1.3.1" + } + }, + "npm-package-arg": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-6.1.0.tgz", + "integrity": "sha512-zYbhP2k9DbJhA0Z3HKUePUgdB1x7MfIfKssC+WLPFMKTBZKpZh5m13PgexJjCq6KW7j17r0jHWcCpxEqnnncSA==", + "requires": { + "hosted-git-info": "^2.6.0", + "osenv": "^0.1.5", + "semver": "^5.5.0", + "validate-npm-package-name": "^3.0.0" + } + }, + "npm-packlist": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.1.11.tgz", + "integrity": "sha512-CxKlZ24urLkJk+9kCm48RTQ7L4hsmgSVzEk0TLGPzzyuFxD7VNgy5Sl24tOLMzQv773a/NeJ1ce1DKeacqffEA==", + "requires": { + "ignore-walk": "^3.0.1", + "npm-bundled": "^1.0.1" + } + }, + "npm-pick-manifest": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-2.1.0.tgz", + "integrity": "sha512-q9zLP8cTr8xKPmMZN3naxp1k/NxVFsjxN6uWuO1tiw9gxg7wZWQ/b5UTfzD0ANw2q1lQxdLKTeCCksq+bPSgbQ==", + "requires": { + "npm-package-arg": "^6.0.0", + "semver": "^5.4.1" + } + }, + "npm-registry-fetch": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-3.8.0.tgz", + "integrity": "sha512-hrw8UMD+Nob3Kl3h8Z/YjmKamb1gf7D1ZZch2otrIXM3uFLB5vjEY6DhMlq80z/zZet6eETLbOXcuQudCB3Zpw==", + "requires": { + "JSONStream": "^1.3.4", + "bluebird": "^3.5.1", + "figgy-pudding": "^3.4.1", + "lru-cache": "^4.1.3", + "make-fetch-happen": "^4.0.1", + "npm-package-arg": "^6.1.0" + } + }, + "npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "requires": { + "path-key": "^2.0.0" + } + }, + "npmlog": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" + }, + "oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + }, + "object-copy": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", + "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", + "requires": { + "copy-descriptor": "^0.1.0", + "define-property": "^0.2.5", + "kind-of": "^3.0.3" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "object-visit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", + "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", + "requires": { + "isobject": "^3.0.0" + } + }, + "object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", + "requires": { + "isobject": "^3.0.1" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1" + } + }, + "onetime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", + "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", + "requires": { + "mimic-fn": "^1.0.0" + } + }, + "optimist": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", + "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", + "requires": { + "minimist": "~0.0.1", + "wordwrap": "~0.0.2" + }, + "dependencies": { + "minimist": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", + "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=" + } + } + }, + "os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" + }, + "os-locale": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-2.1.0.tgz", + "integrity": "sha512-3sslG3zJbEYcaC4YVAvDorjGxc7tv6KVATnLPZONiljsUncvihe9BQoVCEs0RZ1kmf4Hk9OBqlZfJZWI4GanKA==", + "requires": { + "execa": "^0.7.0", + "lcid": "^1.0.0", + "mem": "^1.1.0" + }, + "dependencies": { + "cross-spawn": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", + "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", + "requires": { + "lru-cache": "^4.0.1", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "execa": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", + "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", + "requires": { + "cross-spawn": "^5.0.1", + "get-stream": "^3.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + } + } + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" + }, + "osenv": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=" + }, + "p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "requires": { + "p-try": "^1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "requires": { + "p-limit": "^1.1.0" + } + }, + "p-map": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-1.2.0.tgz", + "integrity": "sha512-r6zKACMNhjPJMTl8KcFH4li//gkrXWfbD6feV8l6doRHlzljFWGJ2AP6iKaCJXyZmAUMOPtvbW7EXkbWO/pLEA==" + }, + "p-map-series": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-map-series/-/p-map-series-1.0.0.tgz", + "integrity": "sha1-v5j+V1cFZYqeE1G++4WuTB8Hvco=", + "requires": { + "p-reduce": "^1.0.0" + } + }, + "p-pipe": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/p-pipe/-/p-pipe-1.2.0.tgz", + "integrity": "sha1-SxoROZoRUgpneQ7loMHViB1r7+k=" + }, + "p-reduce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-reduce/-/p-reduce-1.0.0.tgz", + "integrity": "sha1-GMKw3ZNqRpClKfgjH1ig/bakffo=" + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=" + }, + "p-waterfall": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-waterfall/-/p-waterfall-1.0.0.tgz", + "integrity": "sha1-ftlLPOszMngjU69qrhGqn8I1uwA=", + "requires": { + "p-reduce": "^1.0.0" + } + }, + "pacote": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/pacote/-/pacote-9.1.0.tgz", + "integrity": "sha512-AFXaSWhOtQf3jHqEvg+ZYH/dfT8TKq6TKspJ4qEFwVVuh5aGvMIk6SNF8vqfzz+cBceDIs9drOcpBbrPai7i+g==", + "requires": { + "bluebird": "^3.5.1", + "cacache": "^11.0.2", + "figgy-pudding": "^3.2.1", + "get-stream": "^3.0.0", + "glob": "^7.1.2", + "lru-cache": "^4.1.3", + "make-fetch-happen": "^4.0.1", + "minimatch": "^3.0.4", + "minipass": "^2.3.3", + "mississippi": "^3.0.0", + "mkdirp": "^0.5.1", + "normalize-package-data": "^2.4.0", + "npm-package-arg": "^6.1.0", + "npm-packlist": "^1.1.10", + "npm-pick-manifest": "^2.1.0", + "npm-registry-fetch": "^3.0.0", + "osenv": "^0.1.5", + "promise-inflight": "^1.0.1", + "promise-retry": "^1.1.1", + "protoduck": "^5.0.0", + "rimraf": "^2.6.2", + "safe-buffer": "^5.1.2", + "semver": "^5.5.0", + "ssri": "^6.0.0", + "tar": "^4.4.3", + "unique-filename": "^1.1.0", + "which": "^1.3.0" + }, + "dependencies": { + "chownr": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.2.tgz", + "integrity": "sha512-GkfeAQh+QNy3wquu9oIZr6SS5x7wGdSgNQvD10X3r+AZr1Oys22HW8kAmDMvNg2+Dm0TeGaEuO8gFwdBXxwO8A==" + }, + "minizlib": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.2.1.tgz", + "integrity": "sha512-7+4oTUOWKg7AuL3vloEWekXY2/D20cevzsrNT2kGWm+39J9hGTCBv8VI5Pm5lXZ/o3/mdR4f8rflAPhnQb8mPA==", + "requires": { + "minipass": "^2.2.1" + } + }, + "tar": { + "version": "4.4.10", + "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.10.tgz", + "integrity": "sha512-g2SVs5QIxvo6OLp0GudTqEf05maawKUxXru104iaayWA09551tFCTI8f1Asb4lPfkBr91k07iL4c11XO3/b0tA==", + "requires": { + "chownr": "^1.1.1", + "fs-minipass": "^1.2.5", + "minipass": "^2.3.5", + "minizlib": "^1.2.1", + "mkdirp": "^0.5.0", + "safe-buffer": "^5.1.2", + "yallist": "^3.0.3" + }, + "dependencies": { + "minipass": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.3.5.tgz", + "integrity": "sha512-Gi1W4k059gyRbyVUZQ4mEqLm0YIUiGYfvxhF6SIlk3ui1WVxMTGfGdQ2SInh3PDrRTVvPKgULkpJtT4RH10+VA==", + "requires": { + "safe-buffer": "^5.1.2", + "yallist": "^3.0.0" + } + }, + "yallist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.3.tgz", + "integrity": "sha512-S+Zk8DEWE6oKpV+vI3qWkaK+jSbIK86pCwe2IF/xwIpQ8jEuxpw9NyaGjmp9+BoJv5FV2piqCDcoCtStppiq2A==" + } + } + }, + "yallist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.2.tgz", + "integrity": "sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k=" + } + } + }, + "parallel-transform": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.1.0.tgz", + "integrity": "sha1-1BDwZbBdojCB/NEPKIVMKb2jOwY=", + "requires": { + "cyclist": "~0.2.2", + "inherits": "^2.0.3", + "readable-stream": "^2.1.5" + } + }, + "parse-github-repo-url": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/parse-github-repo-url/-/parse-github-repo-url-1.4.1.tgz", + "integrity": "sha1-nn2LslKmy2ukJZUGC3v23z28H1A=" + }, + "parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", + "requires": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + } + }, + "pascalcase": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", + "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=" + }, + "path-dirname": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", + "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=" + }, + "path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=" + }, + "path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "requires": { + "pify": "^3.0.0" + } + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + }, + "pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=" + }, + "pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=" + }, + "pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "requires": { + "pinkie": "^2.0.0" + } + }, + "pkg-dir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz", + "integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=", + "requires": { + "find-up": "^2.1.0" + } + }, + "posix-character-classes": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", + "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=" + }, + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==" + }, + "promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=" + }, + "promise-retry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-1.1.1.tgz", + "integrity": "sha1-ZznpaOMFHaIM5kl/srUPaRHfPW0=", + "requires": { + "err-code": "^1.0.0", + "retry": "^0.10.0" + } + }, + "promzard": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/promzard/-/promzard-0.3.0.tgz", + "integrity": "sha1-JqXW7ox97kyxIggwWs+5O6OCqe4=", + "requires": { + "read": "1" + } + }, + "proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=" + }, + "protoduck": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/protoduck/-/protoduck-5.0.0.tgz", + "integrity": "sha512-agsGWD8/RZrS4ga6v82Fxb0RHIS2RZnbsSue6A9/MBRhB/jcqOANAMNrqM9900b8duj+Gx+T/JMy5IowDoO/hQ==", + "requires": { + "genfun": "^4.0.1" + } + }, + "pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=" + }, + "psl": { + "version": "1.1.29", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.29.tgz", + "integrity": "sha512-AeUmQ0oLN02flVHXWh9sSJF7mcdFq0ppid/JkErufc3hGIV/AMa8Fo9VgDo/cT2jFdOWoFvHp90qqBH54W+gjQ==" + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "pumpify": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", + "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", + "requires": { + "duplexify": "^3.6.0", + "inherits": "^2.0.3", + "pump": "^2.0.0" + }, + "dependencies": { + "pump": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", + "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", + "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=" + }, + "q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=" + }, + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" + }, + "quick-lru": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-1.1.0.tgz", + "integrity": "sha1-Q2CxfGETatOAeDl/8RQW4Ybc+7g=" + }, + "read": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", + "integrity": "sha1-s9oZvQUkMal2cdRKQmNK33ELQMQ=", + "requires": { + "mute-stream": "~0.0.4" + } + }, + "read-cmd-shim": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/read-cmd-shim/-/read-cmd-shim-1.0.1.tgz", + "integrity": "sha1-LV0Vd4ajfAVdIgd8MsU/gynpHHs=", + "requires": { + "graceful-fs": "^4.1.2" + } + }, + "read-package-json": { + "version": "2.0.13", + "resolved": "https://registry.npmjs.org/read-package-json/-/read-package-json-2.0.13.tgz", + "integrity": "sha512-/1dZ7TRZvGrYqE0UAfN6qQb5GYBsNcqS1C0tNK601CFOJmtHI7NIGXwetEPU/OtoFHZL3hDxm4rolFFVE9Bnmg==", + "requires": { + "glob": "^7.1.1", + "graceful-fs": "^4.1.2", + "json-parse-better-errors": "^1.0.1", + "normalize-package-data": "^2.0.0", + "slash": "^1.0.0" + } + }, + "read-package-tree": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/read-package-tree/-/read-package-tree-5.2.1.tgz", + "integrity": "sha512-2CNoRoh95LxY47LvqrehIAfUVda2JbuFE/HaGYs42bNrGG+ojbw1h3zOcPcQ+1GQ3+rkzNndZn85u1XyZ3UsIA==", + "requires": { + "debuglog": "^1.0.1", + "dezalgo": "^1.0.0", + "once": "^1.3.0", + "read-package-json": "^2.0.0", + "readdir-scoped-modules": "^1.0.0" + } + }, + "read-pkg": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", + "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", + "requires": { + "load-json-file": "^4.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^3.0.0" + } + }, + "read-pkg-up": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", + "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", + "requires": { + "find-up": "^1.0.0", + "read-pkg": "^1.0.0" + }, + "dependencies": { + "find-up": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", + "requires": { + "path-exists": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, + "load-json-file": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0", + "strip-bom": "^2.0.0" + } + }, + "parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "requires": { + "error-ex": "^1.2.0" + } + }, + "path-exists": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", + "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", + "requires": { + "pinkie-promise": "^2.0.0" + } + }, + "path-type": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", + "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", + "requires": { + "graceful-fs": "^4.1.2", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + } + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=" + }, + "read-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", + "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", + "requires": { + "load-json-file": "^1.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^1.0.0" + } + }, + "strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", + "requires": { + "is-utf8": "^0.2.0" + } + } + } + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "readdir-scoped-modules": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/readdir-scoped-modules/-/readdir-scoped-modules-1.0.2.tgz", + "integrity": "sha1-n6+jfShr5dksuuve4DDcm19AZ0c=", + "requires": { + "debuglog": "^1.0.1", + "dezalgo": "^1.0.0", + "graceful-fs": "^4.1.2", + "once": "^1.3.0" + } + }, + "redent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-2.0.0.tgz", + "integrity": "sha1-wbIAe0LVfrE4kHmzyDM2OdXhzKo=", + "requires": { + "indent-string": "^3.0.0", + "strip-indent": "^2.0.0" + } + }, + "regex-not": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", + "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", + "requires": { + "extend-shallow": "^3.0.2", + "safe-regex": "^1.1.0" + } + }, + "repeat-element": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", + "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==" + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" + }, + "repeating": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", + "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", + "requires": { + "is-finite": "^1.0.0" + } + }, + "request": { + "version": "2.88.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", + "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.0", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.4.3", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" + }, + "require-main-filename": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", + "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=" + }, + "resolve-cwd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz", + "integrity": "sha1-AKn3OHVW4nA46uIyyqNypqWbZlo=", + "requires": { + "resolve-from": "^3.0.0" + }, + "dependencies": { + "resolve-from": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", + "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=" + } + } + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==" + }, + "resolve-url": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", + "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=" + }, + "restore-cursor": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", + "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", + "requires": { + "onetime": "^2.0.0", + "signal-exit": "^3.0.2" + } + }, + "ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==" + }, + "retry": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.10.1.tgz", + "integrity": "sha1-52OI0heZLCUnUCQdPTlW/tmNj/Q=" + }, + "right-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", + "integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=", + "requires": { + "align-text": "^0.1.1" + } + }, + "rimraf": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", + "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", + "requires": { + "glob": "^7.0.5" + } + }, + "run-async": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", + "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=", + "requires": { + "is-promise": "^2.1.0" + } + }, + "run-queue": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz", + "integrity": "sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec=", + "requires": { + "aproba": "^1.1.1" + } + }, + "rxjs": { + "version": "5.5.11", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-5.5.11.tgz", + "integrity": "sha512-3bjO7UwWfA2CV7lmwYMBzj4fQ6Cq+ftHc2MvUe+WMS7wcdJ1LosDWmdjPQanYp2dBRj572p7PeU81JUxHKOcBA==", + "requires": { + "symbol-observable": "1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "safe-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", + "requires": { + "ret": "~0.1.10" + } + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "semver": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.1.tgz", + "integrity": "sha512-PqpAxfrEhlSUWge8dwIp4tZnQ25DIOthpiaHNIthsjEFQD6EvqUKUDM7L8O2rShkFccYo1VjJR0coWfNkCubRw==" + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" + }, + "set-value": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz", + "integrity": "sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==", + "requires": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.3", + "split-string": "^3.0.1" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", + "requires": { + "shebang-regex": "^1.0.0" + } + }, + "shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=" + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" + }, + "slash": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", + "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=" + }, + "slide": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz", + "integrity": "sha1-VusCfWW00tzmyy4tMsTUr8nh1wc=" + }, + "smart-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.0.1.tgz", + "integrity": "sha512-RFqinRVJVcCAL9Uh1oVqE6FZkqsyLiVOYEZ20TqIOjuX7iFVJ+zsbs4RIghnw/pTs7mZvt8ZHhvm1ZUrR4fykg==" + }, + "snapdragon": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", + "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", + "requires": { + "base": "^0.11.1", + "debug": "^2.2.0", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "map-cache": "^0.2.2", + "source-map": "^0.5.6", + "source-map-resolve": "^0.5.0", + "use": "^3.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "^0.1.0" + } + }, + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + } + } + }, + "snapdragon-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", + "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", + "requires": { + "define-property": "^1.0.0", + "isobject": "^3.0.0", + "snapdragon-util": "^3.0.1" + }, + "dependencies": { + "define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", + "requires": { + "is-descriptor": "^1.0.0" + } + }, + "is-accessor-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", + "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-data-descriptor": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", + "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", + "requires": { + "kind-of": "^6.0.0" + } + }, + "is-descriptor": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", + "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", + "requires": { + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" + } + } + } + }, + "snapdragon-util": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", + "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", + "requires": { + "kind-of": "^3.2.0" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "socks": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.2.1.tgz", + "integrity": "sha512-0GabKw7n9mI46vcNrVfs0o6XzWzjVa3h6GaSo2UPxtWAROXUWavfJWh1M4PR5tnE0dcnQXZIDFP4yrAysLze/w==", + "requires": { + "ip": "^1.1.5", + "smart-buffer": "^4.0.1" + } + }, + "socks-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-4.0.1.tgz", + "integrity": "sha512-Kezx6/VBguXOsEe5oU3lXYyKMi4+gva72TwJ7pQY5JfqUx2nMk7NXA6z/mpNqIlfQjWYVfeuNvQjexiTaTn6Nw==", + "requires": { + "agent-base": "~4.2.0", + "socks": "~2.2.0" + } + }, + "sort-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-2.0.0.tgz", + "integrity": "sha1-ZYU1WEhh7JfXMNbPQYIuH1ZoQSg=", + "requires": { + "is-plain-obj": "^1.0.0" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" + }, + "source-map-resolve": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz", + "integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==", + "requires": { + "atob": "^2.1.1", + "decode-uri-component": "^0.2.0", + "resolve-url": "^0.2.1", + "source-map-url": "^0.4.0", + "urix": "^0.1.0" + } + }, + "source-map-url": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", + "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=" + }, + "spdx-correct": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.0.0.tgz", + "integrity": "sha512-N19o9z5cEyc8yQQPukRCZ9EUmb4HUpnrmaL/fxS2pBo2jbfcFRVuFZ/oFC+vZz0MNNk0h80iMn5/S6qGZOL5+g==", + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.1.0.tgz", + "integrity": "sha512-4K1NsmrlCU1JJgUrtgEeTVyfx8VaYea9J9LvARxhbHtVtohPs/gFGG5yy49beySjlIMhhXZ4QqujIZEfS4l6Cg==" + }, + "spdx-expression-parse": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", + "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.0.tgz", + "integrity": "sha512-2+EPwgbnmOIl8HjGBXXMd9NAu02vLjOO1nWw4kmeRDFyHn+M/ETfHxQUK0oXg8ctgVnl9t3rosNVsZ1jG61nDA==" + }, + "split": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", + "integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==", + "requires": { + "through": "2" + } + }, + "split-string": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", + "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "requires": { + "extend-shallow": "^3.0.0" + } + }, + "split2": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-2.2.0.tgz", + "integrity": "sha512-RAb22TG39LhI31MbreBgIuKiIKhVsawfTgEGqKHTK87aG+ul/PB8Sqoi3I7kVdRWiCfrKxK3uo4/YUkpNvhPbw==", + "requires": { + "through2": "^2.0.2" + } + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" + }, + "sshpk": { + "version": "1.14.2", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.2.tgz", + "integrity": "sha1-xvxhZIo9nE52T9P8306hBeSSupg=", + "requires": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + } + }, + "ssri": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz", + "integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==", + "requires": { + "figgy-pudding": "^3.5.1" + } + }, + "static-extend": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", + "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", + "requires": { + "define-property": "^0.2.5", + "object-copy": "^0.1.0" + }, + "dependencies": { + "define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", + "requires": { + "is-descriptor": "^0.1.0" + } + } + } + }, + "stream-each": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/stream-each/-/stream-each-1.2.3.tgz", + "integrity": "sha512-vlMC2f8I2u/bZGqkdfLQW/13Zihpej/7PmSiMQsbYddxuTsJp8vRe2x2FvVExZg7FaOds43ROAuFJwPR4MTZLw==", + "requires": { + "end-of-stream": "^1.1.0", + "stream-shift": "^1.0.0" + } + }, + "stream-shift": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.0.tgz", + "integrity": "sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI=" + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=" + }, + "strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=" + }, + "strip-indent": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-2.0.0.tgz", + "integrity": "sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g=" + }, + "strong-log-transformer": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/strong-log-transformer/-/strong-log-transformer-1.0.6.tgz", + "integrity": "sha1-9/uTdYpppXEUAYEnfuoMLrEwH6M=", + "requires": { + "byline": "^5.0.0", + "duplexer": "^0.1.1", + "minimist": "^0.1.0", + "moment": "^2.6.0", + "through": "^2.3.4" + } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "^3.0.0" + } + }, + "symbol-observable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.0.1.tgz", + "integrity": "sha1-g0D8RwLDEi310iKI+IKD9RPT/dQ=" + }, + "tar": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.2.tgz", + "integrity": "sha512-FCEhQ/4rE1zYv9rYXJw/msRqsnmlje5jHP6huWeBZ704jUTy02c5AZyWujpMR1ax6mVw9NyJMfuK2CMDWVIfgA==", + "requires": { + "block-stream": "*", + "fstream": "^1.0.12", + "inherits": "2" + }, + "dependencies": { + "fstream": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", + "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", + "requires": { + "graceful-fs": "^4.1.2", + "inherits": "~2.0.0", + "mkdirp": ">=0.5 0", + "rimraf": "2" + } + } + } + }, + "temp-dir": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-1.0.0.tgz", + "integrity": "sha1-CnwOom06Oa+n4OvqnB/AvE2qAR0=" + }, + "temp-write": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/temp-write/-/temp-write-3.4.0.tgz", + "integrity": "sha1-jP9jD7fp2gXwR8dM5M5NaFRX1JI=", + "requires": { + "graceful-fs": "^4.1.2", + "is-stream": "^1.1.0", + "make-dir": "^1.0.0", + "pify": "^3.0.0", + "temp-dir": "^1.0.0", + "uuid": "^3.0.1" + } + }, + "text-extensions": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/text-extensions/-/text-extensions-1.7.0.tgz", + "integrity": "sha512-AKXZeDq230UaSzaO5s3qQUZOaC7iKbzq0jOFL614R7d9R593HLqAOL0cYoqLdkNrjBSOdmoQI06yigq1TSBXAg==" + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" + }, + "through2": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.3.tgz", + "integrity": "sha1-AARWmzfHx0ujnEPzzteNGtlBQL4=", + "requires": { + "readable-stream": "^2.1.5", + "xtend": "~4.0.1" + } + }, + "tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "requires": { + "os-tmpdir": "~1.0.2" + } + }, + "to-object-path": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", + "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", + "requires": { + "kind-of": "^3.0.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + } + } + }, + "to-regex": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", + "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", + "requires": { + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "regex-not": "^1.0.2", + "safe-regex": "^1.1.0" + } + }, + "to-regex-range": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", + "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "requires": { + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" + } + }, + "tough-cookie": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", + "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", + "requires": { + "psl": "^1.1.24", + "punycode": "^1.4.1" + } + }, + "tr46": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", + "integrity": "sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=", + "requires": { + "punycode": "^2.1.0" + }, + "dependencies": { + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + } + } + }, + "trim-newlines": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-2.0.0.tgz", + "integrity": "sha1-tAPQuRvlDDMd/EuC7s6yLD3hbSA=" + }, + "trim-off-newlines": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/trim-off-newlines/-/trim-off-newlines-1.0.1.tgz", + "integrity": "sha1-n5up2e+odkw4dpi8v+sshI8RrbM=" + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "requires": { + "safe-buffer": "^5.0.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" + }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" + }, + "typescript": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.0.3.tgz", + "integrity": "sha512-tEu6DGxGgRJPb/mVPIZ48e69xCn2yRmCgYmDugAVwmJ6o+0u1RI18eO7E7WBTLYLaEVVOhwQmcdhQHweux/WPg==" + }, + "uglify-js": { + "version": "2.8.29", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", + "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=", + "requires": { + "source-map": "~0.5.1", + "uglify-to-browserify": "~1.0.0", + "yargs": "~3.10.0" + } + }, + "uglify-to-browserify": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", + "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=" + }, + "uid-number": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/uid-number/-/uid-number-0.0.6.tgz", + "integrity": "sha1-DqEOgDXo61uOREnwbaHHMGY7qoE=" + }, + "umask": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/umask/-/umask-1.1.0.tgz", + "integrity": "sha1-8pzr8B31F5ErtY/5xOUP3o4zMg0=" + }, + "union-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz", + "integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=", + "requires": { + "arr-union": "^3.1.0", + "get-value": "^2.0.6", + "is-extendable": "^0.1.1", + "set-value": "^0.4.3" + }, + "dependencies": { + "extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", + "requires": { + "is-extendable": "^0.1.0" + } + }, + "set-value": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz", + "integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=", + "requires": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.1", + "to-object-path": "^0.3.0" + } + } + } + }, + "unique-filename": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.0.tgz", + "integrity": "sha1-0F8v5AMlYIcfMOk8vnNe6iAVFPM=", + "requires": { + "unique-slug": "^2.0.0" + } + }, + "unique-slug": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.0.tgz", + "integrity": "sha1-22Z258fMBimHj/GWCXx4hVrp9Ks=", + "requires": { + "imurmurhash": "^0.1.4" + } + }, + "universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==" + }, + "unset-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", + "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", + "requires": { + "has-value": "^0.3.1", + "isobject": "^3.0.0" + }, + "dependencies": { + "has-value": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", + "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", + "requires": { + "get-value": "^2.0.3", + "has-values": "^0.1.4", + "isobject": "^2.0.0" + }, + "dependencies": { + "isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", + "requires": { + "isarray": "1.0.0" + } + } + } + }, + "has-values": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", + "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=" + } + } + }, + "urix": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", + "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=" + }, + "use": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", + "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==" + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" + }, + "validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "requires": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "validate-npm-package-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-3.0.0.tgz", + "integrity": "sha1-X6kS2B630MdK/BQN5zF/DKffQ34=", + "requires": { + "builtins": "^1.0.3" + } + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=", + "requires": { + "defaults": "^1.0.3" + } + }, + "webidl-conversions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==" + }, + "whatwg-url": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-6.5.0.tgz", + "integrity": "sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ==", + "requires": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + }, + "which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "requires": { + "isexe": "^2.0.0" + } + }, + "which-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" + }, + "wide-align": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", + "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", + "requires": { + "string-width": "^1.0.2 || 2" + } + }, + "window-size": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", + "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=" + }, + "wordwrap": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", + "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=" + }, + "wrap-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", + "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", + "requires": { + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "write-file-atomic": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.3.0.tgz", + "integrity": "sha512-xuPeK4OdjWqtfi59ylvVL0Yn35SF3zgcAcv7rBPFHVaEapaDr4GdGgm3j7ckTwH9wHL7fGmgfAnb0+THrHb8tA==", + "requires": { + "graceful-fs": "^4.1.11", + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.2" + } + }, + "write-json-file": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/write-json-file/-/write-json-file-2.3.0.tgz", + "integrity": "sha1-K2TIozAE1UuGmMdtWFp3zrYdoy8=", + "requires": { + "detect-indent": "^5.0.0", + "graceful-fs": "^4.1.2", + "make-dir": "^1.0.0", + "pify": "^3.0.0", + "sort-keys": "^2.0.0", + "write-file-atomic": "^2.0.0" + } + }, + "write-pkg": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/write-pkg/-/write-pkg-3.2.0.tgz", + "integrity": "sha512-tX2ifZ0YqEFOF1wjRW2Pk93NLsj02+n1UP5RvO6rCs0K6R2g1padvf006cY74PQJKMGS2r42NK7FD0dG6Y6paw==", + "requires": { + "sort-keys": "^2.0.0", + "write-json-file": "^2.2.0" + } + }, + "xregexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xregexp/-/xregexp-4.0.0.tgz", + "integrity": "sha512-PHyM+sQouu7xspQQwELlGwwd05mXUFqwFYfqPO0cC7x4fxyHnnuetmQr6CjJiafIDoH4MogHb9dOoJzR/Y4rFg==" + }, + "xtend": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", + "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=" + }, + "y18n": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", + "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==" + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=" + }, + "yargs": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", + "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", + "requires": { + "camelcase": "^1.0.2", + "cliui": "^2.1.0", + "decamelize": "^1.0.0", + "window-size": "0.1.0" + } + }, + "yargs-parser": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-10.1.0.tgz", + "integrity": "sha512-VCIyR1wJoEBZUqk5PA+oOBF6ypbwh5aNB3I50guxAL/quggdfs4TtNHQrSazFA3fYZ+tEqfs0zIGlv0c/rgjbQ==", + "requires": { + "camelcase": "^4.1.0" + }, + "dependencies": { + "camelcase": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", + "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=" + } + } + } + } +} diff --git a/packages/optimizely-sdk/__mocks__/@react-native-async-storage/async-storage.ts b/packages/optimizely-sdk/__mocks__/@react-native-async-storage/async-storage.ts new file mode 100644 index 000000000..1ba23231b --- /dev/null +++ b/packages/optimizely-sdk/__mocks__/@react-native-async-storage/async-storage.ts @@ -0,0 +1,52 @@ +/** + * Copyright 2022, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +let items: {[key: string]: string} = {} + +export default class AsyncStorage { + + static getItem(key: string, callback?: (error?: Error, result?: string) => void): Promise { + return new Promise(resolve => { + setTimeout(() => resolve(items[key] || null), 1) + }) + } + + static setItem(key: string, value: string, callback?: (error?: Error) => void): Promise { + return new Promise((resolve) => { + setTimeout(() => { + items[key] = value + resolve() + }, 1) + }) + } + + static removeItem(key: string, callback?: (error?: Error, result?: string) => void): Promise { + return new Promise(resolve => { + setTimeout(() => { + items[key] && delete items[key] + // @ts-ignore + resolve() + }, 1) + }) + } + + static dumpItems(): {[key: string]: string} { + return items + } + + static clearStore(): void { + items = {} + } +} diff --git a/packages/optimizely-sdk/__mocks__/@react-native-community/netinfo.ts b/packages/optimizely-sdk/__mocks__/@react-native-community/netinfo.ts new file mode 100644 index 000000000..12fab972a --- /dev/null +++ b/packages/optimizely-sdk/__mocks__/@react-native-community/netinfo.ts @@ -0,0 +1,24 @@ +/** + * Copyright 2022, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +let localCallback: any + +export function addEventListener(callback: any) { + localCallback = callback +} + +export function triggerInternetState(isInternetReachable: boolean) { + localCallback({ isInternetReachable }) +} diff --git a/packages/optimizely-sdk/jest.config.js b/packages/optimizely-sdk/jest.config.js index a2d8c2bae..fd4abcd54 100644 --- a/packages/optimizely-sdk/jest.config.js +++ b/packages/optimizely-sdk/jest.config.js @@ -1,6 +1,6 @@ module.exports = { "transform": { - "^.+\\.tsx?$": "ts-jest" + "^.+\\.(ts|tsx|js|jsx)$": "ts-jest", }, "testRegex": "(/tests/.*|(\\.|/)(test|spec))\\.tsx?$", "moduleFileExtensions": [ @@ -10,5 +10,5 @@ module.exports = { "jsx", "json", "node" - ], + ] } diff --git a/packages/optimizely-sdk/lib/core/event_builder/build_event_v1.ts b/packages/optimizely-sdk/lib/core/event_builder/build_event_v1.ts index ab00cfd76..b1f5b271d 100644 --- a/packages/optimizely-sdk/lib/core/event_builder/build_event_v1.ts +++ b/packages/optimizely-sdk/lib/core/event_builder/build_event_v1.ts @@ -1,5 +1,5 @@ /** - * Copyright 2021 Optimizely + * Copyright 2021-2022, Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,7 +17,7 @@ import { EventTags, ConversionEvent, ImpressionEvent, -} from '@optimizely/js-sdk-event-processor'; +} from '../../modules/event_processor'; import { Event } from '../../shared_types'; diff --git a/packages/optimizely-sdk/lib/core/event_builder/index.ts b/packages/optimizely-sdk/lib/core/event_builder/index.ts index 7a115b357..093994aa0 100644 --- a/packages/optimizely-sdk/lib/core/event_builder/index.ts +++ b/packages/optimizely-sdk/lib/core/event_builder/index.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import { LoggerFacade } from '../../modules/logging'; -import { EventV1 as CommonEventParams } from '@optimizely/js-sdk-event-processor'; +import { EventV1 as CommonEventParams } from '../../modules/event_processor'; import fns from '../../utils/fns'; import { CONTROL_ATTRIBUTES, RESERVED_EVENT_KEYWORDS } from '../../utils/enums'; diff --git a/packages/optimizely-sdk/lib/index.browser.ts b/packages/optimizely-sdk/lib/index.browser.ts index 28caf8aad..0db641b27 100644 --- a/packages/optimizely-sdk/lib/index.browser.ts +++ b/packages/optimizely-sdk/lib/index.browser.ts @@ -20,7 +20,7 @@ import { getErrorHandler, LogLevel } from './modules/logging'; -import { LocalStoragePendingEventsDispatcher } from '@optimizely/js-sdk-event-processor'; +import { LocalStoragePendingEventsDispatcher } from '../lib/modules/event_processor'; import configValidator from './utils/config_validator'; import defaultErrorHandler from './plugins/error_handler'; import defaultEventDispatcher from './plugins/event_dispatcher/index.browser'; diff --git a/packages/optimizely-sdk/lib/index.react_native.tests.js b/packages/optimizely-sdk/lib/index.react_native.tests.js deleted file mode 100644 index 7fcd5728b..000000000 --- a/packages/optimizely-sdk/lib/index.react_native.tests.js +++ /dev/null @@ -1,330 +0,0 @@ -/** - * Copyright 2019-2020, 2022 Optimizely - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import { assert } from 'chai'; -import sinon from 'sinon'; -import * as logging from './modules/logging/logger'; -import * as eventProcessor from './plugins/event_processor'; - -import Optimizely from './optimizely'; -import testData from './tests/test_data'; -import packageJSON from '../package.json'; -import optimizelyFactory from './index.react_native'; -import configValidator from './utils/config_validator'; -import eventProcessorConfigValidator from './utils/event_processor_config_validator'; - -describe('javascript-sdk/react-native', function () { - var clock; - beforeEach(function () { - sinon.stub(optimizelyFactory.eventDispatcher, 'dispatchEvent'); - clock = sinon.useFakeTimers(new Date()); - }); - - afterEach(function () { - optimizelyFactory.eventDispatcher.dispatchEvent.restore(); - clock.restore(); - }); - - describe('APIs', function () { - it('should expose logger, errorHandler, eventDispatcher and enums', function () { - assert.isDefined(optimizelyFactory.logging); - assert.isDefined(optimizelyFactory.logging.createLogger); - assert.isDefined(optimizelyFactory.logging.createNoOpLogger); - assert.isDefined(optimizelyFactory.errorHandler); - assert.isDefined(optimizelyFactory.eventDispatcher); - assert.isDefined(optimizelyFactory.enums); - }); - - describe('createInstance', function () { - var fakeErrorHandler = { handleError: function () { } }; - var fakeEventDispatcher = { dispatchEvent: function () { } }; - var silentLogger; - - beforeEach(function () { - silentLogger = optimizelyFactory.logging.createLogger({ - logLevel: optimizelyFactory.enums.LOG_LEVEL.INFO, - logToConsole: false, - }); - sinon.spy(console, 'error'); - sinon.stub(configValidator, 'validate'); - }); - - afterEach(function () { - console.error.restore(); - configValidator.validate.restore(); - }); - - it('should not throw if the provided config is not valid', function () { - configValidator.validate.throws(new Error('Invalid config or something')); - assert.doesNotThrow(function () { - var optlyInstance = optimizelyFactory.createInstance({ - datafile: {}, - logger: silentLogger, - }); - // Invalid datafile causes onReady Promise rejection - catch this error - optlyInstance.onReady().catch(function () { }); - }); - }); - - it('should create an instance of optimizely', function () { - var optlyInstance = optimizelyFactory.createInstance({ - datafile: {}, - errorHandler: fakeErrorHandler, - eventDispatcher: fakeEventDispatcher, - logger: silentLogger, - }); - // Invalid datafile causes onReady Promise rejection - catch this error - optlyInstance.onReady().catch(function () { }); - - assert.instanceOf(optlyInstance, Optimizely); - assert.equal(optlyInstance.clientVersion, '4.9.2'); - }); - - it('should set the React Native JS client engine and javascript SDK version', function () { - var optlyInstance = optimizelyFactory.createInstance({ - datafile: {}, - errorHandler: fakeErrorHandler, - eventDispatcher: fakeEventDispatcher, - logger: silentLogger, - }); - // Invalid datafile causes onReady Promise rejection - catch this error - optlyInstance.onReady().catch(function () { }); - assert.equal('react-native-js-sdk', optlyInstance.clientEngine); - assert.equal(packageJSON.version, optlyInstance.clientVersion); - }); - - it('should allow passing of "react-sdk" as the clientEngine and convert it to "react-native-sdk"', function () { - var optlyInstance = optimizelyFactory.createInstance({ - clientEngine: 'react-sdk', - datafile: {}, - errorHandler: fakeErrorHandler, - eventDispatcher: fakeEventDispatcher, - logger: silentLogger, - }); - // Invalid datafile causes onReady Promise rejection - catch this error - optlyInstance.onReady().catch(function () { }); - assert.equal('react-native-sdk', optlyInstance.clientEngine); - }); - - it('should activate with provided event dispatcher', function () { - var optlyInstance = optimizelyFactory.createInstance({ - datafile: testData.getTestProjectConfig(), - errorHandler: fakeErrorHandler, - eventDispatcher: optimizelyFactory.eventDispatcher, - logger: silentLogger, - }); - var activate = optlyInstance.activate('testExperiment', 'testUser'); - assert.strictEqual(activate, 'control'); - }); - - describe('when no event dispatcher passed to createInstance', function () { - it('uses the default event dispatcher', function () { - var optlyInstance = optimizelyFactory.createInstance({ - datafile: testData.getTestProjectConfig(), - errorHandler: fakeErrorHandler, - logger: silentLogger, - }); - optlyInstance.activate('testExperiment', 'testUser'); - clock.tick(30001) - sinon.assert.calledOnce(optimizelyFactory.eventDispatcher.dispatchEvent); - }); - }); - - describe('when passing in logLevel', function () { - beforeEach(function () { - sinon.stub(logging, 'setLogLevel'); - }); - - afterEach(function () { - logging.setLogLevel.restore(); - }); - - it('should call logging.setLogLevel', function () { - optimizelyFactory.createInstance({ - datafile: testData.getTestProjectConfig(), - logLevel: optimizelyFactory.enums.LOG_LEVEL.ERROR, - }); - sinon.assert.calledOnce(logging.setLogLevel); - sinon.assert.calledWithExactly(logging.setLogLevel, optimizelyFactory.enums.LOG_LEVEL.ERROR); - }); - }); - - describe('when passing in logger', function () { - beforeEach(function () { - sinon.stub(logging, 'setLogHandler'); - }); - - afterEach(function () { - logging.setLogHandler.restore(); - }); - - it('should call logging.setLogHandler with the supplied logger', function () { - var fakeLogger = { log: function () { } }; - optimizelyFactory.createInstance({ - datafile: testData.getTestProjectConfig(), - logger: fakeLogger, - }); - sinon.assert.calledOnce(logging.setLogHandler); - sinon.assert.calledWithExactly(logging.setLogHandler, fakeLogger); - }); - }); - - describe('event processor configuration', function () { - var eventProcessorSpy; - beforeEach(function () { - eventProcessorSpy = sinon.spy(eventProcessor, 'createEventProcessor'); - }); - - afterEach(function () { - eventProcessor.createEventProcessor.restore(); - }); - - it('should use default event flush interval when none is provided', function () { - optimizelyFactory.createInstance({ - datafile: testData.getTestProjectConfigWithFeatures(), - errorHandler: fakeErrorHandler, - eventDispatcher: fakeEventDispatcher, - logger: silentLogger, - }); - sinon.assert.calledWithExactly( - eventProcessorSpy, - sinon.match({ - flushInterval: 1000, - }) - ); - }); - - describe('with an invalid flush interval', function () { - beforeEach(function () { - sinon.stub(eventProcessorConfigValidator, 'validateEventFlushInterval').returns(false); - }); - - afterEach(function () { - eventProcessorConfigValidator.validateEventFlushInterval.restore(); - }); - - it('should ignore the event flush interval and use the default instead', function () { - optimizelyFactory.createInstance({ - datafile: testData.getTestProjectConfigWithFeatures(), - errorHandler: fakeErrorHandler, - eventDispatcher: fakeEventDispatcher, - logger: silentLogger, - eventFlushInterval: ['invalid', 'flush', 'interval'], - }); - sinon.assert.calledWithExactly( - eventProcessorSpy, - sinon.match({ - flushInterval: 1000, - }) - ); - }); - }); - - describe('with a valid flush interval', function () { - beforeEach(function () { - sinon.stub(eventProcessorConfigValidator, 'validateEventFlushInterval').returns(true); - }); - - afterEach(function () { - eventProcessorConfigValidator.validateEventFlushInterval.restore(); - }); - - it('should use the provided event flush interval', function () { - optimizelyFactory.createInstance({ - datafile: testData.getTestProjectConfigWithFeatures(), - errorHandler: fakeErrorHandler, - eventDispatcher: fakeEventDispatcher, - logger: silentLogger, - eventFlushInterval: 9000, - }); - sinon.assert.calledWithExactly( - eventProcessorSpy, - sinon.match({ - flushInterval: 9000, - }) - ); - }); - }); - - it('should use default event batch size when none is provided', function () { - optimizelyFactory.createInstance({ - datafile: testData.getTestProjectConfigWithFeatures(), - errorHandler: fakeErrorHandler, - eventDispatcher: fakeEventDispatcher, - logger: silentLogger, - }); - sinon.assert.calledWithExactly( - eventProcessorSpy, - sinon.match({ - batchSize: 10, - }) - ); - }); - - describe('with an invalid event batch size', function () { - beforeEach(function () { - sinon.stub(eventProcessorConfigValidator, 'validateEventBatchSize').returns(false); - }); - - afterEach(function () { - eventProcessorConfigValidator.validateEventBatchSize.restore(); - }); - - it('should ignore the event batch size and use the default instead', function () { - optimizelyFactory.createInstance({ - datafile: testData.getTestProjectConfigWithFeatures(), - errorHandler: fakeErrorHandler, - eventDispatcher: fakeEventDispatcher, - logger: silentLogger, - eventBatchSize: null, - }); - sinon.assert.calledWithExactly( - eventProcessorSpy, - sinon.match({ - batchSize: 10, - }) - ); - }); - }); - - describe('with a valid event batch size', function () { - beforeEach(function () { - sinon.stub(eventProcessorConfigValidator, 'validateEventBatchSize').returns(true); - }); - - afterEach(function () { - eventProcessorConfigValidator.validateEventBatchSize.restore(); - }); - - it('should use the provided event batch size', function () { - optimizelyFactory.createInstance({ - datafile: testData.getTestProjectConfigWithFeatures(), - errorHandler: fakeErrorHandler, - eventDispatcher: fakeEventDispatcher, - logger: silentLogger, - eventBatchSize: 300, - }); - sinon.assert.calledWithExactly( - eventProcessorSpy, - sinon.match({ - batchSize: 300, - }) - ); - }); - }); - }); - }); - }); -}); diff --git a/packages/optimizely-sdk/lib/index.react_native.ts b/packages/optimizely-sdk/lib/index.react_native.ts index f781d9fd0..41ed88e46 100644 --- a/packages/optimizely-sdk/lib/index.react_native.ts +++ b/packages/optimizely-sdk/lib/index.react_native.ts @@ -29,7 +29,7 @@ import * as loggerPlugin from './plugins/logger/index.react_native'; import defaultEventDispatcher from './plugins/event_dispatcher/index.browser'; import eventProcessorConfigValidator from './utils/event_processor_config_validator'; import { createNotificationCenter } from './core/notification_center'; -import { createEventProcessor } from './plugins/event_processor'; +import { createEventProcessor } from './plugins/event_processor/index.react_native'; import { OptimizelyDecideOption, Client, Config } from './shared_types'; import { createHttpPollingDatafileManager } from './plugins/datafile_manager/http_polling_datafile_manager'; diff --git a/packages/optimizely-sdk/lib/modules/event_processor/eventDispatcher.ts b/packages/optimizely-sdk/lib/modules/event_processor/eventDispatcher.ts new file mode 100644 index 000000000..15d261cf2 --- /dev/null +++ b/packages/optimizely-sdk/lib/modules/event_processor/eventDispatcher.ts @@ -0,0 +1,32 @@ +/** + * Copyright 2022, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { EventV1 } from "./v1/buildEventV1"; + +export type EventDispatcherResponse = { + statusCode: number +} + +export type EventDispatcherCallback = (response: EventDispatcherResponse) => void + +export interface EventDispatcher { + dispatchEvent(event: EventV1Request, callback: EventDispatcherCallback): void +} + +export interface EventV1Request { + url: string + httpVerb: 'POST' | 'PUT' | 'GET' | 'PATCH' + params: EventV1, +} diff --git a/packages/optimizely-sdk/lib/modules/event_processor/eventProcessor.ts b/packages/optimizely-sdk/lib/modules/event_processor/eventProcessor.ts new file mode 100644 index 000000000..9dd91bdc6 --- /dev/null +++ b/packages/optimizely-sdk/lib/modules/event_processor/eventProcessor.ts @@ -0,0 +1,82 @@ +/** + * Copyright 2022, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// TODO change this to use Managed from js-sdk-models when available +import { Managed } from './managed' +import { ConversionEvent, ImpressionEvent } from './events' +import { EventV1Request } from './eventDispatcher' +import { EventQueue, DefaultEventQueue, SingleEventQueue, EventQueueSink } from './eventQueue' +import { getLogger } from '../logging' +import { NOTIFICATION_TYPES } from '../../utils/enums' +import { NotificationSender } from '../../core/notification_center' + +export const DEFAULT_FLUSH_INTERVAL = 30000 // Unit is ms - default flush interval is 30s +export const DEFAULT_BATCH_SIZE = 10 + +const logger = getLogger('EventProcessor') + +export type ProcessableEvent = ConversionEvent | ImpressionEvent + +export type EventDispatchResult = { result: boolean; event: ProcessableEvent } + +export interface EventProcessor extends Managed { + process(event: ProcessableEvent): void +} + +export function validateAndGetFlushInterval(flushInterval: number): number { + if (flushInterval <= 0) { + logger.warn( + `Invalid flushInterval ${flushInterval}, defaulting to ${DEFAULT_FLUSH_INTERVAL}`, + ) + flushInterval = DEFAULT_FLUSH_INTERVAL + } + return flushInterval +} + +export function validateAndGetBatchSize(batchSize: number): number { + batchSize = Math.floor(batchSize) + if (batchSize < 1) { + logger.warn( + `Invalid batchSize ${batchSize}, defaulting to ${DEFAULT_BATCH_SIZE}`, + ) + batchSize = DEFAULT_BATCH_SIZE + } + batchSize = Math.max(1, batchSize) + return batchSize +} + +export function getQueue(batchSize: number, flushInterval: number, sink: EventQueueSink, batchComparator: (eventA: ProcessableEvent, eventB: ProcessableEvent) => boolean ): EventQueue { + let queue: EventQueue + if (batchSize > 1) { + queue = new DefaultEventQueue({ + flushInterval, + maxQueueSize: batchSize, + sink, + batchComparator, + }) + } else { + queue = new SingleEventQueue({ sink }) + } + return queue +} + +export function sendEventNotification(notificationSender: NotificationSender | undefined, event: EventV1Request): void { + if (notificationSender) { + notificationSender.sendNotifications( + NOTIFICATION_TYPES.LOG_EVENT, + event, + ) + } +} diff --git a/packages/optimizely-sdk/lib/modules/event_processor/eventQueue.ts b/packages/optimizely-sdk/lib/modules/event_processor/eventQueue.ts new file mode 100644 index 000000000..a61d37918 --- /dev/null +++ b/packages/optimizely-sdk/lib/modules/event_processor/eventQueue.ts @@ -0,0 +1,159 @@ +/** + * Copyright 2022, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { getLogger } from '../logging' +// TODO change this to use Managed from js-sdk-models when available +import { Managed } from './managed' + +const logger = getLogger('EventProcessor') + +export type EventQueueSink = (buffer: K[]) => Promise + +export interface EventQueue extends Managed { + enqueue(event: K): void +} + +export interface EventQueueFactory { + createEventQueue(config: { + sink: EventQueueSink + flushInterval: number + maxQueueSize: number + }): EventQueue +} + +class Timer { + private timeout: number + private callback: () => void + private timeoutId?: number + + constructor({ timeout, callback }: { timeout: number; callback: () => void }) { + this.timeout = Math.max(timeout, 0) + this.callback = callback + } + + start(): void { + this.timeoutId = setTimeout(this.callback, this.timeout) as any + } + + refresh(): void { + this.stop() + this.start() + } + + stop(): void { + if (this.timeoutId) { + clearTimeout(this.timeoutId as any) + } + } +} + +export class SingleEventQueue implements EventQueue { + private sink: EventQueueSink + + constructor({ sink }: { sink: EventQueueSink }) { + this.sink = sink + } + + start(): void { + // no-op + } + + stop(): Promise { + // no-op + return Promise.resolve() + } + + enqueue(event: K): void { + this.sink([event]) + } +} + +export class DefaultEventQueue implements EventQueue { + // expose for testing + public timer: Timer + private buffer: K[] + private maxQueueSize: number + private sink: EventQueueSink + // batchComparator is called to determine whether two events can be included + // together in the same batch + private batchComparator: (eventA: K, eventB: K) => boolean + private started: boolean + + constructor({ + flushInterval, + maxQueueSize, + sink, + batchComparator, + }: { + flushInterval: number + maxQueueSize: number + sink: EventQueueSink + batchComparator: (eventA: K, eventB: K) => boolean + }) { + this.buffer = [] + this.maxQueueSize = Math.max(maxQueueSize, 1) + this.sink = sink + this.batchComparator = batchComparator + this.timer = new Timer({ + callback: this.flush.bind(this), + timeout: flushInterval, + }) + this.started = false + } + + start(): void { + this.started = true + // dont start the timer until the first event is enqueued + } + + stop(): Promise { + this.started = false + const result = this.sink(this.buffer) + this.buffer = [] + this.timer.stop() + return result + } + + enqueue(event: K): void { + if (!this.started) { + logger.warn('Queue is stopped, not accepting event') + return + } + + // If new event cannot be included into the current batch, flush so it can + // be in its own new batch. + const bufferedEvent: K | undefined = this.buffer[0] + if (bufferedEvent && !this.batchComparator(bufferedEvent, event)) { + this.flush() + } + + // start the timer when the first event is put in + if (this.buffer.length === 0) { + this.timer.refresh() + } + this.buffer.push(event) + + if (this.buffer.length >= this.maxQueueSize) { + this.flush() + } + } + + flush() : void { + this.sink(this.buffer) + this.buffer = [] + this.timer.stop() + } +} diff --git a/packages/optimizely-sdk/lib/modules/event_processor/events.ts b/packages/optimizely-sdk/lib/modules/event_processor/events.ts new file mode 100644 index 000000000..65cce503b --- /dev/null +++ b/packages/optimizely-sdk/lib/modules/event_processor/events.ts @@ -0,0 +1,101 @@ +/** + * Copyright 2022, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export type VisitorAttribute = { + entityId: string + key: string + value: string | number | boolean +} + +export interface BaseEvent { + type: 'impression' | 'conversion' + timestamp: number + uuid: string + + // projectConfig stuff + context: { + accountId: string + projectId: string + clientName: string + clientVersion: string + revision: string + anonymizeIP: boolean + botFiltering?: boolean + } +} + +export interface ImpressionEvent extends BaseEvent { + type: 'impression' + + user: { + id: string + attributes: VisitorAttribute[] + } + + layer: { + id: string | null + } | null + + experiment: { + id: string | null + key: string + } | null + + variation: { + id: string | null + key: string + } | null + + ruleKey: string + flagKey: string + ruleType: string + enabled: boolean +} + +export interface ConversionEvent extends BaseEvent { + type: 'conversion' + + user: { + id: string + attributes: VisitorAttribute[] + } + + event: { + id: string | null + key: string + } + + revenue: number | null + value: number | null + tags: EventTags | undefined +} + +export type EventTags = { + [key: string]: string | number | null +} + +export function areEventContextsEqual(eventA: BaseEvent, eventB: BaseEvent): boolean { + const contextA = eventA.context + const contextB = eventB.context + return ( + contextA.accountId === contextB.accountId && + contextA.projectId === contextB.projectId && + contextA.clientName === contextB.clientName && + contextA.clientVersion === contextB.clientVersion && + contextA.revision === contextB.revision && + contextA.anonymizeIP === contextB.anonymizeIP && + contextA.botFiltering === contextB.botFiltering + ) +} diff --git a/packages/optimizely-sdk/lib/modules/event_processor/index.react_native.ts b/packages/optimizely-sdk/lib/modules/event_processor/index.react_native.ts new file mode 100644 index 000000000..91bb29a58 --- /dev/null +++ b/packages/optimizely-sdk/lib/modules/event_processor/index.react_native.ts @@ -0,0 +1,23 @@ +/** + * Copyright 2022, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './events' +export * from './eventProcessor' +export * from './eventDispatcher' +export * from './managed' +export * from './pendingEventsDispatcher' +export * from './v1/buildEventV1' +export * from './v1/v1EventProcessor.react_native' diff --git a/packages/optimizely-sdk/lib/modules/event_processor/index.ts b/packages/optimizely-sdk/lib/modules/event_processor/index.ts new file mode 100644 index 000000000..c4eaef01d --- /dev/null +++ b/packages/optimizely-sdk/lib/modules/event_processor/index.ts @@ -0,0 +1,23 @@ +/** + * Copyright 2022, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './events' +export * from './eventProcessor' +export * from './eventDispatcher' +export * from './managed' +export * from './pendingEventsDispatcher' +export * from './v1/buildEventV1' +export * from './v1/v1EventProcessor' diff --git a/packages/optimizely-sdk/lib/modules/event_processor/managed.ts b/packages/optimizely-sdk/lib/modules/event_processor/managed.ts new file mode 100644 index 000000000..6d89843de --- /dev/null +++ b/packages/optimizely-sdk/lib/modules/event_processor/managed.ts @@ -0,0 +1,20 @@ +/** + * Copyright 2022, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export interface Managed { + start(): void + + stop(): Promise +} diff --git a/packages/optimizely-sdk/lib/modules/event_processor/pendingEventsDispatcher.ts b/packages/optimizely-sdk/lib/modules/event_processor/pendingEventsDispatcher.ts new file mode 100644 index 000000000..4f4c8c61b --- /dev/null +++ b/packages/optimizely-sdk/lib/modules/event_processor/pendingEventsDispatcher.ts @@ -0,0 +1,91 @@ +/** + * Copyright 2022, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { getLogger } from '../logging' +import { EventDispatcher, EventV1Request, EventDispatcherCallback } from './eventDispatcher' +import { PendingEventsStore, LocalStorageStore } from './pendingEventsStore' +import { uuid, getTimestamp } from '../../utils/fns' + +const logger = getLogger('EventProcessor') + +export type DispatcherEntry = { + uuid: string + timestamp: number + request: EventV1Request +} + +export class PendingEventsDispatcher implements EventDispatcher { + protected dispatcher: EventDispatcher + protected store: PendingEventsStore + + constructor({ + eventDispatcher, + store, + }: { + eventDispatcher: EventDispatcher + store: PendingEventsStore + }) { + this.dispatcher = eventDispatcher + this.store = store + } + + dispatchEvent(request: EventV1Request, callback: EventDispatcherCallback): void { + this.send( + { + uuid: uuid(), + timestamp: getTimestamp(), + request, + }, + callback, + ) + } + + sendPendingEvents(): void { + const pendingEvents = this.store.values() + + logger.debug('Sending %s pending events from previous page', pendingEvents.length) + + pendingEvents.forEach(item => { + try { + this.send(item, () => {}) + } catch (e) + { + logger.debug(String(e)) + } + }) + } + + protected send(entry: DispatcherEntry, callback: EventDispatcherCallback): void { + this.store.set(entry.uuid, entry) + + this.dispatcher.dispatchEvent(entry.request, response => { + this.store.remove(entry.uuid) + callback(response) + }) + } +} + +export class LocalStoragePendingEventsDispatcher extends PendingEventsDispatcher { + constructor({ eventDispatcher }: { eventDispatcher: EventDispatcher }) { + super({ + eventDispatcher, + store: new LocalStorageStore({ + // TODO make this configurable + maxValues: 100, + key: 'fs_optly_pending_events', + }), + }) + } +} diff --git a/packages/optimizely-sdk/lib/modules/event_processor/pendingEventsStore.ts b/packages/optimizely-sdk/lib/modules/event_processor/pendingEventsStore.ts new file mode 100644 index 000000000..eed4c8e95 --- /dev/null +++ b/packages/optimizely-sdk/lib/modules/event_processor/pendingEventsStore.ts @@ -0,0 +1,117 @@ +/** + * Copyright 2022, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { objectValues } from '../../utils/fns' +import { getLogger } from '../logging'; + +const logger = getLogger('EventProcessor') + +export interface PendingEventsStore { + get(key: string): K | null + + set(key: string, value: K): void + + remove(key: string): void + + values(): K[] + + clear(): void + + replace(newMap: { [key: string]: K }): void +} + +interface StoreEntry { + uuid: string + timestamp: number +} + +export class LocalStorageStore implements PendingEventsStore { + protected LS_KEY: string + protected maxValues: number + + constructor({ key, maxValues = 1000 }: { key: string; maxValues?: number }) { + this.LS_KEY = key + this.maxValues = maxValues + } + + get(key: string): K | null { + return this.getMap()[key] || null + } + + set(key: string, value: K): void { + const map = this.getMap() + map[key] = value + this.replace(map) + } + + remove(key: string): void { + const map = this.getMap() + delete map[key] + this.replace(map) + } + + values(): K[] { + return objectValues(this.getMap()) + } + + clear(): void { + this.replace({}) + } + + replace(map: { [key: string]: K }): void { + try { + // This is a temporary fix to support React Native which does not have localStorage. + window.localStorage && localStorage.setItem(this.LS_KEY, JSON.stringify(map)) + this.clean() + } catch (e) { + logger.error(String(e)) + } + } + + private clean() { + const map = this.getMap() + const keys = Object.keys(map) + const toRemove = keys.length - this.maxValues + if (toRemove < 1) { + return + } + + const entries = keys.map(key => ({ + key, + value: map[key] + })) + + entries.sort((a, b) => a.value.timestamp - b.value.timestamp) + + for (let i = 0; i < toRemove; i++) { + delete map[entries[i].key] + } + + this.replace(map) + } + + private getMap(): { [key: string]: K } { + try { + // This is a temporary fix to support React Native which does not have localStorage. + const data = window.localStorage && localStorage.getItem(this.LS_KEY); + if (data) { + return (JSON.parse(data) as { [key: string]: K }) || {} + } + } catch (e) { + logger.error(String(e)) + } + return {} + } +} diff --git a/packages/optimizely-sdk/lib/modules/event_processor/persistentKeyValueCache.ts b/packages/optimizely-sdk/lib/modules/event_processor/persistentKeyValueCache.ts new file mode 100644 index 000000000..7dd508df9 --- /dev/null +++ b/packages/optimizely-sdk/lib/modules/event_processor/persistentKeyValueCache.ts @@ -0,0 +1,60 @@ +/** + * Copyright 2022, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * An Interface to implement a persistent key value cache which supports strings as keys + * and JSON Object as value. + */ +export default interface PersistentKeyValueCache { + /** + * Returns value stored against a key or null if not found. + * @param key + * @returns + * Resolves promise with + * 1. Object if value found was stored as a JSON Object. + * 2. null if the key does not exist in the cache. + * Rejects the promise in case of an error + */ + get(key: string): Promise; + + /** + * Stores Object in the persistent cache against a key + * @param key + * @param val + * @returns + * Resolves promise without a value if successful + * Rejects the promise in case of an error + */ + set(key: string, val: any): Promise; + + /** + * Checks if a key exists in the cache + * @param key + * Resolves promise with + * 1. true if the key exists + * 2. false if the key does not exist + * Rejects the promise in case of an error + */ + contains(key: string): Promise; + + /** + * Removes the key value pair from cache. + * @param key + * Resolves promise without a value if successful + * Rejects the promise in case of an error + */ + remove(key: string): Promise; +} diff --git a/packages/optimizely-sdk/lib/modules/event_processor/reactNativeAsyncStorageCache.ts b/packages/optimizely-sdk/lib/modules/event_processor/reactNativeAsyncStorageCache.ts new file mode 100644 index 000000000..26b2ac315 --- /dev/null +++ b/packages/optimizely-sdk/lib/modules/event_processor/reactNativeAsyncStorageCache.ts @@ -0,0 +1,47 @@ +/** + * Copyright 2022, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import AsyncStorage from '@react-native-async-storage/async-storage'; + +import PersistentKeyValueCache from './persistentKeyValueCache'; + +export default class ReactNativeAsyncStorageCache implements PersistentKeyValueCache { + get(key: string): Promise { + return AsyncStorage.getItem(key).then((val: string | null) => { + if (!val) { + return null; + } + return JSON.parse(val); + }); + } + + /* eslint-disable */ + set(key: string, val: any): Promise { + try { + return AsyncStorage.setItem(key, JSON.stringify(val)); + } catch (ex) { + return Promise.reject(ex); + } + } + /* eslint-enable */ + + contains(key: string): Promise { + return AsyncStorage.getItem(key).then((val: string | null) => val !== null); + } + + remove(key: string): Promise { + return AsyncStorage.removeItem(key); + } +} diff --git a/packages/optimizely-sdk/lib/modules/event_processor/reactNativeEventsStore.ts b/packages/optimizely-sdk/lib/modules/event_processor/reactNativeEventsStore.ts new file mode 100644 index 000000000..d07928afa --- /dev/null +++ b/packages/optimizely-sdk/lib/modules/event_processor/reactNativeEventsStore.ts @@ -0,0 +1,81 @@ + +/** + * Copyright 2022, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { getLogger } from '../logging' +import { objectValues } from "../../utils/fns" + +import { Synchronizer } from './synchronizer' +import ReactNativeAsyncStorageCache from './reactNativeAsyncStorageCache' + +const logger = getLogger('ReactNativeEventsStore') + +/** + * A key value store which stores objects of type T with string keys + */ +export class ReactNativeEventsStore { + private maxSize: number + private storeKey: string + private synchronizer: Synchronizer = new Synchronizer() + private cache: ReactNativeAsyncStorageCache = new ReactNativeAsyncStorageCache() + + constructor(maxSize: number, storeKey: string) { + this.maxSize = maxSize + this.storeKey = storeKey + } + + public async set(key: string, event: T): Promise { + await this.synchronizer.getLock() + const eventsMap: {[key: string]: T} = await this.cache.get(this.storeKey) || {} + if (Object.keys(eventsMap).length < this.maxSize) { + eventsMap[key] = event + await this.cache.set(this.storeKey, eventsMap) + } else { + logger.warn('React native events store is full. Store key: %s', this.storeKey) + } + this.synchronizer.releaseLock() + return key + } + + public async get(key: string): Promise { + await this.synchronizer.getLock() + const eventsMap: {[key: string]: T} = await this.cache.get(this.storeKey) || {} + this.synchronizer.releaseLock() + return eventsMap[key] + } + + public async getEventsMap(): Promise<{[key: string]: T}> { + return await this.cache.get(this.storeKey) || {} + } + + public async getEventsList(): Promise { + await this.synchronizer.getLock() + const eventsMap: {[key: string]: T} = await this.cache.get(this.storeKey) || {} + this.synchronizer.releaseLock() + return objectValues(eventsMap) + } + + public async remove(key: string): Promise { + await this.synchronizer.getLock() + const eventsMap: {[key: string]: T} = await this.cache.get(this.storeKey) || {} + eventsMap[key] && delete eventsMap[key] + await this.cache.set(this.storeKey, eventsMap) + this.synchronizer.releaseLock() + } + + public async clear(): Promise { + await this.cache.remove(this.storeKey) + } +} diff --git a/packages/optimizely-sdk/lib/modules/event_processor/requestTracker.ts b/packages/optimizely-sdk/lib/modules/event_processor/requestTracker.ts new file mode 100644 index 000000000..ab18b36d1 --- /dev/null +++ b/packages/optimizely-sdk/lib/modules/event_processor/requestTracker.ts @@ -0,0 +1,60 @@ +/** + * Copyright 2022, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * RequestTracker keeps track of in-flight requests for EventProcessor using + * an internal counter. It exposes methods for adding a new request to be + * tracked, and getting a Promise representing the completion of currently + * tracked requests. + */ +class RequestTracker { + private reqsInFlightCount = 0 + private reqsCompleteResolvers: Array<() => void> = [] + + /** + * Track the argument request (represented by a Promise). reqPromise will feed + * into the state of Promises returned by onRequestsComplete. + * @param {Promise} reqPromise + */ + public trackRequest(reqPromise: Promise): void { + this.reqsInFlightCount++ + const onReqComplete = () => { + this.reqsInFlightCount-- + if (this.reqsInFlightCount === 0) { + this.reqsCompleteResolvers.forEach(resolver => resolver()) + this.reqsCompleteResolvers = [] + } + } + reqPromise.then(onReqComplete, onReqComplete) + } + + /** + * Return a Promise that fulfills after all currently-tracked request promises + * are resolved. + * @return {Promise} + */ + public onRequestsComplete(): Promise { + return new Promise(resolve => { + if (this.reqsInFlightCount === 0) { + resolve() + } else { + this.reqsCompleteResolvers.push(resolve) + } + }) + } +} + +export default RequestTracker diff --git a/packages/optimizely-sdk/lib/modules/event_processor/synchronizer.ts b/packages/optimizely-sdk/lib/modules/event_processor/synchronizer.ts new file mode 100644 index 000000000..d6bf32b7b --- /dev/null +++ b/packages/optimizely-sdk/lib/modules/event_processor/synchronizer.ts @@ -0,0 +1,42 @@ +/** + * Copyright 2022, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * This synchronizer makes sure the operations are atomic using promises. + */ +export class Synchronizer { + private lockPromises: Promise[] = [] + private resolvers: any[] = [] + + // Adds a promise to the existing list and returns the promise so that the code block can wait for its turn + public async getLock(): Promise { + this.lockPromises.push(new Promise(resolve => this.resolvers.push(resolve))) + if (this.lockPromises.length === 1) { + return + } + await this.lockPromises[this.lockPromises.length - 2] + } + + // Resolves first promise in the array so that the code block waiting on the first promise can continue execution + public releaseLock(): void { + if (this.lockPromises.length > 0) { + this.lockPromises.shift() + const resolver = this.resolvers.shift() + resolver() + return + } + } +} diff --git a/packages/optimizely-sdk/lib/modules/event_processor/v1/buildEventV1.ts b/packages/optimizely-sdk/lib/modules/event_processor/v1/buildEventV1.ts new file mode 100644 index 000000000..699498dc4 --- /dev/null +++ b/packages/optimizely-sdk/lib/modules/event_processor/v1/buildEventV1.ts @@ -0,0 +1,272 @@ +/** + * Copyright 2022, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { EventTags, ConversionEvent, ImpressionEvent, VisitorAttribute } from '../events' +import { ProcessableEvent } from '../eventProcessor' +import { EventV1Request } from '../eventDispatcher' + +const ACTIVATE_EVENT_KEY = 'campaign_activated' +const CUSTOM_ATTRIBUTE_FEATURE_TYPE = 'custom' +const BOT_FILTERING_KEY = '$opt_bot_filtering' + +export type EventV1 = { + account_id: string + project_id: string + revision: string + client_name: string + client_version: string + anonymize_ip: boolean + enrich_decisions: boolean + visitors: Visitor[] +} + +type Visitor = { + snapshots: Visitor.Snapshot[] + visitor_id: string + attributes: Visitor.Attribute[] +} + +// eslint-disable-next-line @typescript-eslint/no-namespace +namespace Visitor { + type AttributeType = 'custom' + + export type Attribute = { + // attribute id + entity_id: string + // attribute key + key: string + type: AttributeType + value: string | number | boolean + } + + export type Snapshot = { + decisions?: Decision[] + events: SnapshotEvent[] + } + + type Decision = { + campaign_id: string | null + experiment_id: string | null + variation_id: string | null + metadata: Metadata + } + + type Metadata = { + flag_key: string; + rule_key: string; + rule_type: string; + variation_key: string; + enabled: boolean; + } + + export type SnapshotEvent = { + entity_id: string | null + timestamp: number + uuid: string + key: string + revenue?: number + value?: number + tags?: EventTags + } +} + + + +type Attributes = { + [key: string]: string | number | boolean +} + +/** + * Given an array of batchable Decision or ConversionEvent events it returns + * a single EventV1 with proper batching + * + * @param {ProcessableEvent[]} events + * @returns {EventV1} + */ +export function makeBatchedEventV1(events: ProcessableEvent[]): EventV1 { + const visitors: Visitor[] = [] + const data = events[0] + + events.forEach(event => { + if (event.type === 'conversion' || event.type === 'impression') { + const visitor = makeVisitor(event) + + if (event.type === 'impression') { + visitor.snapshots.push(makeDecisionSnapshot(event)) + } else if (event.type === 'conversion') { + visitor.snapshots.push(makeConversionSnapshot(event)) + } + + visitors.push(visitor) + } + }) + + return { + client_name: data.context.clientName, + client_version: data.context.clientVersion, + + account_id: data.context.accountId, + project_id: data.context.projectId, + revision: data.context.revision, + anonymize_ip: data.context.anonymizeIP, + enrich_decisions: true, + + visitors, + } +} + +function makeConversionSnapshot(conversion: ConversionEvent): Visitor.Snapshot { + const tags: EventTags = { + ...conversion.tags, + } + + delete tags['revenue'] + delete tags['value'] + + const event: Visitor.SnapshotEvent = { + entity_id: conversion.event.id, + key: conversion.event.key, + timestamp: conversion.timestamp, + uuid: conversion.uuid, + } + + if (conversion.tags) { + event.tags = conversion.tags + } + + if (conversion.value != null) { + event.value = conversion.value + } + + if (conversion.revenue != null) { + event.revenue = conversion.revenue + } + + return { + events: [event], + } +} + +function makeDecisionSnapshot(event: ImpressionEvent): Visitor.Snapshot { + const { layer, experiment, variation, ruleKey, flagKey, ruleType, enabled } = event + const layerId = layer ? layer.id : null + const experimentId = experiment?.id ?? '' + const variationId = variation?.id ?? '' + const variationKey = variation ? variation.key : '' + + return { + decisions: [ + { + campaign_id: layerId, + experiment_id: experimentId, + variation_id: variationId, + metadata: { + flag_key: flagKey, + rule_key: ruleKey, + rule_type: ruleType, + variation_key: variationKey, + enabled: enabled, + }, + }, + ], + events: [ + { + entity_id: layerId, + timestamp: event.timestamp, + key: ACTIVATE_EVENT_KEY, + uuid: event.uuid, + }, + ], + } +} + +function makeVisitor(data: ImpressionEvent | ConversionEvent): Visitor { + const visitor: Visitor = { + snapshots: [], + visitor_id: data.user.id, + attributes: [], + } + + const type = 'custom' + data.user.attributes.forEach(attr => { + visitor.attributes.push({ + entity_id: attr.entityId, + key: attr.key, + type: type as 'custom', // tell the compiler this is always string "custom" + value: attr.value, + }) + }) + + if (typeof data.context.botFiltering === 'boolean') { + visitor.attributes.push({ + entity_id: BOT_FILTERING_KEY, + key: BOT_FILTERING_KEY, + type: CUSTOM_ATTRIBUTE_FEATURE_TYPE, + value: data.context.botFiltering, + }) + } + return visitor +} + +/** + * Event for usage with v1 logtier + * + * @export + * @interface EventBuilderV1 + */ + +export function buildImpressionEventV1(data: ImpressionEvent): EventV1 { + const visitor = makeVisitor(data) + visitor.snapshots.push(makeDecisionSnapshot(data)) + + return { + client_name: data.context.clientName, + client_version: data.context.clientVersion, + + account_id: data.context.accountId, + project_id: data.context.projectId, + revision: data.context.revision, + anonymize_ip: data.context.anonymizeIP, + enrich_decisions: true, + + visitors: [visitor], + } +} + +export function buildConversionEventV1(data: ConversionEvent): EventV1 { + const visitor = makeVisitor(data) + visitor.snapshots.push(makeConversionSnapshot(data)) + + return { + client_name: data.context.clientName, + client_version: data.context.clientVersion, + + account_id: data.context.accountId, + project_id: data.context.projectId, + revision: data.context.revision, + anonymize_ip: data.context.anonymizeIP, + enrich_decisions: true, + + visitors: [visitor], + } +} + +export function formatEvents(events: ProcessableEvent[]): EventV1Request { + return { + url: 'https://logx.optimizely.com/v1/events', + httpVerb: 'POST', + params: makeBatchedEventV1(events), + } +} diff --git a/packages/optimizely-sdk/lib/modules/event_processor/v1/v1EventProcessor.react_native.ts b/packages/optimizely-sdk/lib/modules/event_processor/v1/v1EventProcessor.react_native.ts new file mode 100644 index 000000000..cc690dbee --- /dev/null +++ b/packages/optimizely-sdk/lib/modules/event_processor/v1/v1EventProcessor.react_native.ts @@ -0,0 +1,237 @@ +/** + * Copyright 2022, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + uuid as id, + objectEntries, +} from '../../../utils/fns' +import { + NetInfoState, + addEventListener as addConnectionListener, +} from "@react-native-community/netinfo" +import { getLogger } from '../../logging' +import { NotificationSender } from '../../../core/notification_center' + +import { + getQueue, + EventProcessor, + ProcessableEvent, + sendEventNotification, + validateAndGetBatchSize, + validateAndGetFlushInterval, + DEFAULT_BATCH_SIZE, + DEFAULT_FLUSH_INTERVAL, +} from "../eventProcessor" +import { ReactNativeEventsStore } from '../reactNativeEventsStore' +import { Synchronizer } from '../synchronizer' +import { EventQueue } from '../eventQueue' +import RequestTracker from '../requestTracker' +import { areEventContextsEqual } from '../events' +import { formatEvents } from './buildEventV1' +import { + EventV1Request, + EventDispatcher, + EventDispatcherResponse, +} from '../eventDispatcher' + +const logger = getLogger('ReactNativeEventProcessor') + +const DEFAULT_MAX_QUEUE_SIZE = 10000 +const PENDING_EVENTS_STORE_KEY = 'fs_optly_pending_events' +const EVENT_BUFFER_STORE_KEY = 'fs_optly_event_buffer' + +/** + * React Native Events Processor with Caching support for events when app is offline. + */ +export class LogTierV1EventProcessor implements EventProcessor { + private dispatcher: EventDispatcher + // expose for testing + public queue: EventQueue + private notificationSender?: NotificationSender + private requestTracker: RequestTracker + + /* eslint-disable */ + private unsubscribeNetInfo: Function | null = null + /* eslint-enable */ + private isInternetReachable = true + private pendingEventsPromise: Promise | null = null + private synchronizer: Synchronizer = new Synchronizer() + + // If a pending event fails to dispatch, this indicates skipping further events to preserve sequence in the next retry. + private shouldSkipDispatchToPreserveSequence = false + + /** + * This Stores Formatted events before dispatching. The events are removed after they are successfully dispatched. + * Stored events are retried on every new event dispatch, when connection becomes available again or when SDK initializes the next time. + */ + private pendingEventsStore: ReactNativeEventsStore + + /** + * This stores individual events generated from the SDK till they are part of the pending buffer. + * The store is cleared right before the event is formatted to be dispatched. + * This is to make sure that individual events are not lost when app closes before the buffer was flushed. + */ + private eventBufferStore: ReactNativeEventsStore + + constructor({ + dispatcher, + flushInterval = DEFAULT_FLUSH_INTERVAL, + batchSize = DEFAULT_BATCH_SIZE, + maxQueueSize = DEFAULT_MAX_QUEUE_SIZE, + notificationCenter, + }: { + dispatcher: EventDispatcher + flushInterval?: number + batchSize?: number + maxQueueSize?: number + notificationCenter?: NotificationSender + }) { + this.dispatcher = dispatcher + this.notificationSender = notificationCenter + this.requestTracker = new RequestTracker() + + flushInterval = validateAndGetFlushInterval(flushInterval) + batchSize = validateAndGetBatchSize(batchSize) + this.queue = getQueue(batchSize, flushInterval, this.drainQueue.bind(this), areEventContextsEqual) + this.pendingEventsStore = new ReactNativeEventsStore(maxQueueSize, PENDING_EVENTS_STORE_KEY) + this.eventBufferStore = new ReactNativeEventsStore(maxQueueSize, EVENT_BUFFER_STORE_KEY) + } + + private async connectionListener(state: NetInfoState) { + if (this.isInternetReachable && !state.isInternetReachable) { + this.isInternetReachable = false + logger.debug('Internet connection lost') + return + } + if (!this.isInternetReachable && state.isInternetReachable) { + this.isInternetReachable = true + logger.debug('Internet connection is restored, attempting to dispatch pending events') + await this.processPendingEvents() + this.shouldSkipDispatchToPreserveSequence = false + } + } + + private isSuccessResponse(status: number): boolean { + return status >= 200 && status < 400 + } + + private async drainQueue(buffer: ProcessableEvent[]): Promise { + if (buffer.length === 0) { + return + } + + await this.synchronizer.getLock() + + // Retry pending failed events while draining queue + await this.processPendingEvents() + + logger.debug('draining queue with %s events', buffer.length) + + const eventCacheKey = id() + const formattedEvent = formatEvents(buffer) + + // Store formatted event before dispatching to be retried later in case of failure. + await this.pendingEventsStore.set(eventCacheKey, formattedEvent) + + // Clear buffer because the buffer has become a formatted event and is already stored in pending cache. + for (const {uuid} of buffer) { + await this.eventBufferStore.remove(uuid) + } + + if (!this.shouldSkipDispatchToPreserveSequence) { + await this.dispatchEvent(eventCacheKey, formattedEvent) + } + + // Resetting skip flag because current sequence of events have all been processed + this.shouldSkipDispatchToPreserveSequence = false + + this.synchronizer.releaseLock() + } + + private async processPendingEvents(): Promise { + logger.debug('Processing pending events from offline storage') + if (!this.pendingEventsPromise) { + // Only process events if existing promise is not in progress + this.pendingEventsPromise = this.getPendingEventsPromise() + } else { + logger.debug('Already processing pending events, returning the existing promise') + } + await this.pendingEventsPromise + this.pendingEventsPromise = null + } + + private async getPendingEventsPromise(): Promise { + const formattedEvents: {[key: string]: any} = await this.pendingEventsStore.getEventsMap() + const eventEntries = objectEntries(formattedEvents) + logger.debug('Processing %s pending events', eventEntries.length) + // Using for loop to be able to wait for previous dispatch to finish before moving on to the new one + for (const [eventKey, event] of eventEntries) { + // If one event dispatch failed, skip subsequent events to preserve sequence + if (this.shouldSkipDispatchToPreserveSequence) { + return + } + await this.dispatchEvent(eventKey, event) + } + } + + private async dispatchEvent(eventCacheKey: string, event: EventV1Request): Promise { + const requestPromise = new Promise((resolve) => { + this.dispatcher.dispatchEvent(event, async ({ statusCode }: EventDispatcherResponse) => { + if (this.isSuccessResponse(statusCode)) { + await this.pendingEventsStore.remove(eventCacheKey) + } else { + this.shouldSkipDispatchToPreserveSequence = true + logger.warn('Failed to dispatch event, Response status Code: %s', statusCode) + } + resolve() + }) + sendEventNotification(this.notificationSender, event) + }) + // Tracking all the requests to dispatch to make sure request is completed before fulfilling the `stop` promise + this.requestTracker.trackRequest(requestPromise) + return requestPromise + } + + public async start(): Promise { + this.queue.start() + this.unsubscribeNetInfo = addConnectionListener(this.connectionListener.bind(this)) + + await this.processPendingEvents() + this.shouldSkipDispatchToPreserveSequence = false + + // Process individual events pending from the buffer. + const events: ProcessableEvent[] = await this.eventBufferStore.getEventsList() + await this.eventBufferStore.clear() + events.forEach(this.process.bind(this)) + } + + public process(event: ProcessableEvent): void { + // Adding events to buffer store. If app closes before dispatch, we can reprocess next time the app initializes + this.eventBufferStore.set(event.uuid, event).then(() => { + this.queue.enqueue(event) + }) + } + + public async stop(): Promise { + // swallow - an error stopping this queue shouldn't prevent this from stopping + try { + this.unsubscribeNetInfo && this.unsubscribeNetInfo() + await this.queue.stop() + return this.requestTracker.onRequestsComplete() + } catch (e) { + logger.error('Error stopping EventProcessor: "%s"', Object(e).message, String(e)) + } + } +} diff --git a/packages/optimizely-sdk/lib/modules/event_processor/v1/v1EventProcessor.ts b/packages/optimizely-sdk/lib/modules/event_processor/v1/v1EventProcessor.ts new file mode 100644 index 000000000..108bf2e1c --- /dev/null +++ b/packages/optimizely-sdk/lib/modules/event_processor/v1/v1EventProcessor.ts @@ -0,0 +1,102 @@ +/** + * Copyright 2022, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { getLogger } from '../../logging' +import { NotificationSender } from '../../../core/notification_center' + +import { EventDispatcher } from '../eventDispatcher' +import { + getQueue, + EventProcessor, + ProcessableEvent, + sendEventNotification, + validateAndGetBatchSize, + validateAndGetFlushInterval, + DEFAULT_BATCH_SIZE, + DEFAULT_FLUSH_INTERVAL, +} from '../eventProcessor' +import { EventQueue } from '../eventQueue' +import RequestTracker from '../requestTracker' +import { areEventContextsEqual } from '../events' +import { formatEvents } from './buildEventV1' + +const logger = getLogger('LogTierV1EventProcessor') + +export class LogTierV1EventProcessor implements EventProcessor { + private dispatcher: EventDispatcher + private queue: EventQueue + private notificationCenter?: NotificationSender + private requestTracker: RequestTracker + + constructor({ + dispatcher, + flushInterval = DEFAULT_FLUSH_INTERVAL, + batchSize = DEFAULT_BATCH_SIZE, + notificationCenter, + }: { + dispatcher: EventDispatcher + flushInterval?: number + batchSize?: number + notificationCenter?: NotificationSender + }) { + this.dispatcher = dispatcher + this.notificationCenter = notificationCenter + this.requestTracker = new RequestTracker() + + flushInterval = validateAndGetFlushInterval(flushInterval) + batchSize = validateAndGetBatchSize(batchSize) + this.queue = getQueue(batchSize, flushInterval, this.drainQueue.bind(this), areEventContextsEqual) + } + + drainQueue(buffer: ProcessableEvent[]): Promise { + const reqPromise = new Promise(resolve => { + logger.debug('draining queue with %s events', buffer.length) + + if (buffer.length === 0) { + resolve() + return + } + + const formattedEvent = formatEvents(buffer) + this.dispatcher.dispatchEvent(formattedEvent, () => { + resolve() + }) + sendEventNotification(this.notificationCenter, formattedEvent) + }) + this.requestTracker.trackRequest(reqPromise) + return reqPromise + } + + process(event: ProcessableEvent): void { + this.queue.enqueue(event) + } + + // TODO[OASIS-6649]: Don't use any type + // eslint-disable-next-line @typescript-eslint/no-explicit-any + stop(): Promise { + // swallow - an error stopping this queue shouldn't prevent this from stopping + try { + this.queue.stop() + return this.requestTracker.onRequestsComplete() + } catch (e) { + logger.error('Error stopping EventProcessor: "%s"', Object(e).message, String(e)) + } + return Promise.resolve() + } + + async start(): Promise { + this.queue.start() + } +} diff --git a/packages/optimizely-sdk/lib/optimizely/index.ts b/packages/optimizely-sdk/lib/optimizely/index.ts index 5347d8786..d6fdd8beb 100644 --- a/packages/optimizely-sdk/lib/optimizely/index.ts +++ b/packages/optimizely-sdk/lib/optimizely/index.ts @@ -16,7 +16,7 @@ import { LoggerFacade, ErrorHandler } from '../modules/logging'; import { sprintf, objectValues } from '../utils/fns'; import { NotificationCenter } from '../core/notification_center'; -import { EventProcessor } from '@optimizely/js-sdk-event-processor'; +import { EventProcessor } from '../../lib/modules/event_processor'; import { UserAttributes, diff --git a/packages/optimizely-sdk/lib/plugins/event_processor/forwarding_event_processor.ts b/packages/optimizely-sdk/lib/plugins/event_processor/forwarding_event_processor.ts index 87397c587..dd0473ac1 100644 --- a/packages/optimizely-sdk/lib/plugins/event_processor/forwarding_event_processor.ts +++ b/packages/optimizely-sdk/lib/plugins/event_processor/forwarding_event_processor.ts @@ -17,7 +17,7 @@ import { EventProcessor, ProcessableEvent, -} from '@optimizely/js-sdk-event-processor'; +} from '../../../lib/modules/event_processor'; import { NotificationSender } from '../../core/notification_center'; import { EventDispatcher } from '../../shared_types'; @@ -26,18 +26,18 @@ import { formatEvents } from '../../core/event_builder/build_event_v1'; class ForwardingEventProcessor implements EventProcessor { private dispatcher: EventDispatcher; - private notificationSender?: NotificationSender; + private NotificationSender?: NotificationSender; constructor(dispatcher: EventDispatcher, notificationSender?: NotificationSender) { this.dispatcher = dispatcher; - this.notificationSender = notificationSender; + this.NotificationSender = notificationSender; } process(event: ProcessableEvent): void { const formattedEvent = formatEvents([event]); this.dispatcher.dispatchEvent(formattedEvent, () => {}); - if (this.notificationSender) { - this.notificationSender.sendNotifications( + if (this.NotificationSender) { + this.NotificationSender.sendNotifications( NOTIFICATION_TYPES.LOG_EVENT, formattedEvent, ) diff --git a/packages/optimizely-sdk/lib/plugins/event_processor/index.react_native.ts b/packages/optimizely-sdk/lib/plugins/event_processor/index.react_native.ts new file mode 100644 index 000000000..277512a6e --- /dev/null +++ b/packages/optimizely-sdk/lib/plugins/event_processor/index.react_native.ts @@ -0,0 +1,26 @@ +/** + * Copyright 2022, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { LogTierV1EventProcessor, LocalStoragePendingEventsDispatcher } from '../../../lib/modules/event_processor/index.react_native'; + +export function createEventProcessor( + ...args: ConstructorParameters +): LogTierV1EventProcessor { + return new LogTierV1EventProcessor(...args); +} + +export default { createEventProcessor, LocalStoragePendingEventsDispatcher }; + diff --git a/packages/optimizely-sdk/lib/plugins/event_processor/index.ts b/packages/optimizely-sdk/lib/plugins/event_processor/index.ts index b12502ce8..b515b7b63 100644 --- a/packages/optimizely-sdk/lib/plugins/event_processor/index.ts +++ b/packages/optimizely-sdk/lib/plugins/event_processor/index.ts @@ -1,5 +1,5 @@ /** - * Copyright 2020, Optimizely + * Copyright 2020, 2022, Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -import { LogTierV1EventProcessor, LocalStoragePendingEventsDispatcher } from '@optimizely/js-sdk-event-processor'; +import { LogTierV1EventProcessor, LocalStoragePendingEventsDispatcher } from '../../../lib/modules/event_processor'; export function createEventProcessor( ...args: ConstructorParameters diff --git a/packages/optimizely-sdk/lib/shared_types.ts b/packages/optimizely-sdk/lib/shared_types.ts index 36bab45dd..f3ff5251b 100644 --- a/packages/optimizely-sdk/lib/shared_types.ts +++ b/packages/optimizely-sdk/lib/shared_types.ts @@ -13,8 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { ErrorHandler, LogHandler, LogLevel, LoggerFacade } from '@optimizely/js-sdk-logging'; -import { EventProcessor } from '@optimizely/js-sdk-event-processor'; +import { ErrorHandler, LogHandler, LogLevel, LoggerFacade } from '../lib/modules/logging'; +import { EventProcessor } from '../lib/modules/event_processor'; import { NotificationCenter as NotificationCenterImpl } from './core/notification_center' import { NOTIFICATION_TYPES } from './utils/enums'; diff --git a/packages/optimizely-sdk/lib/utils/event_tag_utils/index.ts b/packages/optimizely-sdk/lib/utils/event_tag_utils/index.ts index d47ab1944..4fbc60597 100644 --- a/packages/optimizely-sdk/lib/utils/event_tag_utils/index.ts +++ b/packages/optimizely-sdk/lib/utils/event_tag_utils/index.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { EventTags } from '@optimizely/js-sdk-event-processor'; +import { EventTags } from '../../../lib/modules/event_processor'; import { LoggerFacade } from '../../modules/logging'; import { diff --git a/packages/optimizely-sdk/package-lock.json b/packages/optimizely-sdk/package-lock.json index 6e9935a46..056731061 100644 --- a/packages/optimizely-sdk/package-lock.json +++ b/packages/optimizely-sdk/package-lock.json @@ -17710,6 +17710,12 @@ "has-tostringtag": "^1.0.0" } }, + "is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true + }, "is-plain-object": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", @@ -19115,6 +19121,15 @@ "integrity": "sha512-VjFo4P5Whtj4vsLzsYBu5ayHhoHJ0UqNm7ibvShmbmoz7tGi0vXaoJbGdB+GmDMLUdg8DpQXEIeVDAe8MaABvQ==", "dev": true }, + "merge-options": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/merge-options/-/merge-options-3.0.4.tgz", + "integrity": "sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ==", + "dev": true, + "requires": { + "is-plain-obj": "^2.1.0" + } + }, "merge-stream": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-1.0.1.tgz", @@ -22375,6 +22390,37 @@ } } }, + "tsconfig-paths": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.0.0.tgz", + "integrity": "sha512-SLBg2GBKlR6bVtMgJJlud/o3waplKtL7skmLkExomIiaAtLGtVsoXIqP3SYdjbcH9lq/KVv7pMZeCBpLYOit6Q==", + "dev": true, + "requires": { + "json5": "^2.2.1", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "dependencies": { + "json5": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", + "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", + "dev": true + }, + "minimist": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", + "dev": true + }, + "strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", + "dev": true + } + } + }, "tslib": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", diff --git a/packages/optimizely-sdk/package.json b/packages/optimizely-sdk/package.json index 8a3827ebe..9ecccc92a 100644 --- a/packages/optimizely-sdk/package.json +++ b/packages/optimizely-sdk/package.json @@ -41,12 +41,13 @@ "homepage": "https://github.com/optimizely/javascript-sdk/tree/master/packages/optimizely-sdk", "dependencies": { "@optimizely/js-sdk-datafile-manager": "^0.9.5", - "@optimizely/js-sdk-event-processor": "^0.9.2", "json-schema": "^0.4.0", "murmurhash": "^2.0.1", "uuid": "^8.3.2" }, "devDependencies": { + "@react-native-async-storage/async-storage": "^1.2.0", + "@react-native-community/netinfo": "^5.9.10", "@rollup/plugin-commonjs": "^11.0.2", "@rollup/plugin-node-resolve": "^7.1.1", "@types/chai": "^4.2.11", @@ -84,6 +85,19 @@ "webpack": "^5.74.0", "tslib": "^2.4.0" }, + "peerDependencies": { + "@react-native-community/netinfo": "5.9.4", + "@react-native-async-storage/async-storage": "^1.2.0", + "@babel/runtime": "^7.0.0" + }, + "peerDependenciesMeta": { + "@react-native-async-storage/async-storage": { + "optional": true + }, + "@react-native-community/netinfo": { + "optional": true + } + }, "publishConfig": { "access": "public" }, diff --git a/packages/optimizely-sdk/rollup.config.js b/packages/optimizely-sdk/rollup.config.js index 7afe33aea..70d60c278 100644 --- a/packages/optimizely-sdk/rollup.config.js +++ b/packages/optimizely-sdk/rollup.config.js @@ -17,7 +17,7 @@ import commonjs from '@rollup/plugin-commonjs'; import { terser } from 'rollup-plugin-terser'; import resolve from '@rollup/plugin-node-resolve'; -import { dependencies } from './package.json'; +import { dependencies, peerDependencies } from './package.json'; import typescript from 'rollup-plugin-typescript2'; const typescriptPluginOptions = { @@ -42,7 +42,7 @@ const cjsBundleFor = (platform) => ({ commonjs(), typescript(typescriptPluginOptions), ], - external: ['https', 'http', 'url'].concat(Object.keys(dependencies || {})), + external: ['https', 'http', 'url'].concat(Object.keys({ ...dependencies, ...peerDependencies } || {})), input: `lib/index.${platform}.ts`, output: { exports: 'named', diff --git a/packages/optimizely-sdk/tests/buildEventV1.spec.ts b/packages/optimizely-sdk/tests/buildEventV1.spec.ts new file mode 100644 index 000000000..273bcea6e --- /dev/null +++ b/packages/optimizely-sdk/tests/buildEventV1.spec.ts @@ -0,0 +1,812 @@ +/** + * Copyright 2022, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/// + +import { + buildConversionEventV1, + buildImpressionEventV1, + makeBatchedEventV1, +} from '../lib/modules/event_processor/v1/buildEventV1' +import { ImpressionEvent, ConversionEvent } from '../lib/modules/event_processor/events' + +describe('buildEventV1', () => { + describe('buildImpressionEventV1', () => { + it('should build an ImpressionEventV1 when experiment and variation are defined', () => { + const impressionEvent: ImpressionEvent = { + type: 'impression', + timestamp: 69, + uuid: 'uuid', + + context: { + accountId: 'accountId', + projectId: 'projectId', + clientName: 'node-sdk', + clientVersion: '3.0.0', + revision: 'revision', + botFiltering: true, + anonymizeIP: true, + }, + + user: { + id: 'userId', + attributes: [{ entityId: 'attr1-id', key: 'attr1-key', value: 'attr1-value' }], + }, + + layer: { + id: 'layerId', + }, + + experiment: { + id: 'expId', + key: 'expKey', + }, + + variation: { + id: 'varId', + key: 'varKey', + }, + + ruleKey: 'expKey', + flagKey: 'flagKey1', + ruleType: 'experiment', + enabled: true, + } + + const result = buildImpressionEventV1(impressionEvent) + expect(result).toEqual({ + client_name: 'node-sdk', + client_version: '3.0.0', + account_id: 'accountId', + project_id: 'projectId', + revision: 'revision', + anonymize_ip: true, + enrich_decisions: true, + + visitors: [ + { + snapshots: [ + { + decisions: [ + { + campaign_id: 'layerId', + experiment_id: 'expId', + variation_id: 'varId', + metadata: { + flag_key: 'flagKey1', + rule_key: 'expKey', + rule_type: 'experiment', + variation_key: 'varKey', + enabled: true, + }, + }, + ], + events: [ + { + entity_id: 'layerId', + timestamp: 69, + key: 'campaign_activated', + uuid: 'uuid', + }, + ], + }, + ], + visitor_id: 'userId', + attributes: [ + { + entity_id: 'attr1-id', + key: 'attr1-key', + type: 'custom', + value: 'attr1-value', + }, + { + entity_id: '$opt_bot_filtering', + key: '$opt_bot_filtering', + type: 'custom', + value: true, + }, + ], + }, + ], + }) + }) + + it('should build an ImpressionEventV1 when experiment and variation are not defined', () => { + const impressionEvent: ImpressionEvent = { + type: 'impression', + timestamp: 69, + uuid: 'uuid', + + context: { + accountId: 'accountId', + projectId: 'projectId', + clientName: 'node-sdk', + clientVersion: '3.0.0', + revision: 'revision', + botFiltering: true, + anonymizeIP: true, + }, + + user: { + id: 'userId', + attributes: [{ entityId: 'attr1-id', key: 'attr1-key', value: 'attr1-value' }], + }, + + layer: { + id: null, + }, + + experiment: { + id: null, + key: '', + }, + + variation: { + id: null, + key: '', + }, + + ruleKey: '', + flagKey: 'flagKey1', + ruleType: 'rollout', + enabled: true, + } + + const result = buildImpressionEventV1(impressionEvent) + expect(result).toEqual({ + client_name: 'node-sdk', + client_version: '3.0.0', + account_id: 'accountId', + project_id: 'projectId', + revision: 'revision', + anonymize_ip: true, + enrich_decisions: true, + + visitors: [ + { + snapshots: [ + { + decisions: [ + { + campaign_id: null, + experiment_id: "", + variation_id: "", + metadata: { + flag_key: 'flagKey1', + rule_key: '', + rule_type: 'rollout', + variation_key: '', + enabled: true, + }, + }, + ], + events: [ + { + entity_id: null, + timestamp: 69, + key: 'campaign_activated', + uuid: 'uuid', + }, + ], + }, + ], + visitor_id: 'userId', + attributes: [ + { + entity_id: 'attr1-id', + key: 'attr1-key', + type: 'custom', + value: 'attr1-value', + }, + { + entity_id: '$opt_bot_filtering', + key: '$opt_bot_filtering', + type: 'custom', + value: true, + }, + ], + }, + ], + }) + }) + }) + + describe('buildConversionEventV1', () => { + it('should build a ConversionEventV1 when tags object is defined', () => { + const conversionEvent: ConversionEvent = { + type: 'conversion', + timestamp: 69, + uuid: 'uuid', + + context: { + accountId: 'accountId', + projectId: 'projectId', + clientName: 'node-sdk', + clientVersion: '3.0.0', + revision: 'revision', + botFiltering: true, + anonymizeIP: true, + }, + + user: { + id: 'userId', + attributes: [{ entityId: 'attr1-id', key: 'attr1-key', value: 'attr1-value' }], + }, + + event: { + id: 'event-id', + key: 'event-key', + }, + + tags: { + foo: 'bar', + value: '123', + revenue: '1000', + }, + + revenue: 1000, + value: 123, + } + + const result = buildConversionEventV1(conversionEvent) + expect(result).toEqual({ + client_name: 'node-sdk', + client_version: '3.0.0', + account_id: 'accountId', + project_id: 'projectId', + revision: 'revision', + anonymize_ip: true, + enrich_decisions: true, + + visitors: [ + { + snapshots: [ + { + events: [ + { + entity_id: 'event-id', + timestamp: 69, + key: 'event-key', + uuid: 'uuid', + tags: { + foo: 'bar', + value: '123', + revenue: '1000', + }, + revenue: 1000, + value: 123, + }, + ], + }, + ], + visitor_id: 'userId', + attributes: [ + { + entity_id: 'attr1-id', + key: 'attr1-key', + type: 'custom', + value: 'attr1-value', + }, + { + entity_id: '$opt_bot_filtering', + key: '$opt_bot_filtering', + type: 'custom', + value: true, + }, + ], + }, + ], + }) + }) + + it('should build a ConversionEventV1 when tags object is undefined', () => { + const conversionEvent: ConversionEvent = { + type: 'conversion', + timestamp: 69, + uuid: 'uuid', + + context: { + accountId: 'accountId', + projectId: 'projectId', + clientName: 'node-sdk', + clientVersion: '3.0.0', + revision: 'revision', + botFiltering: true, + anonymizeIP: true, + }, + + user: { + id: 'userId', + attributes: [{ entityId: 'attr1-id', key: 'attr1-key', value: 'attr1-value' }], + }, + + event: { + id: 'event-id', + key: 'event-key', + }, + + tags: undefined, + + revenue: 1000, + value: 123, + } + + const result = buildConversionEventV1(conversionEvent) + expect(result).toEqual({ + client_name: 'node-sdk', + client_version: '3.0.0', + account_id: 'accountId', + project_id: 'projectId', + revision: 'revision', + anonymize_ip: true, + enrich_decisions: true, + + visitors: [ + { + snapshots: [ + { + events: [ + { + entity_id: 'event-id', + timestamp: 69, + key: 'event-key', + uuid: 'uuid', + tags: undefined, + revenue: 1000, + value: 123, + }, + ], + }, + ], + visitor_id: 'userId', + attributes: [ + { + entity_id: 'attr1-id', + key: 'attr1-key', + type: 'custom', + value: 'attr1-value', + }, + { + entity_id: '$opt_bot_filtering', + key: '$opt_bot_filtering', + type: 'custom', + value: true, + }, + ], + }, + ], + }) + }) + + it('should build a ConversionEventV1 when event id is null', () => { + const conversionEvent: ConversionEvent = { + type: 'conversion', + timestamp: 69, + uuid: 'uuid', + + context: { + accountId: 'accountId', + projectId: 'projectId', + clientName: 'node-sdk', + clientVersion: '3.0.0', + revision: 'revision', + botFiltering: true, + anonymizeIP: true, + }, + + user: { + id: 'userId', + attributes: [{ entityId: 'attr1-id', key: 'attr1-key', value: 'attr1-value' }], + }, + + event: { + id: null, + key: 'event-key', + }, + + tags: undefined, + + revenue: 1000, + value: 123, + } + + const result = buildConversionEventV1(conversionEvent) + expect(result).toEqual({ + client_name: 'node-sdk', + client_version: '3.0.0', + account_id: 'accountId', + project_id: 'projectId', + revision: 'revision', + anonymize_ip: true, + enrich_decisions: true, + + visitors: [ + { + snapshots: [ + { + events: [ + { + entity_id: null, + timestamp: 69, + key: 'event-key', + uuid: 'uuid', + tags: undefined, + revenue: 1000, + value: 123, + }, + ], + }, + ], + visitor_id: 'userId', + attributes: [ + { + entity_id: 'attr1-id', + key: 'attr1-key', + type: 'custom', + value: 'attr1-value', + }, + { + entity_id: '$opt_bot_filtering', + key: '$opt_bot_filtering', + type: 'custom', + value: true, + }, + ], + }, + ], + }) + }) + + it('should include revenue and value if they are 0', () => { + const conversionEvent: ConversionEvent = { + type: 'conversion', + timestamp: 69, + uuid: 'uuid', + + context: { + accountId: 'accountId', + projectId: 'projectId', + clientName: 'node-sdk', + clientVersion: '3.0.0', + revision: 'revision', + botFiltering: true, + anonymizeIP: true, + }, + + user: { + id: 'userId', + attributes: [{ entityId: 'attr1-id', key: 'attr1-key', value: 'attr1-value' }], + }, + + event: { + id: 'event-id', + key: 'event-key', + }, + + tags: { + foo: 'bar', + value: 0, + revenue: 0, + }, + + revenue: 0, + value: 0, + } + + const result = buildConversionEventV1(conversionEvent) + expect(result).toEqual({ + client_name: 'node-sdk', + client_version: '3.0.0', + account_id: 'accountId', + project_id: 'projectId', + revision: 'revision', + anonymize_ip: true, + enrich_decisions: true, + + visitors: [ + { + snapshots: [ + { + events: [ + { + entity_id: 'event-id', + timestamp: 69, + key: 'event-key', + uuid: 'uuid', + tags: { + foo: 'bar', + value: 0, + revenue: 0, + }, + revenue: 0, + value: 0, + }, + ], + }, + ], + visitor_id: 'userId', + attributes: [ + { + entity_id: 'attr1-id', + key: 'attr1-key', + type: 'custom', + value: 'attr1-value', + }, + { + entity_id: '$opt_bot_filtering', + key: '$opt_bot_filtering', + type: 'custom', + value: true, + }, + ], + }, + ], + }) + }) + + it('should not include $opt_bot_filtering attribute if context.botFiltering is undefined', () => { + const conversionEvent: ConversionEvent = { + type: 'conversion', + timestamp: 69, + uuid: 'uuid', + + context: { + accountId: 'accountId', + projectId: 'projectId', + clientName: 'node-sdk', + clientVersion: '3.0.0', + revision: 'revision', + anonymizeIP: true, + }, + + user: { + id: 'userId', + attributes: [{ entityId: 'attr1-id', key: 'attr1-key', value: 'attr1-value' }], + }, + + event: { + id: 'event-id', + key: 'event-key', + }, + + tags: { + foo: 'bar', + value: '123', + revenue: '1000', + }, + + revenue: 1000, + value: 123, + } + + const result = buildConversionEventV1(conversionEvent) + expect(result).toEqual({ + client_name: 'node-sdk', + client_version: '3.0.0', + account_id: 'accountId', + project_id: 'projectId', + revision: 'revision', + anonymize_ip: true, + enrich_decisions: true, + + visitors: [ + { + snapshots: [ + { + events: [ + { + entity_id: 'event-id', + timestamp: 69, + key: 'event-key', + uuid: 'uuid', + tags: { + foo: 'bar', + value: '123', + revenue: '1000', + }, + revenue: 1000, + value: 123, + }, + ], + }, + ], + visitor_id: 'userId', + attributes: [ + { + entity_id: 'attr1-id', + key: 'attr1-key', + type: 'custom', + value: 'attr1-value', + }, + ], + }, + ], + }) + }) + }) + + describe('makeBatchedEventV1', () => { + it('should batch Conversion and Impression events together', () => { + const conversionEvent: ConversionEvent = { + type: 'conversion', + timestamp: 69, + uuid: 'uuid', + + context: { + accountId: 'accountId', + projectId: 'projectId', + clientName: 'node-sdk', + clientVersion: '3.0.0', + revision: 'revision', + botFiltering: true, + anonymizeIP: true, + }, + + user: { + id: 'userId', + attributes: [{ entityId: 'attr1-id', key: 'attr1-key', value: 'attr1-value' }], + }, + + event: { + id: 'event-id', + key: 'event-key', + }, + + tags: { + foo: 'bar', + value: '123', + revenue: '1000', + }, + + revenue: 1000, + value: 123, + } + + const impressionEvent: ImpressionEvent = { + type: 'impression', + timestamp: 69, + uuid: 'uuid', + + context: { + accountId: 'accountId', + projectId: 'projectId', + clientName: 'node-sdk', + clientVersion: '3.0.0', + revision: 'revision', + botFiltering: true, + anonymizeIP: true, + }, + + user: { + id: 'userId', + attributes: [{ entityId: 'attr1-id', key: 'attr1-key', value: 'attr1-value' }], + }, + + layer: { + id: 'layerId', + }, + + experiment: { + id: 'expId', + key: 'expKey', + }, + + variation: { + id: 'varId', + key: 'varKey', + }, + + ruleKey: 'expKey', + flagKey: 'flagKey1', + ruleType: 'experiment', + enabled: true, + } + + const result = makeBatchedEventV1([impressionEvent, conversionEvent]) + + expect(result).toEqual({ + client_name: 'node-sdk', + client_version: '3.0.0', + account_id: 'accountId', + project_id: 'projectId', + revision: 'revision', + anonymize_ip: true, + enrich_decisions: true, + + visitors: [ + { + snapshots: [ + { + decisions: [ + { + campaign_id: 'layerId', + experiment_id: 'expId', + variation_id: 'varId', + metadata: { + flag_key: 'flagKey1', + rule_key: 'expKey', + rule_type: 'experiment', + variation_key: 'varKey', + enabled: true, + }, + }, + ], + events: [ + { + entity_id: 'layerId', + timestamp: 69, + key: 'campaign_activated', + uuid: 'uuid', + }, + ], + }, + ], + visitor_id: 'userId', + attributes: [ + { + entity_id: 'attr1-id', + key: 'attr1-key', + type: 'custom', + value: 'attr1-value', + }, + { + entity_id: '$opt_bot_filtering', + key: '$opt_bot_filtering', + type: 'custom', + value: true, + }, + ], + }, + { + snapshots: [ + { + events: [ + { + entity_id: 'event-id', + timestamp: 69, + key: 'event-key', + uuid: 'uuid', + tags: { + foo: 'bar', + value: '123', + revenue: '1000', + }, + revenue: 1000, + value: 123, + }, + ], + }, + ], + visitor_id: 'userId', + attributes: [ + { + entity_id: 'attr1-id', + key: 'attr1-key', + type: 'custom', + value: 'attr1-value', + }, + { + entity_id: '$opt_bot_filtering', + key: '$opt_bot_filtering', + type: 'custom', + value: true, + }, + ], + }, + ], + }) + }) + }) +}) diff --git a/packages/optimizely-sdk/tests/eventQueue.spec.ts b/packages/optimizely-sdk/tests/eventQueue.spec.ts new file mode 100644 index 000000000..0ebf82454 --- /dev/null +++ b/packages/optimizely-sdk/tests/eventQueue.spec.ts @@ -0,0 +1,290 @@ +/** + * Copyright 2022, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/// + +import { DefaultEventQueue, SingleEventQueue } from '../lib/modules/event_processor/eventQueue' + +describe('eventQueue', () => { + beforeEach(() => { + jest.useFakeTimers() + }) + + afterEach(() => { + jest.useRealTimers() + jest.resetAllMocks() + }) + + describe('SingleEventQueue', () => { + it('should immediately invoke the sink function when items are enqueued', () => { + const sinkFn = jest.fn() + const queue = new SingleEventQueue({ + sink: sinkFn, + }) + + queue.start() + + queue.enqueue(1) + + expect(sinkFn).toBeCalledTimes(1) + expect(sinkFn).toHaveBeenLastCalledWith([1]) + + queue.enqueue(2) + expect(sinkFn).toBeCalledTimes(2) + expect(sinkFn).toHaveBeenLastCalledWith([2]) + + queue.stop() + }) + }) + + describe('DefaultEventQueue', () => { + it('should treat maxQueueSize = -1 as 1', () => { + const sinkFn = jest.fn() + const queue = new DefaultEventQueue({ + flushInterval: 100, + maxQueueSize: -1, + sink: sinkFn, + batchComparator: () => true + }) + + queue.start() + + queue.enqueue(1) + expect(sinkFn).toHaveBeenCalledTimes(1) + expect(sinkFn).toHaveBeenCalledWith([1]) + queue.enqueue(2) + expect(sinkFn).toHaveBeenCalledTimes(2) + expect(sinkFn).toHaveBeenCalledWith([2]) + + queue.stop() + }) + + it('should treat maxQueueSize = 0 as 1', () => { + const sinkFn = jest.fn() + const queue = new DefaultEventQueue({ + flushInterval: 100, + maxQueueSize: 0, + sink: sinkFn, + batchComparator: () => true + }) + + queue.start() + + queue.enqueue(1) + expect(sinkFn).toHaveBeenCalledTimes(1) + expect(sinkFn).toHaveBeenCalledWith([1]) + queue.enqueue(2) + expect(sinkFn).toHaveBeenCalledTimes(2) + expect(sinkFn).toHaveBeenCalledWith([2]) + + queue.stop() + }) + + it('should invoke the sink function when maxQueueSize is reached', () => { + const sinkFn = jest.fn() + const queue = new DefaultEventQueue({ + flushInterval: 100, + maxQueueSize: 3, + sink: sinkFn, + batchComparator: () => true + }) + + queue.start() + + queue.enqueue(1) + queue.enqueue(2) + expect(sinkFn).not.toHaveBeenCalled() + + queue.enqueue(3) + expect(sinkFn).toHaveBeenCalledTimes(1) + expect(sinkFn).toHaveBeenCalledWith([1, 2, 3]) + + queue.enqueue(4) + queue.enqueue(5) + queue.enqueue(6) + expect(sinkFn).toHaveBeenCalledTimes(2) + expect(sinkFn).toHaveBeenCalledWith([4, 5, 6]) + + queue.stop() + }) + + it('should invoke the sink function when the interval has expired', () => { + const sinkFn = jest.fn() + const queue = new DefaultEventQueue({ + flushInterval: 100, + maxQueueSize: 100, + sink: sinkFn, + batchComparator: () => true + }) + + queue.start() + + queue.enqueue(1) + queue.enqueue(2) + expect(sinkFn).not.toHaveBeenCalled() + + jest.advanceTimersByTime(100) + + expect(sinkFn).toHaveBeenCalledTimes(1) + expect(sinkFn).toHaveBeenCalledWith([1, 2]) + + queue.enqueue(3) + jest.advanceTimersByTime(100) + + expect(sinkFn).toHaveBeenCalledTimes(2) + expect(sinkFn).toHaveBeenCalledWith([3]) + + queue.stop() + }) + + it('should invoke the sink function when an item incompatable with the current batch (according to batchComparator) is received', () => { + const sinkFn = jest.fn() + const queue = new DefaultEventQueue({ + flushInterval: 100, + maxQueueSize: 100, + sink: sinkFn, + // This batchComparator returns true when the argument strings start with the same letter + batchComparator: (s1, s2) => s1[0] === s2[0] + }) + + queue.start() + + queue.enqueue('a1') + queue.enqueue('a2') + // After enqueuing these strings, both starting with 'a', the sinkFn should not yet be called. Thus far all the items enqueued are + // compatible according to the batchComparator. + expect(sinkFn).not.toHaveBeenCalled() + + // Enqueuing a string starting with 'b' should cause the sinkFn to be called + queue.enqueue('b1') + expect(sinkFn).toHaveBeenCalledTimes(1) + expect(sinkFn).toHaveBeenCalledWith(['a1', 'a2']) + }) + + it('stop() should flush the existing queue and call timer.stop()', () => { + const sinkFn = jest.fn() + const queue = new DefaultEventQueue({ + flushInterval: 100, + maxQueueSize: 100, + sink: sinkFn, + batchComparator: () => true + }) + + jest.spyOn(queue.timer, 'stop') + + queue.start() + queue.enqueue(1) + + // stop + start is called when the first item is enqueued + expect(queue.timer.stop).toHaveBeenCalledTimes(1) + + queue.stop() + + expect(sinkFn).toHaveBeenCalledTimes(1) + expect(sinkFn).toHaveBeenCalledWith([1]) + expect(queue.timer.stop).toHaveBeenCalledTimes(2) + }) + + it('flush() should clear the current batch', () => { + const sinkFn = jest.fn() + const queue = new DefaultEventQueue({ + flushInterval: 100, + maxQueueSize: 100, + sink: sinkFn, + batchComparator: () => true + }) + + jest.spyOn(queue.timer, 'refresh') + + queue.start() + queue.enqueue(1) + queue.flush() + + expect(sinkFn).toHaveBeenCalledTimes(1) + expect(sinkFn).toHaveBeenCalledWith([1]) + expect(queue.timer.refresh).toBeCalledTimes(1) + + queue.stop() + }) + + it('stop() should return a promise', () => { + const promise = Promise.resolve() + const sinkFn = jest.fn().mockReturnValue(promise) + const queue = new DefaultEventQueue({ + flushInterval: 100, + maxQueueSize: 100, + sink: sinkFn, + batchComparator: () => true + }) + + expect(queue.stop()).toBe(promise) + }) + + it('should start the timer when the first event is put into the queue', () => { + const sinkFn = jest.fn() + const queue = new DefaultEventQueue({ + flushInterval: 100, + maxQueueSize: 100, + sink: sinkFn, + batchComparator: () => true + }) + + queue.start() + jest.advanceTimersByTime(99) + queue.enqueue(1) + + jest.advanceTimersByTime(2) + expect(sinkFn).toHaveBeenCalledTimes(0) + jest.advanceTimersByTime(98) + + expect(sinkFn).toHaveBeenCalledTimes(1) + expect(sinkFn).toHaveBeenCalledWith([1]) + + jest.advanceTimersByTime(500) + // ensure sink function wasnt called again since no events have + // been added + expect(sinkFn).toHaveBeenCalledTimes(1) + + queue.enqueue(2) + + jest.advanceTimersByTime(100) + expect(sinkFn).toHaveBeenCalledTimes(2) + expect(sinkFn).toHaveBeenLastCalledWith([2]) + + queue.stop() + + }) + + it('should not enqueue additional events after stop() is called', () => { + const sinkFn = jest.fn() + const queue = new DefaultEventQueue({ + flushInterval: 30000, + maxQueueSize: 3, + sink: sinkFn, + batchComparator: () => true + }) + queue.start() + queue.enqueue(1) + queue.stop() + expect(sinkFn).toHaveBeenCalledTimes(1) + expect(sinkFn).toHaveBeenCalledWith([1]) + sinkFn.mockClear() + queue.enqueue(2) + queue.enqueue(3) + queue.enqueue(4) + expect(sinkFn).toBeCalledTimes(0) + }) + }) +}) diff --git a/packages/optimizely-sdk/tests/index.react_native.spec.ts b/packages/optimizely-sdk/tests/index.react_native.spec.ts new file mode 100644 index 000000000..1717bdf55 --- /dev/null +++ b/packages/optimizely-sdk/tests/index.react_native.spec.ts @@ -0,0 +1,340 @@ +/** + * Copyright 2019-2020, 2022 Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/// +import * as logging from '../lib/modules/logging/logger'; +import * as eventProcessor from '../lib//plugins/event_processor/index.react_native'; + +import Optimizely from '../lib/optimizely'; +import testData from '../lib/tests/test_data'; +import packageJSON from '../package.json'; +import optimizelyFactory from '../lib/index.react_native'; +import configValidator from '../lib/utils/config_validator'; +import eventProcessorConfigValidator from '../lib/utils/event_processor_config_validator'; + +describe('javascript-sdk/react-native', () => { + beforeEach(() => { + jest.spyOn(optimizelyFactory.eventDispatcher, 'dispatchEvent'); + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe('APIs', () => { + it('should expose logger, errorHandler, eventDispatcher and enums', () => { + expect(optimizelyFactory.logging).toBeDefined(); + expect(optimizelyFactory.logging.createLogger).toBeDefined(); + expect(optimizelyFactory.logging.createNoOpLogger).toBeDefined(); + expect(optimizelyFactory.errorHandler).toBeDefined(); + expect(optimizelyFactory.eventDispatcher).toBeDefined(); + expect(optimizelyFactory.enums).toBeDefined(); + }); + + describe('createInstance', () => { + var fakeErrorHandler = { handleError: function () {} }; + var fakeEventDispatcher = { dispatchEvent: function () {} }; + // @ts-ignore + var silentLogger; + + beforeEach(() => { + // @ts-ignore + silentLogger = optimizelyFactory.logging.createLogger(); + jest.spyOn(console, 'error'); + jest.spyOn(configValidator, 'validate').mockImplementation(() => { + throw new Error('Invalid config or something'); + }); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + it('should not throw if the provided config is not valid', () => { + expect(function () { + var optlyInstance = optimizelyFactory.createInstance({ + datafile: {}, + // @ts-ignore + logger: silentLogger, + }); + // Invalid datafile causes onReady Promise rejection - catch this error + // @ts-ignore + optlyInstance.onReady().catch(function () {}); + }).not.toThrow(); + }); + + it('should create an instance of optimizely', () => { + var optlyInstance = optimizelyFactory.createInstance({ + datafile: {}, + errorHandler: fakeErrorHandler, + eventDispatcher: fakeEventDispatcher, + // @ts-ignore + logger: silentLogger, + }); + // Invalid datafile causes onReady Promise rejection - catch this error + // @ts-ignore + optlyInstance.onReady().catch(function () {}); + + expect(optlyInstance).toBeInstanceOf(Optimizely); + // @ts-ignore + expect(optlyInstance.clientVersion).toEqual('4.9.2'); + }); + + it('should set the React Native JS client engine and javascript SDK version', () => { + var optlyInstance = optimizelyFactory.createInstance({ + datafile: {}, + errorHandler: fakeErrorHandler, + eventDispatcher: fakeEventDispatcher, + // @ts-ignore + logger: silentLogger, + }); + // Invalid datafile causes onReady Promise rejection - catch this error + // @ts-ignore + optlyInstance.onReady().catch(function () {}); + // @ts-ignore + expect('react-native-js-sdk').toEqual(optlyInstance.clientEngine); + // @ts-ignore + expect(packageJSON.version).toEqual(optlyInstance.clientVersion); + }); + + it('should allow passing of "react-sdk" as the clientEngine and convert it to "react-native-sdk"', () => { + var optlyInstance = optimizelyFactory.createInstance({ + clientEngine: 'react-sdk', + datafile: {}, + errorHandler: fakeErrorHandler, + eventDispatcher: fakeEventDispatcher, + // @ts-ignore + logger: silentLogger, + }); + // Invalid datafile causes onReady Promise rejection - catch this error + // @ts-ignore + optlyInstance.onReady().catch(function () {}); + // @ts-ignore + expect('react-native-sdk').toEqual(optlyInstance.clientEngine); + }); + + describe('when passing in logLevel', () => { + beforeEach(() => { + jest.spyOn(logging, 'setLogLevel'); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + it('should call logging.setLogLevel', () => { + optimizelyFactory.createInstance({ + datafile: testData.getTestProjectConfig(), + logLevel: optimizelyFactory.enums.LOG_LEVEL.ERROR, + }); + expect(logging.setLogLevel).toBeCalledTimes(1); + expect(logging.setLogLevel).toBeCalledWith(optimizelyFactory.enums.LOG_LEVEL.ERROR); + }); + }); + + describe('when passing in logger', () => { + beforeEach(() => { + jest.spyOn(logging, 'setLogHandler'); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + it( + 'should call logging.setLogHandler with the supplied logger', + () => { + var fakeLogger = { log: function () { } }; + optimizelyFactory.createInstance({ + datafile: testData.getTestProjectConfig(), + // @ts-ignore + logger: fakeLogger, + }); + expect(logging.setLogHandler).toBeCalledTimes(1); + expect(logging.setLogHandler).toBeCalledWith(fakeLogger); + } + ); + }); + + describe('event processor configuration', () => { + // @ts-ignore + var eventProcessorSpy; + beforeEach(() => { + eventProcessorSpy = jest.spyOn(eventProcessor, 'createEventProcessor'); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + it('should use default event flush interval when none is provided', () => { + optimizelyFactory.createInstance({ + datafile: testData.getTestProjectConfigWithFeatures(), + errorHandler: fakeErrorHandler, + eventDispatcher: fakeEventDispatcher, + // @ts-ignore + logger: silentLogger, + }); + + expect( + // @ts-ignore + eventProcessorSpy + ).toBeCalledWith( + expect.objectContaining({ + flushInterval: 1000, + }) + ); + }); + + describe('with an invalid flush interval', () => { + beforeEach(() => { + jest.spyOn(eventProcessorConfigValidator, 'validateEventFlushInterval').mockImplementation(() => false); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + it('should ignore the event flush interval and use the default instead', () => { + optimizelyFactory.createInstance({ + datafile: testData.getTestProjectConfigWithFeatures(), + errorHandler: fakeErrorHandler, + eventDispatcher: fakeEventDispatcher, + // @ts-ignore + logger: silentLogger, + // @ts-ignore + eventFlushInterval: ['invalid', 'flush', 'interval'], + }); + expect( + // @ts-ignore + eventProcessorSpy + ).toBeCalledWith( + expect.objectContaining({ + flushInterval: 1000, + }) + ); + }); + }); + + describe('with a valid flush interval', () => { + beforeEach(() => { + jest.spyOn(eventProcessorConfigValidator, 'validateEventFlushInterval').mockImplementation(() => true); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + it('should use the provided event flush interval', () => { + optimizelyFactory.createInstance({ + datafile: testData.getTestProjectConfigWithFeatures(), + errorHandler: fakeErrorHandler, + eventDispatcher: fakeEventDispatcher, + // @ts-ignore + logger: silentLogger, + eventFlushInterval: 9000, + }); + expect( + // @ts-ignore + eventProcessorSpy + ).toBeCalledWith( + expect.objectContaining({ + flushInterval: 9000, + }) + ); + }); + }); + + it('should use default event batch size when none is provided', () => { + optimizelyFactory.createInstance({ + datafile: testData.getTestProjectConfigWithFeatures(), + errorHandler: fakeErrorHandler, + eventDispatcher: fakeEventDispatcher, + // @ts-ignore + logger: silentLogger, + }); + expect( + // @ts-ignore + eventProcessorSpy + ).toBeCalledWith( + expect.objectContaining({ + batchSize: 10, + }) + ); + }); + + describe('with an invalid event batch size', () => { + beforeEach(() => { + jest.spyOn(eventProcessorConfigValidator, 'validateEventBatchSize').mockImplementation(() => false); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + it('should ignore the event batch size and use the default instead', () => { + optimizelyFactory.createInstance({ + datafile: testData.getTestProjectConfigWithFeatures(), + errorHandler: fakeErrorHandler, + eventDispatcher: fakeEventDispatcher, + // @ts-ignore + logger: silentLogger, + // @ts-ignore + eventBatchSize: null, + }); + expect( + // @ts-ignore + eventProcessorSpy + ).toBeCalledWith( + expect.objectContaining({ + batchSize: 10, + }) + ); + }); + }); + + describe('with a valid event batch size', () => { + beforeEach(() => { + jest.spyOn(eventProcessorConfigValidator, 'validateEventBatchSize').mockImplementation(() => true); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + it('should use the provided event batch size', () => { + optimizelyFactory.createInstance({ + datafile: testData.getTestProjectConfigWithFeatures(), + errorHandler: fakeErrorHandler, + eventDispatcher: fakeEventDispatcher, + // @ts-ignore + logger: silentLogger, + eventBatchSize: 300, + }); + expect( + // @ts-ignore + eventProcessorSpy + ).toBeCalledWith( + expect.objectContaining({ + batchSize: 300, + }) + ); + }); + }); + }); + }); + }); +}); diff --git a/packages/optimizely-sdk/tests/pendingEventsDispatcher.spec.ts b/packages/optimizely-sdk/tests/pendingEventsDispatcher.spec.ts new file mode 100644 index 000000000..214b1e939 --- /dev/null +++ b/packages/optimizely-sdk/tests/pendingEventsDispatcher.spec.ts @@ -0,0 +1,261 @@ +/** + * Copyright 2022, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/// + +jest.mock('../lib/utils/fns', () => ({ + __esModule: true, + uuid: jest.fn(), + getTimestamp: jest.fn(), + objectValues: jest.requireActual('../lib/utils/fns').objectValues, +})) + +import { + LocalStoragePendingEventsDispatcher, + PendingEventsDispatcher, + DispatcherEntry, +} from '../lib/modules/event_processor/pendingEventsDispatcher' +import { EventDispatcher, EventV1Request } from '../lib/modules/event_processor/eventDispatcher' +import { EventV1 } from '../lib/modules/event_processor/v1/buildEventV1' +import { PendingEventsStore, LocalStorageStore } from '../lib/modules/event_processor/pendingEventsStore' +import { uuid, getTimestamp } from '../lib/utils/fns' + +describe('LocalStoragePendingEventsDispatcher', () => { + let originalEventDispatcher: EventDispatcher + let pendingEventsDispatcher: PendingEventsDispatcher + + beforeEach(() => { + originalEventDispatcher = { + dispatchEvent: jest.fn(), + } + pendingEventsDispatcher = new LocalStoragePendingEventsDispatcher({ + eventDispatcher: originalEventDispatcher, + }) + ;((getTimestamp as unknown) as jest.Mock).mockReturnValue(1) + ;((uuid as unknown) as jest.Mock).mockReturnValue('uuid') + }) + + afterEach(() => { + localStorage.clear() + }) + + it('should properly send the events to the passed in eventDispatcher, when callback statusCode=200', () => { + const callback = jest.fn() + const eventV1Request: EventV1Request = { + url: 'http://cdn.com', + httpVerb: 'POST', + params: ({ id: 'event' } as unknown) as EventV1, + } + + pendingEventsDispatcher.dispatchEvent(eventV1Request, callback) + + expect(callback).not.toHaveBeenCalled() + // manually invoke original eventDispatcher callback + const internalDispatchCall = ((originalEventDispatcher.dispatchEvent as unknown) as jest.Mock) + .mock.calls[0] + internalDispatchCall[1]({ statusCode: 200 }) + + // assert that the original dispatch function was called with the request + expect((originalEventDispatcher.dispatchEvent as unknown) as jest.Mock).toBeCalledTimes(1) + expect(internalDispatchCall[0]).toEqual(eventV1Request) + + // assert that the passed in callback to pendingEventsDispatcher was called + expect(callback).toHaveBeenCalledTimes(1) + expect(callback).toHaveBeenCalledWith({ statusCode: 200 }) + }) + + it('should properly send the events to the passed in eventDispatcher, when callback statusCode=400', () => { + const callback = jest.fn() + const eventV1Request: EventV1Request = { + url: 'http://cdn.com', + httpVerb: 'POST', + params: ({ id: 'event' } as unknown) as EventV1, + } + + pendingEventsDispatcher.dispatchEvent(eventV1Request, callback) + + expect(callback).not.toHaveBeenCalled() + // manually invoke original eventDispatcher callback + const internalDispatchCall = ((originalEventDispatcher.dispatchEvent as unknown) as jest.Mock) + .mock.calls[0] + internalDispatchCall[1]({ statusCode: 400 }) + + // assert that the original dispatch function was called with the request + expect((originalEventDispatcher.dispatchEvent as unknown) as jest.Mock).toBeCalledTimes(1) + expect(internalDispatchCall[0]).toEqual(eventV1Request) + + // assert that the passed in callback to pendingEventsDispatcher was called + expect(callback).toHaveBeenCalledTimes(1) + expect(callback).toHaveBeenCalledWith({ statusCode: 400}) + }) +}) + +describe('PendingEventsDispatcher', () => { + let originalEventDispatcher: EventDispatcher + let pendingEventsDispatcher: PendingEventsDispatcher + let store: PendingEventsStore + + beforeEach(() => { + originalEventDispatcher = { + dispatchEvent: jest.fn(), + } + store = new LocalStorageStore({ + key: 'test', + maxValues: 3, + }) + pendingEventsDispatcher = new PendingEventsDispatcher({ + store, + eventDispatcher: originalEventDispatcher, + }) + ;((getTimestamp as unknown) as jest.Mock).mockReturnValue(1) + ;((uuid as unknown) as jest.Mock).mockReturnValue('uuid') + }) + + afterEach(() => { + localStorage.clear() + }) + + describe('dispatch', () => { + describe('when the dispatch is successful', () => { + it('should save the pendingEvent to the store and remove it once dispatch is completed', () => { + const callback = jest.fn() + const eventV1Request: EventV1Request = { + url: 'http://cdn.com', + httpVerb: 'POST', + params: ({ id: 'event' } as unknown) as EventV1, + } + + pendingEventsDispatcher.dispatchEvent(eventV1Request, callback) + + expect(store.values()).toHaveLength(1) + expect(store.get('uuid')).toEqual({ + uuid: 'uuid', + timestamp: 1, + request: eventV1Request, + }) + expect(callback).not.toHaveBeenCalled() + + // manually invoke original eventDispatcher callback + const internalDispatchCall = ((originalEventDispatcher.dispatchEvent as unknown) as jest.Mock) + .mock.calls[0] + const internalCallback = internalDispatchCall[1]({ statusCode: 200 }) + + // assert that the original dispatch function was called with the request + expect( + (originalEventDispatcher.dispatchEvent as unknown) as jest.Mock, + ).toBeCalledTimes(1) + expect(internalDispatchCall[0]).toEqual(eventV1Request) + + // assert that the passed in callback to pendingEventsDispatcher was called + expect(callback).toHaveBeenCalledTimes(1) + expect(callback).toHaveBeenCalledWith({ statusCode: 200 }) + + expect(store.values()).toHaveLength(0) + }) + }) + + describe('when the dispatch is unsuccessful', () => { + it('should save the pendingEvent to the store and remove it once dispatch is completed', () => { + const callback = jest.fn() + const eventV1Request: EventV1Request = { + url: 'http://cdn.com', + httpVerb: 'POST', + params: ({ id: 'event' } as unknown) as EventV1, + } + + pendingEventsDispatcher.dispatchEvent(eventV1Request, callback) + + expect(store.values()).toHaveLength(1) + expect(store.get('uuid')).toEqual({ + uuid: 'uuid', + timestamp: 1, + request: eventV1Request, + }) + expect(callback).not.toHaveBeenCalled() + + // manually invoke original eventDispatcher callback + const internalDispatchCall = ((originalEventDispatcher.dispatchEvent as unknown) as jest.Mock) + .mock.calls[0] + internalDispatchCall[1]({ statusCode: 400 }) + + // assert that the original dispatch function was called with the request + expect( + (originalEventDispatcher.dispatchEvent as unknown) as jest.Mock, + ).toBeCalledTimes(1) + expect(internalDispatchCall[0]).toEqual(eventV1Request) + + // assert that the passed in callback to pendingEventsDispatcher was called + expect(callback).toHaveBeenCalledTimes(1) + expect(callback).toHaveBeenCalledWith({ statusCode: 400 }) + + expect(store.values()).toHaveLength(0) + }) + }) + }) + + describe('sendPendingEvents', () => { + describe('when no pending events are in the store', () => { + it('should not invoked dispatch', () => { + expect(store.values()).toHaveLength(0) + + pendingEventsDispatcher.sendPendingEvents() + expect(originalEventDispatcher.dispatchEvent).not.toHaveBeenCalled() + }) + }) + + describe('when there are multiple pending events in the store', () => { + it('should dispatch all of the pending events, and remove them from store', () => { + expect(store.values()).toHaveLength(0) + + const callback = jest.fn() + const eventV1Request1: EventV1Request = { + url: 'http://cdn.com', + httpVerb: 'POST', + params: ({ id: 'event1' } as unknown) as EventV1, + } + + const eventV1Request2: EventV1Request = { + url: 'http://cdn.com', + httpVerb: 'POST', + params: ({ id: 'event2' } as unknown) as EventV1, + } + + store.set('uuid1', { + uuid: 'uuid1', + timestamp: 1, + request: eventV1Request1, + }) + store.set('uuid2', { + uuid: 'uuid2', + timestamp: 2, + request: eventV1Request2, + }) + + expect(store.values()).toHaveLength(2) + + pendingEventsDispatcher.sendPendingEvents() + expect(originalEventDispatcher.dispatchEvent).toHaveBeenCalledTimes(2) + + // manually invoke original eventDispatcher callback + const internalDispatchCalls = ((originalEventDispatcher.dispatchEvent as unknown) as jest.Mock) + .mock.calls + internalDispatchCalls[0][1]({ statusCode: 200 }) + internalDispatchCalls[1][1]({ statusCode: 200 }) + + expect(store.values()).toHaveLength(0) + }) + }) + }) +}) diff --git a/packages/optimizely-sdk/tests/pendingEventsStore.spec.ts b/packages/optimizely-sdk/tests/pendingEventsStore.spec.ts new file mode 100644 index 000000000..9e8062429 --- /dev/null +++ b/packages/optimizely-sdk/tests/pendingEventsStore.spec.ts @@ -0,0 +1,143 @@ +/** + * Copyright 2022, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/// + +import { LocalStorageStore } from '../lib/modules/event_processor/pendingEventsStore' + +type TestEntry = { + uuid: string + timestamp: number + value: string +} + +describe('LocalStorageStore', () => { + let store: LocalStorageStore + beforeEach(() => { + store = new LocalStorageStore({ + key: 'test_key', + maxValues: 3, + }) + }) + + afterEach(() => { + localStorage.clear() + }) + + it('should get, set and remove items', () => { + store.set('1', { + uuid: '1', + timestamp: 1, + value: 'first', + }) + + expect(store.get('1')).toEqual({ + uuid: '1', + timestamp: 1, + value: 'first', + }) + + store.set('1', { + uuid: '1', + timestamp: 2, + value: 'second', + }) + + expect(store.get('1')).toEqual({ + uuid: '1', + timestamp: 2, + value: 'second', + }) + + expect(store.values()).toHaveLength(1) + + store.remove('1') + + expect(store.values()).toHaveLength(0) + }) + + it('should allow replacement of the entire map', () => { + store.set('1', { + uuid: '1', + timestamp: 1, + value: 'first', + }) + + store.set('2', { + uuid: '2', + timestamp: 2, + value: 'second', + }) + + store.set('3', { + uuid: '3', + timestamp: 3, + value: 'third', + }) + + expect(store.values()).toEqual([ + { uuid: '1', timestamp: 1, value: 'first' }, + { uuid: '2', timestamp: 2, value: 'second' }, + { uuid: '3', timestamp: 3, value: 'third' }, + ]) + + const newMap: { [key: string]: TestEntry } = {} + store.values().forEach(item => { + newMap[item.uuid] = { + ...item, + value: 'new', + } + }) + store.replace(newMap) + + expect(store.values()).toEqual([ + { uuid: '1', timestamp: 1, value: 'new' }, + { uuid: '2', timestamp: 2, value: 'new' }, + { uuid: '3', timestamp: 3, value: 'new' }, + ]) + }) + + it(`shouldn't allow more than the configured maxValues, using timestamp to remove the oldest entries`, () => { + store.set('2', { + uuid: '2', + timestamp: 2, + value: 'second', + }) + + store.set('3', { + uuid: '3', + timestamp: 3, + value: 'third', + }) + + store.set('1', { + uuid: '1', + timestamp: 1, + value: 'first', + }) + + store.set('4', { + uuid: '4', + timestamp: 4, + value: 'fourth', + }) + + expect(store.values()).toEqual([ + { uuid: '2', timestamp: 2, value: 'second' }, + { uuid: '3', timestamp: 3, value: 'third' }, + { uuid: '4', timestamp: 4, value: 'fourth' }, + ]) + }) +}) diff --git a/packages/optimizely-sdk/tests/reactNativeEventsStore.spec.ts b/packages/optimizely-sdk/tests/reactNativeEventsStore.spec.ts new file mode 100644 index 000000000..dd26a13a4 --- /dev/null +++ b/packages/optimizely-sdk/tests/reactNativeEventsStore.spec.ts @@ -0,0 +1,219 @@ +/** + * Copyright 2022, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/// + +import { ReactNativeEventsStore } from '../lib/modules/event_processor/reactNativeEventsStore' +import AsyncStorage from '../__mocks__/@react-native-async-storage/async-storage' + +const STORE_KEY = 'test-store' + +describe('ReactNativeEventsStore', () => { + let store: ReactNativeEventsStore + + beforeEach(() => { + store = new ReactNativeEventsStore(5, STORE_KEY) + }) + + describe('set', () => { + it('should store all the events correctly in the store', async () => { + await store.set('event1', {'name': 'event1'}) + await store.set('event2', {'name': 'event2'}) + await store.set('event3', {'name': 'event3'}) + await store.set('event4', {'name': 'event4'}) + const storedPendingEvents = JSON.parse(AsyncStorage.dumpItems()[STORE_KEY]) + expect(storedPendingEvents).toEqual({ + "event1": { "name": "event1" }, + "event2": { "name": "event2" }, + "event3": { "name": "event3" }, + "event4": { "name": "event4" }, + }) + }) + + it('should store all the events when set asynchronously', async (done) => { + const promises = [] + promises.push(store.set('event1', {'name': 'event1'})) + promises.push(store.set('event2', {'name': 'event2'})) + promises.push(store.set('event3', {'name': 'event3'})) + promises.push(store.set('event4', {'name': 'event4'})) + Promise.all(promises).then(() => { + const storedPendingEvents = JSON.parse(AsyncStorage.dumpItems()[STORE_KEY]) + expect(storedPendingEvents).toEqual({ + "event1": { "name": "event1" }, + "event2": { "name": "event2" }, + "event3": { "name": "event3" }, + "event4": { "name": "event4" }, + }) + done() + }) + }) + }) + + describe('get', () => { + it('should correctly get items', async () => { + await store.set('event1', {'name': 'event1'}) + await store.set('event2', {'name': 'event2'}) + await store.set('event3', {'name': 'event3'}) + await store.set('event4', {'name': 'event4'}) + expect(await store.get('event1')).toEqual({'name': 'event1'}) + expect(await store.get('event2')).toEqual({'name': 'event2'}) + expect(await store.get('event3')).toEqual({'name': 'event3'}) + expect(await store.get('event4')).toEqual({'name': 'event4'}) + }) + }) + + describe('getEventsMap', () => { + it('should get the whole map correctly', async () => { + await store.set('event1', {'name': 'event1'}) + await store.set('event2', {'name': 'event2'}) + await store.set('event3', {'name': 'event3'}) + await store.set('event4', {'name': 'event4'}) + const mapResult = await store.getEventsMap() + expect(mapResult).toEqual({ + "event1": { "name": "event1" }, + "event2": { "name": "event2" }, + "event3": { "name": "event3" }, + "event4": { "name": "event4" }, + }) + }) + }) + + describe('getEventsList', () => { + it('should get all the events as a list', async () => { + await store.set('event1', {'name': 'event1'}) + await store.set('event2', {'name': 'event2'}) + await store.set('event3', {'name': 'event3'}) + await store.set('event4', {'name': 'event4'}) + const listResult = await store.getEventsList() + expect(listResult).toEqual([ + { "name": "event1" }, + { "name": "event2" }, + { "name": "event3" }, + { "name": "event4" }, + ]) + }) + }) + + describe('remove', () => { + it('should correctly remove items from the store', async () => { + await store.set('event1', {'name': 'event1'}) + await store.set('event2', {'name': 'event2'}) + await store.set('event3', {'name': 'event3'}) + await store.set('event4', {'name': 'event4'}) + let storedPendingEvents = JSON.parse(AsyncStorage.dumpItems()[STORE_KEY]) + expect(storedPendingEvents).toEqual({ + "event1": { "name": "event1" }, + "event2": { "name": "event2" }, + "event3": { "name": "event3" }, + "event4": { "name": "event4" }, + }) + + await store.remove('event1') + storedPendingEvents = JSON.parse(AsyncStorage.dumpItems()[STORE_KEY]) + expect(storedPendingEvents).toEqual({ + "event2": { "name": "event2" }, + "event3": { "name": "event3" }, + "event4": { "name": "event4" }, + }) + + await store.remove('event2') + storedPendingEvents = JSON.parse(AsyncStorage.dumpItems()[STORE_KEY]) + expect(storedPendingEvents).toEqual({ + "event3": { "name": "event3" }, + "event4": { "name": "event4" }, + }) + }) + + it('should correctly remove items from the store when removed asynchronously', async (done) => { + await store.set('event1', {'name': 'event1'}) + await store.set('event2', {'name': 'event2'}) + await store.set('event3', {'name': 'event3'}) + await store.set('event4', {'name': 'event4'}) + let storedPendingEvents = JSON.parse(AsyncStorage.dumpItems()[STORE_KEY]) + expect(storedPendingEvents).toEqual({ + "event1": { "name": "event1" }, + "event2": { "name": "event2" }, + "event3": { "name": "event3" }, + "event4": { "name": "event4" }, + }) + + const promises = [] + promises.push(store.remove('event1')) + promises.push(store.remove('event2')) + promises.push(store.remove('event3')) + Promise.all(promises).then(() => { + let storedPendingEvents = JSON.parse(AsyncStorage.dumpItems()[STORE_KEY]) + expect(storedPendingEvents).toEqual({ "event4": { "name": "event4" }}) + done() + }) + }) + }) + + describe('clear', () => { + it('should clear the whole store',async () => { + await store.set('event1', {'name': 'event1'}) + await store.set('event2', {'name': 'event2'}) + await store.set('event3', {'name': 'event3'}) + await store.set('event4', {'name': 'event4'}) + let storedPendingEvents = JSON.parse(AsyncStorage.dumpItems()[STORE_KEY]) + expect(storedPendingEvents).toEqual({ + "event1": { "name": "event1" }, + "event2": { "name": "event2" }, + "event3": { "name": "event3" }, + "event4": { "name": "event4" }, + }) + await store.clear() + storedPendingEvents = JSON.parse(AsyncStorage.dumpItems()[STORE_KEY] || '{}') + expect(storedPendingEvents).toEqual({}) + }) + }) + + describe('maxSize', () => { + it('should not add anymore events if the store if full', async () => { + await store.set('event1', {'name': 'event1'}) + await store.set('event2', {'name': 'event2'}) + await store.set('event3', {'name': 'event3'}) + await store.set('event4', {'name': 'event4'}) + + let storedPendingEvents = JSON.parse(AsyncStorage.dumpItems()[STORE_KEY]) + expect(storedPendingEvents).toEqual({ + "event1": { "name": "event1" }, + "event2": { "name": "event2" }, + "event3": { "name": "event3" }, + "event4": { "name": "event4" }, + }) + await store.set('event5', {'name': 'event5'}) + + storedPendingEvents = JSON.parse(AsyncStorage.dumpItems()[STORE_KEY]) + expect(storedPendingEvents).toEqual({ + "event1": { "name": "event1" }, + "event2": { "name": "event2" }, + "event3": { "name": "event3" }, + "event4": { "name": "event4" }, + "event5": { "name": "event5" }, + }) + + await store.set('event6', {'name': 'event6'}) + storedPendingEvents = JSON.parse(AsyncStorage.dumpItems()[STORE_KEY]) + expect(storedPendingEvents).toEqual({ + "event1": { "name": "event1" }, + "event2": { "name": "event2" }, + "event3": { "name": "event3" }, + "event4": { "name": "event4" }, + "event5": { "name": "event5" }, + }) + }) + }) +}) diff --git a/packages/optimizely-sdk/tests/requestTracker.spec.ts b/packages/optimizely-sdk/tests/requestTracker.spec.ts new file mode 100644 index 000000000..70f241500 --- /dev/null +++ b/packages/optimizely-sdk/tests/requestTracker.spec.ts @@ -0,0 +1,64 @@ +/** + * Copyright 2022, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import RequestTracker from '../lib/modules/event_processor/requestTracker' + +describe('requestTracker', () => { + describe('onRequestsComplete', () => { + it('returns an immediately-fulfilled promise when no requests are in flight', async () => { + const tracker = new RequestTracker() + await tracker.onRequestsComplete() + }) + + it('returns a promise that fulfills after in-flight requests are complete', async () => { + let resolveReq1: () => void + const req1 = new Promise(resolve => { + resolveReq1 = resolve + }) + let resolveReq2: () => void + const req2 = new Promise(resolve => { + resolveReq2 = resolve + }) + let resolveReq3: () => void + const req3 = new Promise(resolve => { + resolveReq3 = resolve + }) + + const tracker = new RequestTracker() + tracker.trackRequest(req1) + tracker.trackRequest(req2) + tracker.trackRequest(req3) + + let reqsComplete = false + const reqsCompletePromise = tracker.onRequestsComplete().then(() => { + reqsComplete = true + }) + + resolveReq1!() + await req1 + expect(reqsComplete).toBe(false) + + resolveReq2!() + await req2 + expect(reqsComplete).toBe(false) + + resolveReq3!() + await req3 + await reqsCompletePromise + expect(reqsComplete).toBe(true) + }) + }) +}) diff --git a/packages/optimizely-sdk/tests/v1EventProcessor.react_native.spec.ts b/packages/optimizely-sdk/tests/v1EventProcessor.react_native.spec.ts new file mode 100644 index 000000000..dc490d990 --- /dev/null +++ b/packages/optimizely-sdk/tests/v1EventProcessor.react_native.spec.ts @@ -0,0 +1,887 @@ +/** + * Copyright 2022, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/// +import { NotificationSender } from '../lib/core/notification_center' +import { NOTIFICATION_TYPES } from '../lib/utils/enums' + +import { LogTierV1EventProcessor } from '../lib/modules/event_processor/v1/v1EventProcessor.react_native' +import { + EventDispatcher, + EventV1Request, + EventDispatcherCallback, +} from '../lib/modules/event_processor/eventDispatcher' +import { EventProcessor, ProcessableEvent } from '../lib/modules/event_processor/eventProcessor' +import { buildImpressionEventV1, makeBatchedEventV1 } from '../lib/modules/event_processor/v1/buildEventV1' +import AsyncStorage from '../__mocks__/@react-native-async-storage/async-storage' +import { triggerInternetState } from '../__mocks__/@react-native-community/netinfo' +import { DefaultEventQueue } from '../lib/modules/event_processor/eventQueue' + +function createImpressionEvent() { + return { + type: 'impression' as 'impression', + timestamp: 69, + uuid: 'uuid', + + context: { + accountId: 'accountId', + projectId: 'projectId', + clientName: 'node-sdk', + clientVersion: '3.0.0', + revision: '1', + botFiltering: true, + anonymizeIP: true, + }, + + user: { + id: 'userId', + attributes: [{ entityId: 'attr1-id', key: 'attr1-key', value: 'attr1-value' }], + }, + + layer: { + id: 'layerId', + }, + + experiment: { + id: 'expId', + key: 'expKey', + }, + + variation: { + id: 'varId', + key: 'varKey', + }, + + ruleKey: 'expKey', + flagKey: 'flagKey1', + ruleType: 'experiment', + enabled: false, + } +} + +function createConversionEvent() { + return { + type: 'conversion' as 'conversion', + timestamp: 69, + uuid: 'uuid', + + context: { + accountId: 'accountId', + projectId: 'projectId', + clientName: 'node-sdk', + clientVersion: '3.0.0', + revision: '1', + botFiltering: true, + anonymizeIP: true, + }, + + user: { + id: 'userId', + attributes: [{ entityId: 'attr1-id', key: 'attr1-key', value: 'attr1-value' }], + }, + + event: { + id: 'event-id', + key: 'event-key', + }, + + tags: { + foo: 'bar', + value: '123', + revenue: '1000', + }, + + revenue: 1000, + value: 123, + } +} + +describe('LogTierV1EventProcessorReactNative', () => { + describe('New Events', () => { + let stubDispatcher: EventDispatcher + let dispatchStub: jest.Mock + + beforeEach(() => { + dispatchStub = jest.fn() + + stubDispatcher = { + dispatchEvent(event: EventV1Request, callback: EventDispatcherCallback): void { + dispatchStub(event) + callback({ statusCode: 200 }) + }, + } + }) + + afterEach(() => { + jest.resetAllMocks() + AsyncStorage.clearStore() + }) + + describe('stop()', () => { + let localCallback: EventDispatcherCallback + beforeEach(async () => { + stubDispatcher = { + dispatchEvent(event: EventV1Request, callback: EventDispatcherCallback): void { + dispatchStub(event) + localCallback = callback + }, + } + }) + + it('should return a resolved promise when there is nothing in queue', async () => { + const processor = new LogTierV1EventProcessor({ + dispatcher: stubDispatcher, + flushInterval: 100, + batchSize: 100, + }) + + await processor.start() + + await processor.stop() + }) + + it('should return a promise that is resolved when the dispatcher callback returns a 200 response', async (done) => { + const processor = new LogTierV1EventProcessor({ + dispatcher: stubDispatcher, + flushInterval: 100, + batchSize: 100, + }) + await processor.start() + const impressionEvent = createImpressionEvent() + processor.process(impressionEvent) + + await new Promise(resolve => setTimeout(resolve, 150)) + processor.stop().then(() => { + done() + }) + + localCallback({ statusCode: 200 }) + }) + + it('should return a promise that is resolved when the dispatcher callback returns a 400 response', async (done) => { + // This test is saying that even if the request fails to send but + // the `dispatcher` yielded control back, then the `.stop()` promise should be resolved + let localCallback: any + stubDispatcher = { + dispatchEvent(event: EventV1Request, callback: EventDispatcherCallback): void { + dispatchStub(event) + localCallback = callback + }, + } + + const processor = new LogTierV1EventProcessor({ + dispatcher: stubDispatcher, + flushInterval: 100, + batchSize: 100, + }) + await processor.start() + + const impressionEvent = createImpressionEvent() + processor.process(impressionEvent) + + await new Promise(resolve => setTimeout(resolve, 150)) + processor.stop().then(() => { + done() + }) + + localCallback({ statusCode: 400 }) + }) + + it('should return a promise when multiple event batches are sent', async () => { + stubDispatcher = { + dispatchEvent(event: EventV1Request, callback: EventDispatcherCallback): void { + dispatchStub(event) + callback({ statusCode: 200 }) + }, + } + + const processor = new LogTierV1EventProcessor({ + dispatcher: stubDispatcher, + flushInterval: 100, + batchSize: 100, + }) + + await processor.start() + + const impressionEvent1 = createImpressionEvent() + const impressionEvent2 = createImpressionEvent() + impressionEvent2.context.revision = '2' + processor.process(impressionEvent1) + processor.process(impressionEvent2) + + await new Promise(resolve => setTimeout(resolve, 150)) + await processor.stop() + expect(dispatchStub).toBeCalledTimes(2) + }) + + it('should stop accepting events after stop is called', async () => { + const dispatcher = { + dispatchEvent: jest.fn((event: EventV1Request, callback: EventDispatcherCallback) => { + setTimeout(() => callback({ statusCode: 204 }), 0) + }) + } + const processor = new LogTierV1EventProcessor({ + dispatcher, + flushInterval: 100, + batchSize: 3, + }) + await processor.start() + + const impressionEvent1 = createImpressionEvent() + processor.process(impressionEvent1) + await new Promise(resolve => setTimeout(resolve, 150)) + + await processor.stop() + // calling stop should haver flushed the current batch of size 1 + expect(dispatcher.dispatchEvent).toBeCalledTimes(1) + + dispatcher.dispatchEvent.mockClear(); + + // From now on, subsequent events should be ignored. + // Process 3 more, which ordinarily would have triggered + // a flush due to the batch size. + const impressionEvent2 = createImpressionEvent() + processor.process(impressionEvent2) + const impressionEvent3 = createImpressionEvent() + processor.process(impressionEvent3) + const impressionEvent4 = createImpressionEvent() + processor.process(impressionEvent4) + // Since we already stopped the processor, the dispatcher should + // not have been called again. + await new Promise(resolve => setTimeout(resolve, 150)) + expect(dispatcher.dispatchEvent).toBeCalledTimes(0) + }) + }) + + describe('when batchSize = 1', () => { + let processor: EventProcessor + beforeEach(async () => { + processor = new LogTierV1EventProcessor({ + dispatcher: stubDispatcher, + flushInterval: 100, + batchSize: 1, + }) + await processor.start() + }) + + afterEach(async () => { + await processor.stop() + }) + + it('should immediately flush events as they are processed', async () => { + const impressionEvent = createImpressionEvent() + processor.process(impressionEvent) + + await new Promise(resolve => setTimeout(resolve, 50)) + + expect(dispatchStub).toHaveBeenCalledTimes(1) + expect(dispatchStub).toHaveBeenCalledWith({ + url: 'https://logx.optimizely.com/v1/events', + httpVerb: 'POST', + params: buildImpressionEventV1(impressionEvent), + }) + }) + }) + + describe('when batchSize = 3, flushInterval = 300', () => { + let processor: EventProcessor + beforeEach(async () => { + processor = new LogTierV1EventProcessor({ + dispatcher: stubDispatcher, + flushInterval: 300, + batchSize: 3, + }) + await processor.start() + }) + + afterEach(async () => { + await processor.stop() + }) + + it('should wait until 3 events to be in the queue before it flushes', async () => { + const impressionEvent1 = createImpressionEvent() + const impressionEvent2 = createImpressionEvent() + const impressionEvent3 = createImpressionEvent() + + processor.process(impressionEvent1) + processor.process(impressionEvent2) + + await new Promise(resolve => setTimeout(resolve, 50)) + expect(dispatchStub).toHaveBeenCalledTimes(0) + + processor.process(impressionEvent3) + + await new Promise(resolve => setTimeout(resolve, 50)) + expect(dispatchStub).toHaveBeenCalledTimes(1) + expect(dispatchStub).toHaveBeenCalledWith({ + url: 'https://logx.optimizely.com/v1/events', + httpVerb: 'POST', + params: makeBatchedEventV1([ + impressionEvent1, + impressionEvent2, + impressionEvent3, + ]), + }) + }) + + it('should flush the current batch when it receives an event with a different context revision than the current batch', async () => { + const impressionEvent1 = createImpressionEvent() + const conversionEvent = createConversionEvent() + const impressionEvent2 = createImpressionEvent() + + // createImpressionEvent and createConversionEvent create events with revision '1' + // We modify this one's revision to '2' in order to test that the queue is flushed + // when an event with a different revision is processed. + impressionEvent2.context.revision = '2' + + processor.process(impressionEvent1) + processor.process(conversionEvent) + + await new Promise(resolve => setTimeout(resolve, 50)) + expect(dispatchStub).toHaveBeenCalledTimes(0) + + processor.process(impressionEvent2) + + await new Promise(resolve => setTimeout(resolve, 50)) + expect(dispatchStub).toHaveBeenCalledTimes(1) + expect(dispatchStub).toHaveBeenCalledWith({ + url: 'https://logx.optimizely.com/v1/events', + httpVerb: 'POST', + params: makeBatchedEventV1([impressionEvent1, conversionEvent]), + }) + }) + + it('should flush the current batch when it receives an event with a different context projectId than the current batch', async () => { + const impressionEvent1 = createImpressionEvent() + const conversionEvent = createConversionEvent() + const impressionEvent2 = createImpressionEvent() + + impressionEvent2.context.projectId = 'projectId2' + + processor.process(impressionEvent1) + processor.process(conversionEvent) + + await new Promise(resolve => setTimeout(resolve, 50)) + expect(dispatchStub).toHaveBeenCalledTimes(0) + + processor.process(impressionEvent2) + + await new Promise(resolve => setTimeout(resolve, 50)) + expect(dispatchStub).toHaveBeenCalledTimes(1) + expect(dispatchStub).toHaveBeenCalledWith({ + url: 'https://logx.optimizely.com/v1/events', + httpVerb: 'POST', + params: makeBatchedEventV1([impressionEvent1, conversionEvent]), + }) + }) + + it('should flush the queue when the flush interval happens', async () => { + const impressionEvent1 = createImpressionEvent() + + processor.process(impressionEvent1) + + expect(dispatchStub).toHaveBeenCalledTimes(0) + + await new Promise(resolve => setTimeout(resolve, 350)) + + expect(dispatchStub).toHaveBeenCalledTimes(1) + expect(dispatchStub).toHaveBeenCalledWith({ + url: 'https://logx.optimizely.com/v1/events', + httpVerb: 'POST', + params: makeBatchedEventV1([impressionEvent1]), + }) + + processor.process(createImpressionEvent()) + processor.process(createImpressionEvent()) + // flushing should reset queue, at this point only has two events + expect(dispatchStub).toHaveBeenCalledTimes(1) + }) + }) + + describe('when a notification center is provided', () => { + it('should trigger a notification when the event dispatcher dispatches an event', async () => { + const dispatcher: EventDispatcher = { + dispatchEvent: jest.fn() + } + + const notificationCenter: NotificationSender = { + sendNotifications: jest.fn() + } + + const processor = new LogTierV1EventProcessor({ + dispatcher, + notificationCenter, + batchSize: 1, + }) + await processor.start() + + const impressionEvent1 = createImpressionEvent() + processor.process(impressionEvent1) + + await new Promise(resolve => setTimeout(resolve, 150)) + expect(notificationCenter.sendNotifications).toBeCalledTimes(1) + const event = (dispatcher.dispatchEvent as jest.Mock).mock.calls[0][0] + expect(notificationCenter.sendNotifications).toBeCalledWith(NOTIFICATION_TYPES.LOG_EVENT, event) + }) + }) + + describe('invalid batchSize', () => { + it('should ignore a batchSize of 0 and use the default', async () => { + const processor = new LogTierV1EventProcessor({ + dispatcher: stubDispatcher, + flushInterval: 30000, + batchSize: 0, + }) + await processor.start() + + const impressionEvent1 = createImpressionEvent() + processor.process(impressionEvent1) + + await new Promise(resolve => setTimeout(resolve, 150)) + expect(dispatchStub).toHaveBeenCalledTimes(0) + const impressionEvents = [impressionEvent1] + for (let i = 0; i < 9; i++) { + const evt = createImpressionEvent() + processor.process(evt) + impressionEvents.push(evt) + } + + await new Promise(resolve => setTimeout(resolve, 150)) + expect(dispatchStub).toHaveBeenCalledTimes(1) + expect(dispatchStub).toHaveBeenCalledWith({ + url: 'https://logx.optimizely.com/v1/events', + httpVerb: 'POST', + params: makeBatchedEventV1(impressionEvents), + }) + }) + }) + }) + + describe('Pending Events', () => { + let stubDispatcher: EventDispatcher + let dispatchStub: jest.Mock + + beforeEach(() => { + dispatchStub = jest.fn() + }) + + afterEach(() => { + jest.clearAllMocks() + AsyncStorage.clearStore() + }) + + describe('Retry Pending Events', () => { + describe('App start', () => { + it('should dispatch all the pending events in correct order', async () => { + let receivedEvents: EventV1Request[] = [] + + stubDispatcher = { + dispatchEvent(event: EventV1Request, callback: EventDispatcherCallback): void { + dispatchStub(event) + callback({ statusCode: 400 }) + }, + } + + let processor = new LogTierV1EventProcessor({ + dispatcher: stubDispatcher, + flushInterval: 100, + batchSize: 1, + }) + + await processor.start() + let event1 = createConversionEvent() + event1.user.id = 'user1' + let event2 = createConversionEvent() + event2.user.id = 'user2' + let event3 = createConversionEvent() + event3.user.id = 'user3' + let event4 = createConversionEvent() + event4.user.id = 'user4' + + processor.process(event1) + processor.process(event2) + processor.process(event3) + processor.process(event4) + + await new Promise(resolve => setTimeout(resolve, 100)) + + expect(dispatchStub).toBeCalledTimes(4) + + await processor.stop() + + jest.clearAllMocks() + + receivedEvents = [] + stubDispatcher = { + dispatchEvent(event: EventV1Request, callback: EventDispatcherCallback): void { + receivedEvents.push(event) + dispatchStub(event) + callback({ statusCode: 200 }) + }, + } + + processor = new LogTierV1EventProcessor({ + dispatcher: stubDispatcher, + flushInterval: 100, + batchSize: 1, + }) + + await processor.start() + + receivedEvents.forEach((e, i) => { + expect(e.params.visitors[0].visitor_id).toEqual(`user${i+1}`) + }) + + expect(dispatchStub).toBeCalledTimes(4) + + await processor.stop() + }) + + it('should process all the events left in buffer when the app closed last time', async () => { + stubDispatcher = { + dispatchEvent(event: EventV1Request, callback: EventDispatcherCallback): void { + dispatchStub(event) + callback({ statusCode: 200 }) + }, + } + + let processor = new LogTierV1EventProcessor({ + dispatcher: stubDispatcher, + flushInterval: 1000, + batchSize: 4, + }) + + await processor.start() + let event1 = createConversionEvent() + event1.user.id = 'user1' + event1.uuid = 'user1' + let event2 = createConversionEvent() + event2.user.id = 'user2' + event2.uuid = 'user2' + + processor.process(event1) + processor.process(event2) + + await new Promise(resolve => setTimeout(resolve, 100)) + + // Explicitly stopping the timer to simulate app close + ;(processor.queue as DefaultEventQueue).timer.stop() + + let receivedEvents: EventV1Request[] = [] + stubDispatcher = { + dispatchEvent(event: EventV1Request, callback: EventDispatcherCallback): void { + receivedEvents.push(event) + dispatchStub(event) + callback({ statusCode: 200 }) + }, + } + + processor = new LogTierV1EventProcessor({ + dispatcher: stubDispatcher, + flushInterval: 100, + batchSize: 4, + }) + + await processor.start() + + await new Promise(resolve => setTimeout(resolve, 150)) + expect(dispatchStub).toBeCalledTimes(1) + expect(receivedEvents.length).toEqual(1) + const receivedEvent = receivedEvents[0] + + receivedEvent.params.visitors.forEach((v, i) => { + expect(v.visitor_id).toEqual(`user${i+1}`) + }) + + await processor.stop() + }) + + it('should dispatch pending events first and then process events in buffer store', async () => { + stubDispatcher = { + dispatchEvent(event: EventV1Request, callback: EventDispatcherCallback): void { + dispatchStub(event) + callback({ statusCode: 400 }) + }, + } + + let processor = new LogTierV1EventProcessor({ + dispatcher: stubDispatcher, + flushInterval: 300, + batchSize: 3, + }) + + await processor.start() + + for (let i = 0; i < 8; i++) { + let event = createConversionEvent() + event.user.id = `user${i}` + event.uuid = `user${i}` + processor.process(event) + } + + await new Promise(resolve => setTimeout(resolve, 50)) + + expect(dispatchStub).toBeCalledTimes(2) + + ;(processor.queue as DefaultEventQueue).timer.stop() + + jest.clearAllMocks() + + const visitorIds: string[] = [] + stubDispatcher = { + dispatchEvent(event: EventV1Request, callback: EventDispatcherCallback): void { + dispatchStub(event) + event.params.visitors.forEach(visitor => visitorIds.push(visitor.visitor_id)) + callback({ statusCode: 200 }) + }, + } + + processor = new LogTierV1EventProcessor({ + dispatcher: stubDispatcher, + flushInterval: 200, + batchSize: 3, + }) + + await processor.start() + + expect(dispatchStub).toBeCalledTimes(2) + + await new Promise(resolve => setTimeout(resolve, 250)) + expect(visitorIds.length).toEqual(8) + expect(visitorIds).toEqual(['user0', 'user1', 'user2', 'user3', 'user4', 'user5', 'user6', 'user7']) + }) + }) + + describe('When a new event is dispatched', () => { + it('should dispatch all the pending events first and then new event in correct order', async () => { + let receivedVisitorIds: string[] = [] + let dispatchCount = 0 + stubDispatcher = { + dispatchEvent(event: EventV1Request, callback: EventDispatcherCallback): void { + dispatchStub(event) + dispatchCount++ + if (dispatchCount > 4) { + event.params.visitors.forEach(visitor => receivedVisitorIds.push(visitor.visitor_id)) + callback({ statusCode: 200 }) + } else { + callback({ statusCode: 400 }) + } + }, + } + + let processor = new LogTierV1EventProcessor({ + dispatcher: stubDispatcher, + flushInterval: 100, + batchSize: 1, + }) + + await processor.start() + let event1 = createConversionEvent() + event1.user.id = event1.uuid = 'user1' + let event2 = createConversionEvent() + event2.user.id = event2.uuid = 'user2' + let event3 = createConversionEvent() + event3.user.id = event3.uuid = 'user3' + let event4 = createConversionEvent() + event4.user.id = event4.uuid = 'user4' + + processor.process(event1) + processor.process(event2) + processor.process(event3) + processor.process(event4) + + await new Promise(resolve => setTimeout(resolve, 100)) + + // Four events will return response code 400 which means only the first pending event will be tried each time and rest will be skipped + expect(dispatchStub).toBeCalledTimes(4) + + jest.resetAllMocks() + + let event5 = createConversionEvent() + event5.user.id = event5.uuid = 'user5' + + processor.process(event5) + + await new Promise(resolve => setTimeout(resolve, 100)) + expect(dispatchStub).toBeCalledTimes(5) + expect(receivedVisitorIds).toEqual(['user1', 'user2', 'user3', 'user4', 'user5']) + await processor.stop() + }) + + it('should skip dispatching subsequent events if an event fails to dispatch', async () => { + let receivedVisitorIds: string[] = [] + let dispatchCount = 0 + stubDispatcher = { + dispatchEvent(event: EventV1Request, callback: EventDispatcherCallback): void { + dispatchStub(event) + dispatchCount++ + event.params.visitors.forEach(visitor => receivedVisitorIds.push(visitor.visitor_id)) + callback({ statusCode: 400 }) + }, + } + + let processor = new LogTierV1EventProcessor({ + dispatcher: stubDispatcher, + flushInterval: 100, + batchSize: 1, + }) + + await processor.start() + let event1 = createConversionEvent() + event1.user.id = event1.uuid = 'user1' + let event2 = createConversionEvent() + event2.user.id = event2.uuid = 'user2' + let event3 = createConversionEvent() + event3.user.id = event3.uuid = 'user3' + let event4 = createConversionEvent() + event4.user.id = event4.uuid = 'user4' + + processor.process(event1) + await new Promise(resolve => setTimeout(resolve, 50)) + expect(dispatchStub).toBeCalledTimes(1) + + processor.process(event2) + await new Promise(resolve => setTimeout(resolve, 50)) + expect(dispatchStub).toBeCalledTimes(2) + + processor.process(event3) + await new Promise(resolve => setTimeout(resolve, 50)) + expect(dispatchStub).toBeCalledTimes(3) + + processor.process(event4) + await new Promise(resolve => setTimeout(resolve, 50)) + expect(dispatchStub).toBeCalledTimes(4) + + expect(dispatchCount).toEqual(4) + + // subsequent events were skipped with each attempt because of request failure + expect(receivedVisitorIds).toEqual(['user1', 'user1', 'user1', 'user1']) + await processor.stop() + }) + }) + + describe('When internet connection is restored', () => { + it('should dispatch all the pending events in correct order when internet connection is restored', async () => { + let receivedVisitorIds: string[] = [] + let dispatchCount = 0 + stubDispatcher = { + dispatchEvent(event: EventV1Request, callback: EventDispatcherCallback): void { + dispatchStub(event) + dispatchCount++ + if (dispatchCount > 4) { + event.params.visitors.forEach(visitor => receivedVisitorIds.push(visitor.visitor_id)) + callback({ statusCode: 200 }) + } else { + callback({ statusCode: 400 }) + } + }, + } + + let processor = new LogTierV1EventProcessor({ + dispatcher: stubDispatcher, + flushInterval: 100, + batchSize: 1, + }) + + await processor.start() + triggerInternetState(false) + let event1 = createConversionEvent() + event1.user.id = event1.uuid = 'user1' + let event2 = createConversionEvent() + event2.user.id = event2.uuid = 'user2' + let event3 = createConversionEvent() + event3.user.id = event3.uuid = 'user3' + let event4 = createConversionEvent() + event4.user.id = event4.uuid = 'user4' + + processor.process(event1) + processor.process(event2) + processor.process(event3) + processor.process(event4) + + await new Promise(resolve => setTimeout(resolve, 50)) + + // Four events will return response code 400 which means only the first pending event will be tried each time and rest will be skipped + expect(dispatchStub).toBeCalledTimes(4) + + jest.resetAllMocks() + + triggerInternetState(true) + await new Promise(resolve => setTimeout(resolve, 50)) + expect(dispatchStub).toBeCalledTimes(4) + expect(receivedVisitorIds).toEqual(['user1', 'user2', 'user3', 'user4']) + await processor.stop() + }) + + it('should not dispatch duplicate events if internet is lost and restored twice in a short interval', async () => { + let receivedVisitorIds: string[] = [] + let dispatchCount = 0 + stubDispatcher = { + dispatchEvent(event: EventV1Request, callback: EventDispatcherCallback): void { + dispatchStub(event) + dispatchCount++ + if (dispatchCount > 4) { + event.params.visitors.forEach(visitor => receivedVisitorIds.push(visitor.visitor_id)) + callback({ statusCode: 200 }) + } else { + callback({ statusCode: 400 }) + } + }, + } + + let processor = new LogTierV1EventProcessor({ + dispatcher: stubDispatcher, + flushInterval: 100, + batchSize: 1, + }) + + await processor.start() + triggerInternetState(false) + let event1 = createConversionEvent() + event1.user.id = event1.uuid = 'user1' + let event2 = createConversionEvent() + event2.user.id = event2.uuid = 'user2' + let event3 = createConversionEvent() + event3.user.id = event3.uuid = 'user3' + let event4 = createConversionEvent() + event4.user.id = event4.uuid = 'user4' + + processor.process(event1) + processor.process(event2) + processor.process(event3) + processor.process(event4) + + await new Promise(resolve => setTimeout(resolve, 100)) + + // Four events will return response code 400 which means only the first pending event will be tried each time and rest will be skipped + expect(dispatchStub).toBeCalledTimes(4) + + jest.resetAllMocks() + + triggerInternetState(true) + triggerInternetState(false) + triggerInternetState(true) + triggerInternetState(false) + triggerInternetState(true) + + await new Promise(resolve => setTimeout(resolve, 100)) + expect(dispatchStub).toBeCalledTimes(4) + expect(receivedVisitorIds).toEqual(['user1', 'user2', 'user3', 'user4']) + await processor.stop() + }) + }) + }) + }) +}) diff --git a/packages/optimizely-sdk/tests/v1EventProcessor.spec.ts b/packages/optimizely-sdk/tests/v1EventProcessor.spec.ts new file mode 100644 index 000000000..59f6cdb79 --- /dev/null +++ b/packages/optimizely-sdk/tests/v1EventProcessor.spec.ts @@ -0,0 +1,530 @@ +/** + * Copyright 2022, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +/// + +import { LogTierV1EventProcessor } from '../lib/modules/event_processor/v1/v1EventProcessor' +import { + EventDispatcher, + EventV1Request, + EventDispatcherCallback, +} from '../lib/modules/event_processor/eventDispatcher' +import { EventProcessor } from '../lib/modules/event_processor/eventProcessor' +import { buildImpressionEventV1, makeBatchedEventV1 } from '../lib/modules/event_processor/v1/buildEventV1' +import { NotificationCenter, NotificationSender } from '../lib/core/notification_center' +import { NOTIFICATION_TYPES } from '../lib/utils/enums' + +function createImpressionEvent() { + return { + type: 'impression' as 'impression', + timestamp: 69, + uuid: 'uuid', + + context: { + accountId: 'accountId', + projectId: 'projectId', + clientName: 'node-sdk', + clientVersion: '3.0.0', + revision: '1', + botFiltering: true, + anonymizeIP: true, + }, + + user: { + id: 'userId', + attributes: [{ entityId: 'attr1-id', key: 'attr1-key', value: 'attr1-value' }], + }, + + layer: { + id: 'layerId', + }, + + experiment: { + id: 'expId', + key: 'expKey', + }, + + variation: { + id: 'varId', + key: 'varKey', + }, + + ruleKey: 'expKey', + flagKey: 'flagKey1', + ruleType: 'experiment', + enabled: true, + } +} + +function createConversionEvent() { + return { + type: 'conversion' as 'conversion', + timestamp: 69, + uuid: 'uuid', + + context: { + accountId: 'accountId', + projectId: 'projectId', + clientName: 'node-sdk', + clientVersion: '3.0.0', + revision: '1', + botFiltering: true, + anonymizeIP: true, + }, + + user: { + id: 'userId', + attributes: [{ entityId: 'attr1-id', key: 'attr1-key', value: 'attr1-value' }], + }, + + event: { + id: 'event-id', + key: 'event-key', + }, + + tags: { + foo: 'bar', + value: '123', + revenue: '1000', + }, + + revenue: 1000, + value: 123, + } +} + +describe('LogTierV1EventProcessor', () => { + let stubDispatcher: EventDispatcher + let dispatchStub: jest.Mock + // TODO change this to ProjectConfig when js-sdk-models is available + let testProjectConfig: any + + beforeEach(() => { + jest.useFakeTimers() + + testProjectConfig = {} + dispatchStub = jest.fn() + + stubDispatcher = { + dispatchEvent(event: EventV1Request, callback: EventDispatcherCallback): void { + dispatchStub(event) + callback({ statusCode: 200 }) + }, + } + }) + + afterEach(() => { + jest.resetAllMocks() + }) + + describe('stop()', () => { + let localCallback: EventDispatcherCallback + beforeEach(() => { + stubDispatcher = { + dispatchEvent(event: EventV1Request, callback: EventDispatcherCallback): void { + dispatchStub(event) + localCallback = callback + }, + } + }) + + it('should return a resolved promise when there is nothing in queue', done => { + const processor = new LogTierV1EventProcessor({ + dispatcher: stubDispatcher, + flushInterval: 100, + batchSize: 100, + }) + + processor.stop().then(() => { + done() + }) + }) + + it('should return a promise that is resolved when the dispatcher callback returns a 200 response', done => { + const processor = new LogTierV1EventProcessor({ + dispatcher: stubDispatcher, + flushInterval: 100, + batchSize: 100, + }) + processor.start() + + const impressionEvent = createImpressionEvent() + processor.process(impressionEvent) + + processor.stop().then(() => { + done() + }) + + localCallback({ statusCode: 200 }) + }) + + it('should return a promise that is resolved when the dispatcher callback returns a 400 response', done => { + // This test is saying that even if the request fails to send but + // the `dispatcher` yielded control back, then the `.stop()` promise should be resolved + let localCallback: any + stubDispatcher = { + dispatchEvent(event: EventV1Request, callback: EventDispatcherCallback): void { + dispatchStub(event) + localCallback = callback + }, + } + + const processor = new LogTierV1EventProcessor({ + dispatcher: stubDispatcher, + flushInterval: 100, + batchSize: 100, + }) + processor.start() + + const impressionEvent = createImpressionEvent() + processor.process(impressionEvent) + + processor.stop().then(() => { + done() + }) + + localCallback({ + statusCode: 400, + }) + }) + + it('should return a promise when multiple event batches are sent', done => { + stubDispatcher = { + dispatchEvent(event: EventV1Request, callback: EventDispatcherCallback): void { + dispatchStub(event) + callback({ statusCode: 200 }) + }, + } + + const processor = new LogTierV1EventProcessor({ + dispatcher: stubDispatcher, + flushInterval: 100, + batchSize: 100, + }) + processor.start() + + const impressionEvent1 = createImpressionEvent() + const impressionEvent2 = createImpressionEvent() + impressionEvent2.context.revision = '2' + processor.process(impressionEvent1) + processor.process(impressionEvent2) + + processor.stop().then(() => { + expect(dispatchStub).toBeCalledTimes(2) + done() + }) + }) + + it('should stop accepting events after stop is called', () => { + const dispatcher = { + dispatchEvent: jest.fn((event: EventV1Request, callback: EventDispatcherCallback) => { + setTimeout(() => callback({ statusCode: 204 }), 0) + }) + } + const processor = new LogTierV1EventProcessor({ + dispatcher, + flushInterval: 100, + batchSize: 3, + }) + processor.start() + + const impressionEvent1 = createImpressionEvent() + processor.process(impressionEvent1) + processor.stop() + // calling stop should haver flushed the current batch of size 1 + expect(dispatcher.dispatchEvent).toBeCalledTimes(1) + + dispatcher.dispatchEvent.mockClear(); + + // From now on, subsequent events should be ignored. + // Process 3 more, which ordinarily would have triggered + // a flush due to the batch size. + const impressionEvent2 = createImpressionEvent() + processor.process(impressionEvent2) + const impressionEvent3 = createImpressionEvent() + processor.process(impressionEvent3) + const impressionEvent4 = createImpressionEvent() + processor.process(impressionEvent4) + // Since we already stopped the processor, the dispatcher should + // not have been called again. + expect(dispatcher.dispatchEvent).toBeCalledTimes(0) + }) + + it('should resolve the stop promise after all dispatcher requests are done', async () => { + const dispatchCbs: Array = [] + const dispatcher = { + dispatchEvent: jest.fn((event: EventV1Request, callback: EventDispatcherCallback) => { + dispatchCbs.push(callback) + }) + } + + const processor = new LogTierV1EventProcessor({ + dispatcher, + flushInterval: 100, + batchSize: 2, + }) + processor.start() + + for (let i = 0; i < 4; i++) { + processor.process(createImpressionEvent()) + } + expect(dispatchCbs.length).toBe(2) + + let stopPromiseResolved = false + const stopPromise = processor.stop().then(() => { + stopPromiseResolved = true + }) + expect(stopPromiseResolved).toBe(false) + + dispatchCbs[0]({ statusCode: 204 }) + jest.advanceTimersByTime(100) + expect(stopPromiseResolved).toBe(false) + dispatchCbs[1]({ statusCode: 204 }) + await stopPromise + expect(stopPromiseResolved).toBe(true) + }) + }) + + describe('when batchSize = 1', () => { + let processor: EventProcessor + beforeEach(() => { + processor = new LogTierV1EventProcessor({ + dispatcher: stubDispatcher, + flushInterval: 100, + batchSize: 1, + }) + processor.start() + }) + + afterEach(() => { + processor.stop() + }) + + it('should immediately flush events as they are processed', () => { + const impressionEvent = createImpressionEvent() + processor.process(impressionEvent) + + expect(dispatchStub).toHaveBeenCalledTimes(1) + expect(dispatchStub).toHaveBeenCalledWith({ + url: 'https://logx.optimizely.com/v1/events', + httpVerb: 'POST', + params: buildImpressionEventV1(impressionEvent), + }) + }) + }) + + describe('when batchSize = 3, flushInterval = 100', () => { + let processor: EventProcessor + beforeEach(() => { + processor = new LogTierV1EventProcessor({ + dispatcher: stubDispatcher, + flushInterval: 100, + batchSize: 3, + }) + processor.start() + }) + + afterEach(() => { + processor.stop() + }) + + it('should wait until 3 events to be in the queue before it flushes', () => { + const impressionEvent1 = createImpressionEvent() + const impressionEvent2 = createImpressionEvent() + const impressionEvent3 = createImpressionEvent() + + processor.process(impressionEvent1) + processor.process(impressionEvent2) + + expect(dispatchStub).toHaveBeenCalledTimes(0) + + processor.process(impressionEvent3) + + expect(dispatchStub).toHaveBeenCalledTimes(1) + expect(dispatchStub).toHaveBeenCalledWith({ + url: 'https://logx.optimizely.com/v1/events', + httpVerb: 'POST', + params: makeBatchedEventV1([ + impressionEvent1, + impressionEvent2, + impressionEvent3, + ]), + }) + }) + + it('should flush the current batch when it receives an event with a different context revision than the current batch', async () => { + const impressionEvent1 = createImpressionEvent() + const conversionEvent = createConversionEvent() + const impressionEvent2 = createImpressionEvent() + + // createImpressionEvent and createConversionEvent create events with revision '1' + // We modify this one's revision to '2' in order to test that the queue is flushed + // when an event with a different revision is processed. + impressionEvent2.context.revision = '2' + + processor.process(impressionEvent1) + processor.process(conversionEvent) + + expect(dispatchStub).toHaveBeenCalledTimes(0) + + processor.process(impressionEvent2) + + expect(dispatchStub).toHaveBeenCalledTimes(1) + expect(dispatchStub).toHaveBeenCalledWith({ + url: 'https://logx.optimizely.com/v1/events', + httpVerb: 'POST', + params: makeBatchedEventV1([impressionEvent1, conversionEvent]), + }) + + await processor.stop() + + expect(dispatchStub).toHaveBeenCalledTimes(2) + + expect(dispatchStub).toHaveBeenCalledWith({ + url: 'https://logx.optimizely.com/v1/events', + httpVerb: 'POST', + params: makeBatchedEventV1([impressionEvent2]), + }) + }) + + it('should flush the current batch when it receives an event with a different context projectId than the current batch', async () => { + const impressionEvent1 = createImpressionEvent() + const conversionEvent = createConversionEvent() + const impressionEvent2 = createImpressionEvent() + + impressionEvent2.context.projectId = 'projectId2' + + processor.process(impressionEvent1) + processor.process(conversionEvent) + + expect(dispatchStub).toHaveBeenCalledTimes(0) + + processor.process(impressionEvent2) + + expect(dispatchStub).toHaveBeenCalledTimes(1) + expect(dispatchStub).toHaveBeenCalledWith({ + url: 'https://logx.optimizely.com/v1/events', + httpVerb: 'POST', + params: makeBatchedEventV1([impressionEvent1, conversionEvent]), + }) + + await processor.stop() + + expect(dispatchStub).toHaveBeenCalledTimes(2) + + expect(dispatchStub).toHaveBeenCalledWith({ + url: 'https://logx.optimizely.com/v1/events', + httpVerb: 'POST', + params: makeBatchedEventV1([impressionEvent2]), + }) + }) + + it('should flush the queue when the flush interval happens', () => { + const impressionEvent1 = createImpressionEvent() + + processor.process(impressionEvent1) + + expect(dispatchStub).toHaveBeenCalledTimes(0) + + jest.advanceTimersByTime(100) + + expect(dispatchStub).toHaveBeenCalledTimes(1) + expect(dispatchStub).toHaveBeenCalledWith({ + url: 'https://logx.optimizely.com/v1/events', + httpVerb: 'POST', + params: makeBatchedEventV1([impressionEvent1]), + }) + + processor.process(createImpressionEvent()) + processor.process(createImpressionEvent()) + // flushing should reset queue, at this point only has two events + expect(dispatchStub).toHaveBeenCalledTimes(1) + }) + + }) + + describe('when a notification center is provided', () => { + it('should trigger a notification when the event dispatcher dispatches an event', () => { + const dispatcher: EventDispatcher = { + dispatchEvent: jest.fn() + } + + const notificationCenter: NotificationSender = { + sendNotifications: jest.fn() + } + + const processor = new LogTierV1EventProcessor({ + dispatcher, + notificationCenter, + batchSize: 1, + }) + processor.start() + + const impressionEvent1 = createImpressionEvent() + processor.process(impressionEvent1) + + expect(notificationCenter.sendNotifications).toBeCalledTimes(1) + const event = (dispatcher.dispatchEvent as jest.Mock).mock.calls[0][0] + expect(notificationCenter.sendNotifications).toBeCalledWith(NOTIFICATION_TYPES.LOG_EVENT, event) + }) + }) + + describe('invalid flushInterval or batchSize', () => { + it('should ignore a flushInterval of 0 and use the default', () => { + const processor = new LogTierV1EventProcessor({ + dispatcher: stubDispatcher, + flushInterval: 0, + batchSize: 10, + }) + processor.start() + + const impressionEvent1 = createImpressionEvent() + processor.process(impressionEvent1) + expect(dispatchStub).toHaveBeenCalledTimes(0) + jest.advanceTimersByTime(30000) + expect(dispatchStub).toHaveBeenCalledTimes(1) + expect(dispatchStub).toHaveBeenCalledWith({ + url: 'https://logx.optimizely.com/v1/events', + httpVerb: 'POST', + params: makeBatchedEventV1([impressionEvent1]), + }) + }) + + it('should ignore a batchSize of 0 and use the default', () => { + const processor = new LogTierV1EventProcessor({ + dispatcher: stubDispatcher, + flushInterval: 30000, + batchSize: 0, + }) + processor.start() + + const impressionEvent1 = createImpressionEvent() + processor.process(impressionEvent1) + expect(dispatchStub).toHaveBeenCalledTimes(0) + const impressionEvents = [impressionEvent1] + for (let i = 0; i < 9; i++) { + const evt = createImpressionEvent() + processor.process(evt) + impressionEvents.push(evt) + } + expect(dispatchStub).toHaveBeenCalledTimes(1) + expect(dispatchStub).toHaveBeenCalledWith({ + url: 'https://logx.optimizely.com/v1/events', + httpVerb: 'POST', + params: makeBatchedEventV1(impressionEvents), + }) + }) + }) +}) diff --git a/packages/optimizely-sdk/tsconfig.json b/packages/optimizely-sdk/tsconfig.json index 4225427c1..51dfbda08 100644 --- a/packages/optimizely-sdk/tsconfig.json +++ b/packages/optimizely-sdk/tsconfig.json @@ -7,6 +7,7 @@ "./typings/*" ], }, + "resolveJsonModule": true, "allowJs": true, "declaration": true, "module": "esnext", @@ -24,6 +25,8 @@ ], "include": [ "./lib/**/*.ts", - "./lib/**/*.js" + "./lib/**/*.js", + "./lib/modules/**/*.ts", + "./lib/modules/**/**/*.ts" ] }