diff --git a/.github/workflows/pr-build.yml b/.github/workflows/pr-build.yml index c52a1dcf..f7a7d669 100644 --- a/.github/workflows/pr-build.yml +++ b/.github/workflows/pr-build.yml @@ -44,6 +44,7 @@ jobs: if: '!matrix.coverage' run: | npx lerna bootstrap --hoist + npx lerna run compile npx lerna run test shell: bash env: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4a6e4245..88e97eb0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -31,6 +31,7 @@ jobs: - name: Execute tests with Lerna run: | npx lerna bootstrap --hoist + npx lerna run compile npx lerna run test - name: Publish package to npm diff --git a/.gitignore b/.gitignore index 4ffe12ad..e351405e 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ docs .idea/ .nyc_output/ *.lcov +dist diff --git a/.nycrc.json b/.nycrc.json index 27a3d3bc..0cbcf40d 100644 --- a/.nycrc.json +++ b/.nycrc.json @@ -1,12 +1,14 @@ { "all": true, + "include": [ + "**/dist/lib/**/*.js" + ], "exclude": [ - "**/Gruntfile.js", - "**/.prettierrc.js", "**/*.d.ts", "**/sample/**", "**/test/**", "**/test-d/**", - "**/test_async/**" + "**/test_async/**", + "**/docs/**" ] } diff --git a/README.md b/README.md index f1f64b59..6bb2fda7 100644 --- a/README.md +++ b/README.md @@ -89,14 +89,18 @@ AWS will not: ## Testing from Source -This repo uses [Lerna](https://lernajs.io) to manage multiple packages. To install Lerna: +This repo uses [Lerna](https://lernajs.io) to manage multiple packages. To install Lerna as a CLI: ``` -npm install lerna +npm install -g lerna ``` To install devDependencies and peerDependencies for all packages: ``` lerna bootstrap --hoist ``` +This repo has a combination of TypeScript and JavaScript source files. To transpile the TypeScript files for testing, run: +``` +lerna run compile +``` To run tests for all packages: ``` lerna run test diff --git a/package-lock.json b/package-lock.json index f13d894a..75883b32 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,6 +4,142 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "@aws-sdk/config-resolver": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/config-resolver/-/config-resolver-3.12.0.tgz", + "integrity": "sha512-xx4LcuJqgrT3fZ1FY45nIDCTzAh/pWfLM4Bh5rb1V8mT/ROAuSdG+NHYOVSOUOt7RN6X+cbvhZmztya3LxqL8g==", + "dev": true, + "requires": { + "@aws-sdk/signature-v4": "3.12.0", + "@aws-sdk/types": "3.12.0", + "tslib": "^1.8.0" + } + }, + "@aws-sdk/is-array-buffer": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/is-array-buffer/-/is-array-buffer-3.12.0.tgz", + "integrity": "sha512-JtrxC2ZinhiL2GIfMoPYkmd7A5ykpYw4Bf4/uMHJ9d3NcFpsT84ipw4eZhclR+mSR9RUYSP0ObgcDLdjW3xm1w==", + "dev": true, + "requires": { + "tslib": "^1.8.0" + } + }, + "@aws-sdk/middleware-stack": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-stack/-/middleware-stack-3.6.1.tgz", + "integrity": "sha512-EPsIxMi8LtCt7YwTFpWGlVGYJc0q4kwFbOssY02qfqdCnyqi2y5wo089dH7OdxUooQ0D7CPsXM1zTTuzvm+9Fw==", + "dev": true, + "requires": { + "tslib": "^1.8.0" + } + }, + "@aws-sdk/node-config-provider": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/node-config-provider/-/node-config-provider-3.12.0.tgz", + "integrity": "sha512-eJAjQ5PN+cwd0AC4QOUjOjrmCAASkCmovDsNndjWmFjNumJkcUvTezAjOC6TLiEop9M1cT0zkhPBEDGjzDnjZQ==", + "dev": true, + "requires": { + "@aws-sdk/property-provider": "3.12.0", + "@aws-sdk/shared-ini-file-loader": "3.12.0", + "@aws-sdk/types": "3.12.0", + "tslib": "^1.8.0" + } + }, + "@aws-sdk/property-provider": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/property-provider/-/property-provider-3.12.0.tgz", + "integrity": "sha512-4x9S0mtpehp++g+KWx12ZnYa396qCxJXB/n/njppXlWjUz7am527IN24YVTpFoP2CpNo4uZb9Xi8fW6veZSTJg==", + "dev": true, + "requires": { + "@aws-sdk/types": "3.12.0", + "tslib": "^1.8.0" + } + }, + "@aws-sdk/service-error-classification": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/service-error-classification/-/service-error-classification-3.15.0.tgz", + "integrity": "sha512-ay7l8AgtGM3NvvUhsW8cKmTcDX460fGZZLUXUEbdtO274RzHLU3jKSLj0jg74b2212ZFRIMwh2OQHiy8brLu8Q==" + }, + "@aws-sdk/shared-ini-file-loader": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/shared-ini-file-loader/-/shared-ini-file-loader-3.12.0.tgz", + "integrity": "sha512-vmd0gIZ0bc5hgyEDYufKfMsDKIPHV1ZXc8UzICV3BAsVf1eXhY8j+19OcxqlB+jWtLnnd2L28XslfRCYK9gduw==", + "dev": true, + "requires": { + "tslib": "^1.8.0" + } + }, + "@aws-sdk/signature-v4": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4/-/signature-v4-3.12.0.tgz", + "integrity": "sha512-11ZwHj8GjzsQNmebAaxhFcqSOgK6+5fUcrUDRu+R9HMvdKIuiLpawqCZELupbg4uqhka953rAldjbHjYhUaLuw==", + "dev": true, + "requires": { + "@aws-sdk/is-array-buffer": "3.12.0", + "@aws-sdk/types": "3.12.0", + "@aws-sdk/util-hex-encoding": "3.12.0", + "@aws-sdk/util-uri-escape": "3.12.0", + "tslib": "^1.8.0" + } + }, + "@aws-sdk/smithy-client": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/smithy-client/-/smithy-client-3.15.0.tgz", + "integrity": "sha512-i25agLm/8+pSSTVGFxSKiCNVN9EqT2UuwKtxHk2qOBQAVUhn7Rd6BLbmaGRM1yOvTIEAv4DW5hYJsxyLWg+3vw==", + "dev": true, + "requires": { + "@aws-sdk/middleware-stack": "3.15.0", + "@aws-sdk/types": "3.15.0", + "tslib": "^2.0.0" + }, + "dependencies": { + "@aws-sdk/middleware-stack": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-stack/-/middleware-stack-3.15.0.tgz", + "integrity": "sha512-UjTI3K2TcfK3CpLKc7lp4/Bh0UzKR86vb7smLwFGt/3r/VvLxrN7XyU69+BFOk9vOGFro0H2LbGT7355aTQlfw==", + "dev": true, + "requires": { + "tslib": "^2.0.0" + } + }, + "@aws-sdk/types": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.15.0.tgz", + "integrity": "sha512-hRfp40av3/dwxUH7KAgfLL3D02ohc7NYs/R0xFgYWEpAieB9/BHKuJPFGOSetqWw+Z6NOLiKKva8+/CcVOqzJQ==", + "dev": true + }, + "tslib": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz", + "integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==", + "dev": true + } + } + }, + "@aws-sdk/types": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.12.0.tgz", + "integrity": "sha512-7vnVBV0IdNQ+yyCQFkyLkRohvr7PHj//nGcth9RXG+VmQfp4+8CgBlMuXoeEWvDntrRgdh5lzDO0CliVRquxkw==", + "dev": true + }, + "@aws-sdk/util-hex-encoding": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-hex-encoding/-/util-hex-encoding-3.12.0.tgz", + "integrity": "sha512-hXzhCmPU8Q2U8QkSmMtPhT1sUtXbeFrEtFPyTbWr9p7AccWM3cOCZilOcUtV04cx9RgKNyhY/O1NOdByvSY1lQ==", + "dev": true, + "requires": { + "tslib": "^1.8.0" + } + }, + "@aws-sdk/util-uri-escape": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-uri-escape/-/util-uri-escape-3.12.0.tgz", + "integrity": "sha512-ZkiGtqsE+Krr4ARweq/AV7llrEqLDMR3/R9gvwDcurYSBt1V1hNGTdGNUCSKeKmmeMxneAZXmp+xM3FYZoIjIw==", + "dev": true, + "requires": { + "tslib": "^1.8.0" + } + }, "@babel/code-frame": { "version": "7.12.11", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", @@ -14,30 +150,31 @@ } }, "@babel/compat-data": { - "version": "7.13.15", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.13.15.tgz", - "integrity": "sha512-ltnibHKR1VnrU4ymHyQ/CXtNXI6yZC0oJThyW78Hft8XndANwi+9H+UIklBDraIjFEJzw8wmcM427oDd9KS5wA==", + "version": "7.13.8", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.13.8.tgz", + "integrity": "sha512-EaI33z19T4qN3xLXsGf48M2cDqa6ei9tPZlfLdb2HC+e/cFtREiRd8hdSqDbwdLB0/+gLwqJmCYASH0z2bUdog==", "dev": true }, "@babel/core": { - "version": "7.13.15", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.13.15.tgz", - "integrity": "sha512-6GXmNYeNjS2Uz+uls5jalOemgIhnTMeaXo+yBUA72kC2uX/8VW6XyhVIo2L8/q0goKQA3EVKx0KOQpVKSeWadQ==", + "version": "7.13.8", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.13.8.tgz", + "integrity": "sha512-oYapIySGw1zGhEFRd6lzWNLWFX2s5dA/jm+Pw/+59ZdXtjyIuwlXbrId22Md0rgZVop+aVoqow2riXhBLNyuQg==", "dev": true, "requires": { "@babel/code-frame": "^7.12.13", - "@babel/generator": "^7.13.9", - "@babel/helper-compilation-targets": "^7.13.13", - "@babel/helper-module-transforms": "^7.13.14", - "@babel/helpers": "^7.13.10", - "@babel/parser": "^7.13.15", + "@babel/generator": "^7.13.0", + "@babel/helper-compilation-targets": "^7.13.8", + "@babel/helper-module-transforms": "^7.13.0", + "@babel/helpers": "^7.13.0", + "@babel/parser": "^7.13.4", "@babel/template": "^7.12.13", - "@babel/traverse": "^7.13.15", - "@babel/types": "^7.13.14", + "@babel/traverse": "^7.13.0", + "@babel/types": "^7.13.0", "convert-source-map": "^1.7.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.1.2", + "lodash": "^4.17.19", "semver": "^6.3.0", "source-map": "^0.5.0" }, @@ -52,9 +189,9 @@ } }, "@babel/parser": { - "version": "7.13.15", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.13.15.tgz", - "integrity": "sha512-b9COtcAlVEQljy/9fbcMHpG+UIW9ReF+gpaxDHTlZd0c6/UU9ng8zdySAW9sRTzpvcdCHn6bUcbuYUgGzLAWVQ==", + "version": "7.13.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.13.9.tgz", + "integrity": "sha512-nEUfRiARCcaVo3ny3ZQjURjHQZUo/JkEw7rLlSZy/psWGnvwXFtPcr6jb7Yb41DVW5LTe6KRq9LGleRNsg1Frw==", "dev": true }, "json5": { @@ -86,12 +223,12 @@ } }, "@babel/helper-compilation-targets": { - "version": "7.13.13", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.13.13.tgz", - "integrity": "sha512-q1kcdHNZehBwD9jYPh3WyXcsFERi39X4I59I3NadciWtNDyZ6x+GboOxncFK0kXlKIv6BJm5acncehXWUjWQMQ==", + "version": "7.13.8", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.13.8.tgz", + "integrity": "sha512-pBljUGC1y3xKLn1nrx2eAhurLMA8OqBtBP/JwG4U8skN7kf8/aqwwxpV1N6T0e7r6+7uNitIa/fUxPFagSXp3A==", "dev": true, "requires": { - "@babel/compat-data": "^7.13.12", + "@babel/compat-data": "^7.13.8", "@babel/helper-validator-option": "^7.12.17", "browserslist": "^4.14.5", "semver": "^6.3.0" @@ -126,37 +263,38 @@ } }, "@babel/helper-member-expression-to-functions": { - "version": "7.13.12", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.13.12.tgz", - "integrity": "sha512-48ql1CLL59aKbU94Y88Xgb2VFy7a95ykGRbJJaaVv+LX5U8wFpLfiGXJJGUozsmA1oEh/o5Bp60Voq7ACyA/Sw==", + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.13.0.tgz", + "integrity": "sha512-yvRf8Ivk62JwisqV1rFRMxiSMDGnN6KH1/mDMmIrij4jztpQNRoHqqMG3U6apYbGRPJpgPalhva9Yd06HlUxJQ==", "dev": true, "requires": { - "@babel/types": "^7.13.12" + "@babel/types": "^7.13.0" } }, "@babel/helper-module-imports": { - "version": "7.13.12", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.13.12.tgz", - "integrity": "sha512-4cVvR2/1B693IuOvSI20xqqa/+bl7lqAMR59R4iu39R9aOX8/JoYY1sFaNvUMyMBGnHdwvJgUrzNLoUZxXypxA==", + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.12.13.tgz", + "integrity": "sha512-NGmfvRp9Rqxy0uHSSVP+SRIW1q31a7Ji10cLBcqSDUngGentY4FRiHOFZFE1CLU5eiL0oE8reH7Tg1y99TDM/g==", "dev": true, "requires": { - "@babel/types": "^7.13.12" + "@babel/types": "^7.12.13" } }, "@babel/helper-module-transforms": { - "version": "7.13.14", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.13.14.tgz", - "integrity": "sha512-QuU/OJ0iAOSIatyVZmfqB0lbkVP0kDRiKj34xy+QNsnVZi/PA6BoSoreeqnxxa9EHFAIL0R9XOaAR/G9WlIy5g==", + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.13.0.tgz", + "integrity": "sha512-Ls8/VBwH577+pw7Ku1QkUWIyRRNHpYlts7+qSqBBFCW3I8QteB9DxfcZ5YJpOwH6Ihe/wn8ch7fMGOP1OhEIvw==", "dev": true, "requires": { - "@babel/helper-module-imports": "^7.13.12", - "@babel/helper-replace-supers": "^7.13.12", - "@babel/helper-simple-access": "^7.13.12", + "@babel/helper-module-imports": "^7.12.13", + "@babel/helper-replace-supers": "^7.13.0", + "@babel/helper-simple-access": "^7.12.13", "@babel/helper-split-export-declaration": "^7.12.13", "@babel/helper-validator-identifier": "^7.12.11", "@babel/template": "^7.12.13", - "@babel/traverse": "^7.13.13", - "@babel/types": "^7.13.14" + "@babel/traverse": "^7.13.0", + "@babel/types": "^7.13.0", + "lodash": "^4.17.19" } }, "@babel/helper-optimise-call-expression": { @@ -169,24 +307,24 @@ } }, "@babel/helper-replace-supers": { - "version": "7.13.12", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.13.12.tgz", - "integrity": "sha512-Gz1eiX+4yDO8mT+heB94aLVNCL+rbuT2xy4YfyNqu8F+OI6vMvJK891qGBTqL9Uc8wxEvRW92Id6G7sDen3fFw==", + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.13.0.tgz", + "integrity": "sha512-Segd5me1+Pz+rmN/NFBOplMbZG3SqRJOBlY+mA0SxAv6rjj7zJqr1AVr3SfzUVTLCv7ZLU5FycOM/SBGuLPbZw==", "dev": true, "requires": { - "@babel/helper-member-expression-to-functions": "^7.13.12", + "@babel/helper-member-expression-to-functions": "^7.13.0", "@babel/helper-optimise-call-expression": "^7.12.13", "@babel/traverse": "^7.13.0", - "@babel/types": "^7.13.12" + "@babel/types": "^7.13.0" } }, "@babel/helper-simple-access": { - "version": "7.13.12", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.13.12.tgz", - "integrity": "sha512-7FEjbrx5SL9cWvXioDbnlYTppcZGuCY6ow3/D5vMggb2Ywgu4dMrpTJX0JdQAIcRRUElOIxF3yEooa9gUb9ZbA==", + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.12.13.tgz", + "integrity": "sha512-0ski5dyYIHEfwpWGx5GPWhH35j342JaflmCeQmsPWcrOQDtCN6C1zKAVRFVbK53lPW2c9TsuLLSUDf0tIGJ5hA==", "dev": true, "requires": { - "@babel/types": "^7.13.12" + "@babel/types": "^7.12.13" } }, "@babel/helper-split-export-declaration": { @@ -211,9 +349,9 @@ "dev": true }, "@babel/helpers": { - "version": "7.13.10", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.13.10.tgz", - "integrity": "sha512-4VO883+MWPDUVRF3PhiLBUFHoX/bsLTGFpFK/HqvvfBZz2D57u9XzPVNFVBTc0PW/CWR9BXTOKt8NF4DInUHcQ==", + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.13.0.tgz", + "integrity": "sha512-aan1MeFPxFacZeSz6Ld7YZo5aPuqnKlD7+HZY75xQsueczFccP9A7V05+oe0XpLwHK3oLorPe9eaAUljL7WEaQ==", "dev": true, "requires": { "@babel/template": "^7.12.13", @@ -274,19 +412,20 @@ } }, "@babel/traverse": { - "version": "7.13.15", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.13.15.tgz", - "integrity": "sha512-/mpZMNvj6bce59Qzl09fHEs8Bt8NnpEDQYleHUPZQ3wXUMvXi+HJPLars68oAbmp839fGoOkv2pSL2z9ajCIaQ==", + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.13.0.tgz", + "integrity": "sha512-xys5xi5JEhzC3RzEmSGrs/b3pJW/o87SypZ+G/PhaE7uqVQNv/jlmVIBXuoh5atqQ434LfXV+sf23Oxj0bchJQ==", "dev": true, "requires": { "@babel/code-frame": "^7.12.13", - "@babel/generator": "^7.13.9", + "@babel/generator": "^7.13.0", "@babel/helper-function-name": "^7.12.13", "@babel/helper-split-export-declaration": "^7.12.13", - "@babel/parser": "^7.13.15", - "@babel/types": "^7.13.14", + "@babel/parser": "^7.13.0", + "@babel/types": "^7.13.0", "debug": "^4.1.0", - "globals": "^11.1.0" + "globals": "^11.1.0", + "lodash": "^4.17.19" }, "dependencies": { "@babel/code-frame": { @@ -299,9 +438,9 @@ } }, "@babel/parser": { - "version": "7.13.15", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.13.15.tgz", - "integrity": "sha512-b9COtcAlVEQljy/9fbcMHpG+UIW9ReF+gpaxDHTlZd0c6/UU9ng8zdySAW9sRTzpvcdCHn6bUcbuYUgGzLAWVQ==", + "version": "7.13.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.13.9.tgz", + "integrity": "sha512-nEUfRiARCcaVo3ny3ZQjURjHQZUo/JkEw7rLlSZy/psWGnvwXFtPcr6jb7Yb41DVW5LTe6KRq9LGleRNsg1Frw==", "dev": true }, "globals": { @@ -313,9 +452,9 @@ } }, "@babel/types": { - "version": "7.13.14", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.13.14.tgz", - "integrity": "sha512-A2aa3QTkWoyqsZZFl56MLUsfmh7O0gN41IPvXAE/++8ojpbz12SszD7JEGYVdn4f9Kt4amIei07swF1h4AqmmQ==", + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.13.0.tgz", + "integrity": "sha512-hE+HE8rnG1Z6Wzo+MhaKE5lM5eMx71T4EHJgku2E3xIfaULhDcxiiRxUYgwX8qwP1BBSlag+TdGOt6JAidIZTA==", "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.12.11", @@ -3128,10 +3267,19 @@ "aws-xray-sdk-core": { "version": "file:packages/core", "requires": { - "@types/cls-hooked": "^4.2.2", + "@aws-sdk/service-error-classification": "^3.4.1", + "@aws-sdk/types": "^3.4.1", + "@types/cls-hooked": "^4.3.3", "atomic-batcher": "^1.0.2", "cls-hooked": "^4.2.2", "semver": "^5.3.0" + }, + "dependencies": { + "@aws-sdk/types": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.6.1.tgz", + "integrity": "sha512-4Dx3eRTrUHLxhFdLJL8zdNGzVsJfAxtxPYYGmIddUkO2Gj3WA1TGjdfG4XN/ClI6e1XonCHafQX3UYO/mgnH3g==" + } } }, "aws-xray-sdk-express": { @@ -3665,12 +3813,6 @@ "pump": "^3.0.0" } }, - "http-cache-semantics": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", - "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==", - "dev": true - }, "lowercase-keys": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", @@ -3760,9 +3902,9 @@ } }, "caniuse-lite": { - "version": "1.0.30001208", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001208.tgz", - "integrity": "sha512-OE5UE4+nBOro8Dyvv0lfx+SRtfVIOM9uhKqFmJeUbGriqhhStgp1A0OyBpgy3OUF8AhYCT+PVwPC1gMl2ZcQMA==", + "version": "1.0.30001197", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001197.tgz", + "integrity": "sha512-8aE+sqBqtXz4G8g35Eg/XEaFr2N7rd/VQ6eABGBmNtcB8cN6qNJhMi6oSFy4UWWZgqgL3filHT8Nha4meu3tsw==", "dev": true }, "caseless": { @@ -4226,12 +4368,6 @@ "is-obj": "^2.0.0" } }, - "is-obj": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", - "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", - "dev": true - }, "make-dir": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", @@ -4957,9 +5093,9 @@ "dev": true }, "electron-to-chromium": { - "version": "1.3.712", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.712.tgz", - "integrity": "sha512-3kRVibBeCM4vsgoHHGKHmPocLqtFAGTrebXxxtgKs87hNUzXrX2NuS3jnBys7IozCnw7viQlozxKkmty2KNfrw==", + "version": "1.3.683", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.683.tgz", + "integrity": "sha512-8mFfiAesXdEdE0DhkMKO7W9U6VU/9T3VTWwZ+4g84/YMP4kgwgFtQgUxuu7FUMcvSeKSNhFQNU+WZ68BQTLT5A==", "dev": true }, "emitter-listener": { @@ -5211,18 +5347,18 @@ }, "dependencies": { "ansi-escapes": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.1.tgz", - "integrity": "sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", "dev": true, "requires": { - "type-fest": "^0.11.0" + "type-fest": "^0.21.3" } }, "type-fest": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz", - "integrity": "sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==", + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", "dev": true } } @@ -5399,9 +5535,9 @@ "dev": true }, "eslint-rule-docs": { - "version": "1.1.221", - "resolved": "https://registry.npmjs.org/eslint-rule-docs/-/eslint-rule-docs-1.1.221.tgz", - "integrity": "sha512-9vxNL90AmYii4Wy99MLQ3KfGyyfDixLurp9NlxB6tF3tqatL8YuAvjhm6vxRXKEEeMUh5Jc7lT49kiOWY/FhSQ==", + "version": "1.1.226", + "resolved": "https://registry.npmjs.org/eslint-rule-docs/-/eslint-rule-docs-1.1.226.tgz", + "integrity": "sha512-Wnn0ETzE2v2UT0OdRCcdMNPkQtbzyZr3pPPXnkreP0l6ZJaKqnl88dL1DqZ6nCCZZwDGBAnN0Y+nCvGxxLPQLQ==", "dev": true }, "eslint-scope": { @@ -7029,6 +7165,14 @@ "requires": { "is-stream": "^2.0.0", "type-fest": "^0.8.0" + }, + "dependencies": { + "is-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", + "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", + "dev": true + } } }, "he": { @@ -7476,9 +7620,9 @@ "dev": true }, "irregular-plurals": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/irregular-plurals/-/irregular-plurals-3.2.0.tgz", - "integrity": "sha512-YqTdPLfwP7YFN0SsD3QUVCkm9ZG2VzOXv3DOrw5G5mkMbVwptTwVcFv7/C0vOpBmgTxAeTG19XpUs1E522LW9Q==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/irregular-plurals/-/irregular-plurals-3.3.0.tgz", + "integrity": "sha512-MVBLKUTangM3EfRPFROhmWQQKRDsrgI83J8GS3jXy+OwYqiR2/aoWndYQ5416jLE3uaGgLH7ncme3X9y09gZ3g==", "dev": true }, "is-absolute": { @@ -7695,9 +7839,9 @@ "dev": true }, "is-path-inside": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.2.tgz", - "integrity": "sha512-/2UGPSgmtqwo1ktx8NDHjuPwZWmHhO+gj0f93EkhLB5RgW9RZevWYYlIkS6zePc6U2WpOdQYIwHe9YC4DWEBVg==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", "dev": true }, "is-plain-obj": { @@ -12496,9 +12640,9 @@ } }, "supports-hyperlinks": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.1.0.tgz", - "integrity": "sha512-zoE5/e+dnEijk6ASB6/qrK+oYdm2do1hjoLWrqUC/8WEIW1gbxFcKuBof7sW8ArN6e+AYvsE8HBGiVRWL/F5CA==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.2.0.tgz", + "integrity": "sha512-6sXEzV5+I5j8Bmq9/vUphGRM/RJNT9SCURJLjwfOg51heRtguGWDzcaBlgAzKhQa0EVNpPEKzQuBwZ8S8WaCeQ==", "dev": true, "requires": { "has-flag": "^4.0.0", @@ -12606,10 +12750,24 @@ }, "dependencies": { "agent-base": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-5.1.1.tgz", - "integrity": "sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g==", - "dev": true + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "requires": { + "debug": "4" + } + }, + "http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "dev": true, + "requires": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + } }, "https-proxy-agent": { "version": "4.0.0", @@ -12619,6 +12777,14 @@ "requires": { "agent-base": "5", "debug": "4" + }, + "dependencies": { + "agent-base": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-5.1.1.tgz", + "integrity": "sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g==", + "dev": true + } } } } @@ -12848,9 +13014,9 @@ } }, "tsd": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/tsd/-/tsd-0.13.1.tgz", - "integrity": "sha512-+UYM8LRG/M4H8ISTg2ow8SWi65PS7Os+4DUnyiQLbJysXBp2DEmws9SMgBH+m8zHcJZqUJQ+mtDWJXP1IAvB2A==", + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/tsd/-/tsd-0.15.1.tgz", + "integrity": "sha512-8ADO2rPntfNiJV4KiqJiiiitfkXLxCbKEFN672JgwNiaEIuiyurTc1+w3InZ+0DqBz73B6Z3UflZcNGw5xMaDA==", "dev": true, "requires": { "eslint-formatter-pretty": "^4.0.0", @@ -12861,59 +13027,6 @@ "update-notifier": "^4.1.0" }, "dependencies": { - "@nodelib/fs.stat": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.4.tgz", - "integrity": "sha512-IYlHJA0clt2+Vg7bccq+TzRdJvv19c2INqBSsoOLp1je7xjtr7J26+WXR72MCdvU9q1qTzIWDfhMf+DRvQJK4Q==", - "dev": true - }, - "array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true - }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "requires": { - "path-type": "^4.0.0" - } - }, - "fast-glob": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.5.tgz", - "integrity": "sha512-2DtFcgT68wiTTiwZ2hNdJfcHNke9XOfnwmBRWXhmeKM8rF0TGwmC/Qto3S7RoZKp5cilZbxzO5iTNTQsJ+EeDg==", - "dev": true, - "requires": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.0", - "merge2": "^1.3.0", - "micromatch": "^4.0.2", - "picomatch": "^2.2.1" - } - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, "find-up": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", @@ -12924,32 +13037,6 @@ "path-exists": "^4.0.0" } }, - "globby": { - "version": "11.0.2", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.2.tgz", - "integrity": "sha512-2ZThXDvvV8fYFRVIxnrMQBipZQDr7MxKAmQK1vujaj9/7eF0efG7BPUKJ7jP7G5SLF37xKDXvO4S/KKLj/Z0og==", - "dev": true, - "requires": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.1.1", - "ignore": "^5.1.4", - "merge2": "^1.3.0", - "slash": "^3.0.0" - } - }, - "ignore": { - "version": "5.1.8", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz", - "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==", - "dev": true - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, "locate-path": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", @@ -12978,16 +13065,6 @@ "yargs-parser": "^18.1.3" } }, - "micromatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", - "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", - "dev": true, - "requires": { - "braces": "^3.0.1", - "picomatch": "^2.0.5" - } - }, "p-limit": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", @@ -13030,12 +13107,6 @@ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true }, - "path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true - }, "read-pkg": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", @@ -13075,21 +13146,6 @@ } } }, - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - }, "type-fest": { "version": "0.13.1", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", @@ -13181,6 +13237,12 @@ "is-typedarray": "^1.0.0" } }, + "typescript": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.3.tgz", + "integrity": "sha512-qOcYwxaByStAWrBf4x0fibwZvMRG+r4cQoTjbPtUlrWjBHbmCAww1i448U0GJ+3cNNEtebDteo/cHOR3xJ4wEw==", + "dev": true + }, "uc.micro": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", diff --git a/package.json b/package.json index 5dfbc1f0..ff8d3389 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,10 @@ "private": true, "license": "Apache-2.0", "devDependencies": { + "@aws-sdk/middleware-stack": "^3.3.0", + "@aws-sdk/node-config-provider": "^3.4.1", + "@aws-sdk/config-resolver": "^3.4.1", + "@aws-sdk/smithy-client": "^3.4.1", "@hapi/hapi": "^20.0.0", "@types/chai": "^4.2.12", "@types/koa": "^2.11.3", @@ -40,7 +44,8 @@ "rewire": "^4.0.1", "sinon": "^9.0.2", "sinon-chai": "^3.5.0", - "tsd": "^0.13.1", + "tsd": "^0.15.1", + "typescript": "^4.1.3", "upath": "^1.2.0" }, "engines": { diff --git a/packages/core/README.md b/packages/core/README.md index e9091b24..9ee6aae0 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -465,6 +465,8 @@ function sendRequest(host, cb) { ### Capture all outgoing AWS requests +This is only available for AWS SDK v2 due to the modular architecture of AWS SDK v3. For more details on the difference between AWS SDK v2 and v3, see this [blog post](https://aws.amazon.com/blogs/developer/modular-aws-sdk-for-javascript-is-now-generally-available/). + ```js var AWS = captureAWS(require('aws-sdk')); @@ -473,6 +475,23 @@ var AWS = captureAWS(require('aws-sdk')); ### Capture outgoing AWS requests on a single client + +AWS SDK v3 + +```js +import { S3, PutObjectCommand } from '@aws-sdk/client-s3'; + +const s3 = AWSXRay.captureAWSv3Client(new S3({})); + +await s3.send(new PutObjectCommand({ + Bucket: bucketName, + Key: keyName, + Body: 'Hello!', +})); +``` + +AWS SDK v2 + ```js var s3 = AWSXRay.captureAWSClient(new AWS.S3()); @@ -623,6 +642,26 @@ function sendRequest(host, cb, subsegment) { ### Capture outgoing AWS requests on a single client +AWS SDK v3 + +You must re-capture the client every time the subsegment is attached to a new parent. + +```js +import { S3, PutObjectCommand } from '@aws-sdk/client-s3'; + +// subsegment is an optional parameter that is required for manual mode +// and can be omitted in automatic mode (e.g. inside a Lambda function). +const s3 = AWSXRay.captureAWSv3Client(new S3({}), subsegment); + +await s3.send(new PutObjectCommand({ + Bucket: bucketName, + Key: keyName, + Body: 'Hello!', +})); +``` + +AWS SDK v2 + ```js var s3 = AWSXRay.captureAWSClient(new AWS.S3()); var params = { @@ -639,6 +678,8 @@ s3.putObject(params, function(err, data) { ### Capture all outgoing AWS requests +This is only available for AWS SDK v2 due to the modular architecture of AWS SDK v3. For more details on the difference between AWS SDK v2 and v3, see this [blog post](https://aws.amazon.com/blogs/developer/modular-aws-sdk-for-javascript-is-now-generally-available/). + ```js var AWS = captureAWS(require('aws-sdk')); diff --git a/packages/core/lib/aws-xray.d.ts b/packages/core/lib/aws-xray.d.ts index 5347366f..bdba9865 100644 --- a/packages/core/lib/aws-xray.d.ts +++ b/packages/core/lib/aws-xray.d.ts @@ -39,7 +39,7 @@ export { captureAsyncFunc, captureCallbackFunc, captureFunc } from './capture' export { captureAWS, captureAWSClient } from './patchers/aws_p'; -export function captureAWSv3Client(client: T, manualSeg?: SegmentLike): T; +export { captureAWSClient as captureAWSv3Client } from './patchers/aws3_p'; export { captureHTTPs, captureHTTPsGlobal } from './patchers/http_p'; diff --git a/packages/core/lib/aws-xray.js b/packages/core/lib/aws-xray.js index 7f39e38a..e491dcb2 100644 --- a/packages/core/lib/aws-xray.js +++ b/packages/core/lib/aws-xray.js @@ -9,7 +9,7 @@ var LambdaEnv = require('./env/aws_lambda'); // pkginfo as an empty object var pkginfo = {} try { - pkginfo = require('../package.json'); + pkginfo = require('../../package.json'); } catch (err) { logging.getLogger().debug('Failed to load SDK data:', err); } @@ -180,8 +180,15 @@ var AWSXRay = { captureAWSClient: require('./patchers/aws_p').captureAWSClient, + /** + * @param {AWSv3.Service} service - An instance of a AWS SDK v3 service to wrap. + * @param {Segment|Subsegment} segment - Optional segment for manual mode. + * @memberof AWSXRay + * @function + * @see module:aws3_p.captureAWSClient + */ - captureAWSv3Client: (client) => client, + captureAWSv3Client: require('./patchers/aws3_p').captureAWSClient, /** * @param {http|https} module - The built in Node.js HTTP or HTTPS module. diff --git a/packages/core/lib/patchers/aws3_p.d.ts b/packages/core/lib/patchers/aws3_p.d.ts new file mode 100644 index 00000000..6a5d5b94 --- /dev/null +++ b/packages/core/lib/patchers/aws3_p.d.ts @@ -0,0 +1,10 @@ +import { Client } from '@aws-sdk/types'; +import { SegmentLike } from '../aws-xray'; +/** + * Instruments AWS SDK V3 clients with X-Ray via middleware. + * + * @param client - AWS SDK V3 client to instrument + * @param manualSegment - Parent segment or subsegment that is passed in for manual mode users + * @returns - the client with the X-Ray instrumentation middleware added to its middleware stack + */ +export declare function captureAWSClient>(client: T, manualSegment?: SegmentLike): T diff --git a/packages/core/lib/patchers/aws3_p.ts b/packages/core/lib/patchers/aws3_p.ts new file mode 100644 index 00000000..7a6e8613 --- /dev/null +++ b/packages/core/lib/patchers/aws3_p.ts @@ -0,0 +1,190 @@ +import { + Pluggable, + Client, + MetadataBearer, + BuildMiddleware, + MiddlewareStack, + BuildHandlerOptions, +} from '@aws-sdk/types'; + +import { RegionResolvedConfig } from '@aws-sdk/config-resolver'; + +import { isThrottlingError } from '@aws-sdk/service-error-classification'; + +import { SdkError } from '@aws-sdk/smithy-client'; + +import ServiceSegment from '../segments/attributes/aws'; + +import { stringify } from 'querystring'; + +import Subsegment from '../segments/attributes/subsegment'; + +const contextUtils = require('../context_utils'); + +const logger = require('../logger'); + +const { safeParseInt } = require('../utils'); + +import { getCauseTypeFromHttpStatus } from '../utils'; +import { SegmentLike } from '../aws-xray'; + +const XRAY_PLUGIN_NAME: string = 'XRaySDKInstrumentation'; + +interface HttpResponse { + response?: { + status?: number, + content_length?: number + } +}; + +const buildAttributesFromMetadata = async ( + service: string, + operation: string, + region: string, + res: any | null, + error: SdkError | null, +): Promise<[ServiceSegment, HttpResponse]> => { + const { extendedRequestId, requestId, httpStatusCode: statusCode, attempts } = res?.output?.$metadata || error?.$metadata; + + const aws = new ServiceSegment( + { + extendedRequestId, + requestId, + retryCount: attempts, + request: { + operation, + httpRequest: { + region, + statusCode, + }, + }, + }, + service, + ); + + const http: HttpResponse = {}; + + if (statusCode) { + http.response = {}; + http.response.status = statusCode; + } + if (res?.response?.headers && res?.response?.headers['content-length'] !== undefined) { + if (!http.response) { + http.response = {}; + } + http.response.content_length = safeParseInt(res.response.headers['content-length']); + } + return [aws, http]; +} + +function addFlags(http: HttpResponse, subsegment: Subsegment, err?: SdkError): void { + if (err && isThrottlingError(err)) { + subsegment.addThrottleFlag(); + } else if (safeParseInt(http.response?.status) === 429 || safeParseInt(err?.$metadata?.httpStatusCode) === 429) { + subsegment.addThrottleFlag(); + } + + const cause = getCauseTypeFromHttpStatus(safeParseInt(http.response?.status)); + if (cause === 'fault') { + subsegment.addFaultFlag(); + } else if (cause === 'error') { + subsegment.addErrorFlag(); + } +}; + +const getXRayMiddleware = (config: RegionResolvedConfig, manualSegment?: SegmentLike): BuildMiddleware => (next: any, context: any) => async (args: any) => { + const segment = contextUtils.isAutomaticMode() ? contextUtils.resolveSegment() : manualSegment; + const {clientName, commandName} = context; + const operation: string = commandName.slice(0, -7); // Strip trailing "Command" string + const service: string = clientName.slice(0, -6); // Strip trailing "Client" string + + if (!segment) { + const output = service + '.' + operation.charAt(0).toLowerCase() + operation.slice(1); + + if (!contextUtils.isAutomaticMode()) { + logger.getLogger().info('Call ' + output + ' requires a segment object' + + ' passed to captureAWSv3Client for tracing in manual mode. Ignoring.'); + } else { + logger.getLogger().info('Call ' + output + + ' is missing the sub/segment context for automatic mode. Ignoring.'); + } + return next(args); + } + + const subsegment: Subsegment = segment.addNewSubsegment(service); + subsegment.addAttribute('namespace', 'aws'); + const parent = (segment instanceof Subsegment ? segment.segment : segment); + + args.request.headers['X-Amzn-Trace-Id'] = stringify( + { + Root: parent.trace_id, + Parent: subsegment.id, + Sampled: parent.notTraced ? '0' : '1', + }, + ';', + ); + + let res; + try { + res = await next(args); + if (!res) throw new Error('Failed to get response from instrumented AWS Client.'); + + const [aws, http] = await buildAttributesFromMetadata( + service, + operation, + await config.region(), + res, + null, + ); + + subsegment.addAttribute('aws', aws); + subsegment.addAttribute('http', http); + + addFlags(http, subsegment); + subsegment.close(); + return res; + } catch (err) { + if (err.$metadata) { + const [aws, http] = await buildAttributesFromMetadata( + service, + operation, + await config.region(), + null, + err, + ); + + subsegment.addAttribute('aws', aws); + subsegment.addAttribute('http', http); + addFlags(http, subsegment, err); + } + + const errObj = { message: err.message, name: err.name, stack: err.stack || new Error().stack }; + subsegment.close(errObj, true); + throw err; + } +}; + +const xRayMiddlewareOptions: BuildHandlerOptions = { + name: XRAY_PLUGIN_NAME, + step: 'build', +}; + +const getXRayPlugin = (config: RegionResolvedConfig, manualSegment?: SegmentLike): Pluggable => ({ + applyToStack: (stack: MiddlewareStack) => { + stack.add(getXRayMiddleware(config, manualSegment), xRayMiddlewareOptions); + }, +}); + +/** + * Instruments AWS SDK V3 clients with X-Ray via middleware. + * + * @param client - AWS SDK V3 client to instrument + * @param manualSegment - Parent segment or subsegment that is passed in for manual mode users + * @returns - the client with the X-Ray instrumentation middleware added to its middleware stack + */ +export function captureAWSClient>(client: T, manualSegment?: SegmentLike): T { + // Remove existing middleware to ensure operation is idempotent + client.middlewareStack.remove(XRAY_PLUGIN_NAME); + client.middlewareStack.use(getXRayPlugin(client.config, manualSegment)); + return client; +} diff --git a/packages/core/lib/segments/attributes/aws.js b/packages/core/lib/segments/attributes/aws.js index 66f071f4..be463337 100644 --- a/packages/core/lib/segments/attributes/aws.js +++ b/packages/core/lib/segments/attributes/aws.js @@ -19,11 +19,15 @@ function Aws(res, serviceName) { Aws.prototype.init = function init(res, serviceName) { //TODO: account ID this.operation = formatOperation(res.request.operation) || ''; - this.region = res.request.httpRequest.region || ''; - this.request_id = res.requestId || ''; + if (res && res.request && res.request.httpRequest && res.request.httpRequest.region) { + this.region = res.request.httpRequest.region; + } + if (res && res.requestId) { + this.request_id = res.requestId; + } this.retries = res.retryCount || 0; - if (res.extendedRequestId && serviceName === 's3') + if (res.extendedRequestId && serviceName && serviceName.toLowerCase() === 's3') this.id_2 = res.extendedRequestId; this.addData(capturer.capture(serviceName, res)); diff --git a/packages/core/lib/segments/attributes/subsegment.d.ts b/packages/core/lib/segments/attributes/subsegment.d.ts index f37b0c75..022b1063 100644 --- a/packages/core/lib/segments/attributes/subsegment.d.ts +++ b/packages/core/lib/segments/attributes/subsegment.d.ts @@ -1,4 +1,5 @@ import * as http from 'http'; +import { Segment, SegmentLike } from '../../aws-xray'; declare class Subsegment { id: string; @@ -6,6 +7,8 @@ declare class Subsegment { start_time: number; in_progress?: boolean; subsegments?: Array; + parent: SegmentLike; + segment: Segment; constructor(name: string); diff --git a/packages/core/lib/segments/segment.d.ts b/packages/core/lib/segments/segment.d.ts index c689eb40..f83de01e 100644 --- a/packages/core/lib/segments/segment.d.ts +++ b/packages/core/lib/segments/segment.d.ts @@ -11,6 +11,7 @@ declare class Segment { parent_id?: string; origin?: string; subsegments?: Array; + notTraced?: boolean; constructor(name: string, rootId?: string | null, parentId?: string | null); diff --git a/packages/core/package.json b/packages/core/package.json index 8e529bf9..4bd520a1 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -5,10 +5,16 @@ "author": "Amazon Web Services", "contributors": [ "Sandra McMullen ", - "William Armiros " + "William Armiros ", + "Moritz Onken " ], - "main": "lib/index.js", - "types": "lib/index.d.ts", + "files": [ + "dist/lib/**/*", + "LICENSE", + "README.md" + ], + "main": "dist/lib/index.js", + "types": "dist/lib/index.d.ts", "engines": { "node": ">= 10.x" }, @@ -17,15 +23,22 @@ }, "//": "@types/cls-hooked is exposed in API so must be in dependencies, not devDependencies", "dependencies": { - "@types/cls-hooked": "^4.2.2", + "@aws-sdk/types": "^3.4.1", + "@aws-sdk/service-error-classification": "^3.4.1", + "@types/cls-hooked": "^4.3.3", "atomic-batcher": "^1.0.2", "cls-hooked": "^4.2.2", "semver": "^5.3.0" }, "scripts": { - "test": "mocha --recursive ./test/ -R spec && tsd && mocha --recursive ./test_async/ -R spec", + "prepare": "npm run compile", + "compile": "tsc && npm run copy-lib && npm run copy-test", + "copy-lib": "find lib -type f \\( -name '*.d.ts' -o -name '*.json' \\) | xargs -I % ../../scripts/cp-with-structure.sh % dist", + "copy-test": "find test -name '*.json' | xargs -I % ../../scripts/cp-with-structure.sh % dist", + "test": "npm run compile && mocha --recursive ./dist/test/ -R spec && tsd && mocha --recursive ./dist/test_async/ -R spec", "test-d": "tsd", - "test-async": "mocha --recursive ./test_async/ -R spec", + "test-async": "npm run compile && mocha --recursive ./dist/test_async/ -R spec", + "clean": "rm -rf dist && rm -rf node_modules", "testcov": "nyc npm run test", "reportcov": "nyc report --reporter=text-lcov > coverage.lcov && codecov" }, diff --git a/packages/core/test/integration/segment_maintained_across_shared_promise.test.js b/packages/core/test/integration/segment_maintained_across_shared_promise.test.js index 25a58c10..e43f0fe9 100644 --- a/packages/core/test/integration/segment_maintained_across_shared_promise.test.js +++ b/packages/core/test/integration/segment_maintained_across_shared_promise.test.js @@ -7,7 +7,7 @@ if (!global.Promise) { var assert = require('chai').assert; var http = require('http'); -var AWSXRay = require('../../'); +var AWSXRay = require('../../lib'); var Segment = AWSXRay.Segment; AWSXRay.capturePromise(); diff --git a/packages/core/test/unit/aws-xray.test.js b/packages/core/test/unit/aws-xray.test.js index 6ca9f375..9dd1f80f 100644 --- a/packages/core/test/unit/aws-xray.test.js +++ b/packages/core/test/unit/aws-xray.test.js @@ -2,7 +2,6 @@ var assert = require('chai').assert; var chai = require('chai'); var sinon = require('sinon'); var sinonChai = require('sinon-chai'); -var upath = require('upath'); var segmentUtils = require('../../lib/segments/segment_utils'); @@ -30,14 +29,12 @@ describe('AWSXRay', function() { // This test requires both index.js and aws-xray.js are first time required. // We should always clear the require cache for these two files so this test // could run independently. - Object.keys(require.cache).forEach(function(key) { - var normalisedPath = upath.toUnix(key); - if(normalisedPath.includes('core/lib/index.js') || normalisedPath.includes('core/lib/aws-xray.js')) { - delete require.cache[key]; - } - }); + const indexPath = '../../lib/index'; + const xrayPath = '../../lib/aws-xray'; + delete require.cache[require.resolve(indexPath)]; + delete require.cache[require.resolve(xrayPath)]; - AWSXRay = require('../../lib/index'); + AWSXRay = require(indexPath); setSDKDataStub.should.have.been.calledWithExactly(sinon.match.object); setServiceDataStub.should.have.been.calledWithExactly(sinon.match.object); diff --git a/packages/core/test/unit/env/aws_lambda.test.js b/packages/core/test/unit/env/aws_lambda.test.js index ceb19f1f..ddb1f2ec 100644 --- a/packages/core/test/unit/env/aws_lambda.test.js +++ b/packages/core/test/unit/env/aws_lambda.test.js @@ -41,7 +41,7 @@ describe('AWSLambda', function() { }); describe('#init', function() { - var disableReusableSocketStub, populateStub, sandbox, setSegmentStub, validateStub; + var disableReusableSocketStub, disableCentralizedSamplingStub, populateStub, sandbox, setSegmentStub, validateStub; beforeEach(function() { sandbox = sinon.createSandbox(); diff --git a/packages/core/test/unit/patchers/aws3_p.test.js b/packages/core/test/unit/patchers/aws3_p.test.js new file mode 100644 index 00000000..fe953530 --- /dev/null +++ b/packages/core/test/unit/patchers/aws3_p.test.js @@ -0,0 +1,251 @@ +var assert = require('chai').assert; +var chai = require('chai'); +var sinon = require('sinon'); +var sinonChai = require('sinon-chai'); + +var Aws = require('../../../lib/segments/attributes/aws'); +var awsPatcher = require('../../../lib/patchers/aws3_p'); +var contextUtils = require('../../../lib/context_utils'); +var Segment = require('../../../lib/segments/segment'); +var Utils = require('../../../lib/utils'); + +var { constructStack } = require('@aws-sdk/middleware-stack'); + +var logger = require('../../../lib/logger').getLogger(); + +chai.should(); +chai.use(sinonChai); + +var traceId = '1-57fbe041-2c7ad569f5d6ff149137be86'; + +describe('AWS v3 patcher', function() { + describe('#captureAWSClient', function() { + var customStub, sandbox, useMiddleware; + + var awsClient = { + send: function() {}, + config: { + serviceId: 's3', + }, + middlewareStack: constructStack(), + }; + + beforeEach(function() { + sandbox = sinon.createSandbox(); + customStub = sandbox.stub(awsClient, 'send'); + useMiddleware = sandbox.stub(awsClient.middlewareStack, 'use'); + }); + + afterEach(function() { + sandbox.restore(); + }); + + it('should call middlewareStack.use and return the service', function() { + const patched = awsPatcher.captureAWSClient(awsClient); + useMiddleware.should.have.been.calledOnce; + assert.equal(patched, awsClient); + }); + }); + + describe('#captureAWSRequest', function() { + var awsClient, awsRequest, sandbox, segment, stubResolve, addNewSubsegmentStub, sub; + + before(function() { + awsClient = { + send: async (req) => { + const context = { + clientName: 'S3Client', + commandName: 'ListBucketsCommand', + }; + const handler = awsClient.middlewareStack.resolve((args) => { + const error = req.response.error; + if (error) { + const err = new Error(error.message); + err.name = error.code; + err.$metadata = req.response.$metadata; + throw err; + } + return args; + }, context); + await handler(req); + return req.response; + }, + config: { + region: async () => 'us-east-1', + }, + middlewareStack: constructStack(), + }; + }); + + beforeEach(function() { + sandbox = sinon.createSandbox(); + + awsRequest = new (class ListBucketsCommand { + constructor() { + this.request = { + method: 'GET', + url: '/', + connection: { + remoteAddress: 'localhost' + }, + headers: {} + }; + this.response = {}; + this.output = { + $metadata: { + requestId: '123', + extendedRequestId: '456', + } + }; + } + })(); + + segment = new Segment('testSegment', traceId); + sub = segment.addNewSubsegment('subseg'); + stubResolve = sandbox.stub(contextUtils, 'resolveSegment').returns(segment); + addNewSubsegmentStub = sandbox.stub(segment, 'addNewSubsegment').returns(sub); + }); + + afterEach(function() { + sandbox.restore(); + }); + + describe('#automaticMode', () => { + beforeEach(() => { + sandbox.stub(contextUtils, 'isAutomaticMode').returns(true); + }); + + before(() => { + awsClient = awsPatcher.captureAWSClient(awsClient); + }); + + it('should log an info statement and exit if parent is not found in the context for automatic mode', (done) => { + stubResolve.returns(); + const logStub = sandbox.stub(logger, 'info'); + + awsClient.send(awsRequest); + + setTimeout(function() { + logStub.should.have.been.calledOnce; + done(); + }, 50); + }); + + it('should inject the tracing headers', async function() { + await awsClient.send(awsRequest); + + assert.isTrue(addNewSubsegmentStub.calledWith('S3')); + + const expected = new RegExp('^Root=' + traceId + ';Parent=' + sub.id + ';Sampled=1$'); + assert.match(awsRequest.request.headers['X-Amzn-Trace-Id'], expected); + }); + + it('should close on complete with no errors when code 200 is seen', async function() { + const closeStub = sandbox.stub(sub, 'close').returns(); + sandbox.stub(sub, 'addAttribute').returns(); + sandbox.stub(Aws.prototype, 'init').returns(); + + awsRequest.response = { + $metadata: { httpStatusCode: 200 }, + }; + + await awsClient.send(awsRequest); + + closeStub.should.have.been.calledWithExactly(); + }); + + it('should mark the subsegment as throttled and error if code 429 is seen', async function() { + const throttleStub = sandbox.stub(sub, 'addThrottleFlag').returns(); + + sandbox.stub(sub, 'addAttribute').returns(); + sandbox.stub(Aws.prototype, 'init').returns(); + + awsRequest.response = { + error: { message: 'throttling', code: 'ThrottlingError' }, + $metadata: { httpStatusCode: 429 }, + }; + + await awsClient.send(awsRequest).catch(() => null); + + throttleStub.should.have.been.calledOnce; + assert.isTrue(sub.error); + }); + + it('should mark the subsegment as throttled and error if code service.throttledError returns true, regardless of status code', async function() { + const throttleStub = sandbox.stub(sub, 'addThrottleFlag').returns(); + + sandbox.stub(sub, 'addAttribute').returns(); + sandbox.stub(Aws.prototype, 'init').returns(); + + awsRequest.response = { + error: { message: 'throttling', code: 'ProvisionedThroughputExceededException' }, + $metadata: { httpStatusCode: 400 }, + }; + + await awsClient.send(awsRequest).catch(() => null); + + throttleStub.should.have.been.calledOnce; + assert.isTrue(sub.error); + }); + + it('should capture an error on the response and mark exception as remote', async function() { + const closeStub = sandbox.stub(sub, 'close').returns(); + const getCauseStub = sandbox.stub(Utils, 'getCauseTypeFromHttpStatus').returns(); + + sandbox.stub(sub, 'addAttribute').returns(); + sandbox.stub(Aws.prototype, 'init').returns(); + + const error = { message: 'big error', code: 'Error' }; + + awsRequest.response = { + error, + $metadata: { httpStatusCode: 500 }, + }; + + await awsClient.send(awsRequest).catch(() => null); + + getCauseStub.should.have.been.calledWithExactly(500); + closeStub.should.have.been.calledWithExactly(sinon.match({ message: error.message, name: error.code}), true); + }); + }); + + describe('#manualMode', () => { + beforeEach(() => { + sandbox.stub(contextUtils, 'isAutomaticMode').returns(false); + }); + + it('should log an info statement and exit if parent is not found in the context for manual mode', (done) => { + awsClient = awsPatcher.captureAWSClient(awsClient); + var logStub = sandbox.stub(logger, 'info'); + + awsClient.send(awsRequest); + + setTimeout(function() { + logStub.should.have.been.calledOnce; + done(); + }, 50); + }); + + it('should use the provided parent segment', () => { + awsClient = awsPatcher.captureAWSClient(awsClient, segment); + + awsClient.send(awsRequest); + + assert.isTrue(addNewSubsegmentStub.calledWith('S3')); + }); + + it('should handle several calls to capture', () => { + const otherSeg = new Segment('otherTest'); + const otherAddNewStub = sandbox.stub(otherSeg, 'addNewSubsegment'); + + awsClient = awsPatcher.captureAWSClient(awsClient, segment); + awsClient.send(awsRequest); + assert.isTrue(addNewSubsegmentStub.calledWith('S3')); + + awsClient = awsPatcher.captureAWSClient(awsClient, otherSeg); + awsClient.send(awsRequest); + assert.isTrue(otherAddNewStub.calledWith('S3')); + }); + }); + }); +}); diff --git a/packages/core/test_async/integration/segment_maintained_across_awaited.test.js b/packages/core/test_async/integration/segment_maintained_across_awaited.test.js index 479ffab8..f844a4bf 100644 --- a/packages/core/test_async/integration/segment_maintained_across_awaited.test.js +++ b/packages/core/test_async/integration/segment_maintained_across_awaited.test.js @@ -1,6 +1,6 @@ var assert = require('chai').assert; var http = require('http'); -var AWSXRay = require('../../'); +var AWSXRay = require('../../lib'); var Segment = AWSXRay.Segment; AWSXRay.enableAutomaticMode(); diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json new file mode 100644 index 00000000..93199148 --- /dev/null +++ b/packages/core/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es2019", + "strict": true, + "strictNullChecks": true, + "declaration": false, + "esModuleInterop": true, + "skipLibCheck": true, + "allowJs": true, + "checkJs": false, + "outDir": "dist", + }, + "include": ["lib", "test", "test_async"], + "exclude": ["dist", "node_modules"] +} diff --git a/packages/express/test-d/index.test-d.ts b/packages/express/test-d/index.test-d.ts index 9720c4c3..d965afd4 100644 --- a/packages/express/test-d/index.test-d.ts +++ b/packages/express/test-d/index.test-d.ts @@ -1,5 +1,5 @@ import * as AWSXRay from 'aws-xray-sdk-core'; -import * as express from 'express'; +import express from 'express'; import { expectType } from 'tsd'; import * as xrayExpress from '../lib'; diff --git a/packages/express/tsconfig.json b/packages/express/tsconfig.json new file mode 100644 index 00000000..2f980427 --- /dev/null +++ b/packages/express/tsconfig.json @@ -0,0 +1,5 @@ +{ + "compilerOptions": { + "esModuleInterop": true + } +} diff --git a/packages/full_sdk/tsconfig.json b/packages/full_sdk/tsconfig.json new file mode 100644 index 00000000..2f980427 --- /dev/null +++ b/packages/full_sdk/tsconfig.json @@ -0,0 +1,5 @@ +{ + "compilerOptions": { + "esModuleInterop": true + } +} diff --git a/packages/mysql/tsconfig.json b/packages/mysql/tsconfig.json new file mode 100644 index 00000000..2f980427 --- /dev/null +++ b/packages/mysql/tsconfig.json @@ -0,0 +1,5 @@ +{ + "compilerOptions": { + "esModuleInterop": true + } +} diff --git a/packages/postgres/tsconfig.json b/packages/postgres/tsconfig.json new file mode 100644 index 00000000..2f980427 --- /dev/null +++ b/packages/postgres/tsconfig.json @@ -0,0 +1,5 @@ +{ + "compilerOptions": { + "esModuleInterop": true + } +} diff --git a/packages/restify/tsconfig.json b/packages/restify/tsconfig.json new file mode 100644 index 00000000..2f980427 --- /dev/null +++ b/packages/restify/tsconfig.json @@ -0,0 +1,5 @@ +{ + "compilerOptions": { + "esModuleInterop": true + } +} diff --git a/scripts/cp-with-structure.sh b/scripts/cp-with-structure.sh new file mode 100755 index 00000000..1a9b046d --- /dev/null +++ b/scripts/cp-with-structure.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +## This script copies a file provided as the first parameter into the destination directory +## provided by the second parameter, while maintaining the directory structure of the source +## file. It is similar to 'cp --parents' on Linux, but usable cross-platform +src=(${*: 1}) +dest=${*: -1:1} +for filename in $src; do + [ -e "$filename" ] || continue + dirPath=$(dirname "${filename}") + mkdir -p $dest/$dirPath + cp -a $filename $dest/$dirPath +done