diff --git a/.eslintrc.json b/.eslintrc.json index 4656d6d0..5ea7c777 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -10,6 +10,7 @@ }, "ignorePatterns": ["tests/fixtures/**/*.js"], "rules": { + "arrow-spacing": "error", "quotes": ["error", "single"], "space-before-function-paren": ["error", { "named": "never", diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..df3ddcba --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.json linguist-language=JSON-with-Comments \ No newline at end of file diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml new file mode 100644 index 00000000..f9643616 --- /dev/null +++ b/.github/workflows/coverage.yml @@ -0,0 +1,32 @@ +# This workflow will run our tests, generate an lcov code coverage file, +# and send that coverage to Coveralls + +name: Code Coverage + +on: + push: + branches-ignore: dev/* + pull_request: + +jobs: + Coveralls: + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [16.x] + + steps: + - uses: actions/checkout@v2 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node-version }} + - run: npm ci + - run: git config --global user.email "slapshot@yext.com" + - run: git config --global user.name "Jambo run-tests.yml" + - run: npx jest --config=jest-coverage.json + - name: Coveralls + uses: coverallsapp/github-action@master + with: + github-token: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index fd25cccf..1969304d 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -3,9 +3,7 @@ name: Run Tests -on: - pull_request: - branches: [ master ] +on: [push, pull_request] jobs: build: @@ -14,7 +12,7 @@ jobs: strategy: matrix: - node-version: [10.x, 12.x, 14.x] + node-version: [12.x, 14.x, 16.x] steps: - uses: actions/checkout@v2 @@ -23,4 +21,6 @@ jobs: with: node-version: ${{ matrix.node-version }} - run: npm ci + - run: git config --global user.email "slapshot@yext.com" + - run: git config --global user.name "Jambo run-tests.yml" - run: npm test diff --git a/README.md b/README.md index 94991ab9..4831df87 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,11 @@ # Jambo +
+ + Coverage Status + +
+ Jambo is a JAMStack implementation using Handlebars. ## Installation @@ -9,6 +15,7 @@ Install jambo from npm, and save it to your package.json as a dev-dependency. ```bash npm install -D jambo ``` + ___ ## Usage @@ -28,27 +35,31 @@ Currently, only answers-hitchhiker-theme is supported. ###### Optional Arguments ---theme _theme_name_ +--themeUrl _theme_url_ + +The git URL of the theme to import, if a theme should be imported on init. -Import a theme after initializing the repo. +--useSubmodules _true/false_ + +If importing a theme on init, whether to import it as a git submodule as opposed to regular files. Defaults to false. #### Import ```bash -npx jambo import --theme answers-hitchhiker-theme +npx jambo import --themeUrl https://github.com/yext/answers-hitchhiker-theme.git ``` The import command imports the designated theme into the 'themes' folder. -**--theme** _theme_name_ +**--themeUrl** _theme_url_ -The name of the theme to import. +The git URL of the theme to import. ###### Optional Arguments ---addAsSubmodule _true/false_ +--useSubmodules _true/false_ -Whether to import the theme as a submodule, defaults to true. +Whether to import the theme as a git submodule, as opposed to regular files. Defaults to false. #### Override diff --git a/jest-coverage.json b/jest-coverage.json new file mode 100644 index 00000000..052c29a7 --- /dev/null +++ b/jest-coverage.json @@ -0,0 +1,15 @@ +{ + "collectCoverage": true, + "collectCoverageFrom": ["src/**"], + "setupFilesAfterEnv": [ + "./tests/setup/setup.js" + ], + "testMatch": [ + "**/tests/**/*.js" + ], + "testPathIgnorePatterns": [ + "/tests/fixtures/", + "/tests/setup/", + "/tests/acceptance/" + ] +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 67ee428d..7cfe9e49 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "jambo", - "version": "1.10.4", + "version": "1.11.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -3528,6 +3528,98 @@ "@types/istanbul-lib-report": "*" } }, + "@types/jest": { + "version": "26.0.23", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-26.0.23.tgz", + "integrity": "sha512-ZHLmWMJ9jJ9PTiT58juykZpL7KjwJywFN3Rr2pTSkyQfydf/rk22yS7W8p5DaVUMQ2BQC7oYiU3FjbTM/mYrOA==", + "dev": true, + "requires": { + "jest-diff": "^26.0.0", + "pretty-format": "^26.0.0" + }, + "dependencies": { + "@jest/types": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", + "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^15.0.0", + "chalk": "^4.0.0" + } + }, + "@types/istanbul-reports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz", + "integrity": "sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true + }, + "chalk": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz", + "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "diff-sequences": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-26.6.2.tgz", + "integrity": "sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q==", + "dev": true + }, + "jest-diff": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-26.6.2.tgz", + "integrity": "sha512-6m+9Z3Gv9wN0WFVasqjCL/06+EFCMTqDEUl/b87HYK2rAPTyfz4ZIuSlPhY51PIQRWx5TaxeF1qmXKe9gfN3sA==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "diff-sequences": "^26.6.2", + "jest-get-type": "^26.3.0", + "pretty-format": "^26.6.2" + } + }, + "jest-get-type": { + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.3.0.tgz", + "integrity": "sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==", + "dev": true + }, + "pretty-format": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz", + "integrity": "sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg==", + "dev": true, + "requires": { + "@jest/types": "^26.6.2", + "ansi-regex": "^5.0.0", + "ansi-styles": "^4.0.0", + "react-is": "^17.0.1" + } + }, + "react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true + } + } + }, "@types/minimatch": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", @@ -3577,47 +3669,17 @@ "dev": true }, "abab": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.3.tgz", - "integrity": "sha512-tsFzPpcttalNjFBCFMqsKYQcWxxen1pgJR56by//QwvJc4/OUS3kPOOttx2tSIfjsylB0pYu7f5D3K1RCxUnUg==", - "dev": true - }, - "acorn": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.3.1.tgz", - "integrity": "sha512-tLc0wSnatxAQHVHUapaHdz72pi9KUyHjq5KyHjGg9Y8Ifdc79pTh2XvI6I1/chZbnM7QtNKzh66ooDogPZSleA==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz", + "integrity": "sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q==", "dev": true }, - "acorn-globals": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-4.3.4.tgz", - "integrity": "sha512-clfQEh21R+D0leSbUdWf3OcfqyaCSAQ8Ryq00bofSekfr9W8u1jyYZo6ir0xu9Gtcf7BjcHJpnbZH7JOCpP60A==", - "dev": true, - "requires": { - "acorn": "^6.0.1", - "acorn-walk": "^6.0.1" - }, - "dependencies": { - "acorn": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz", - "integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==", - "dev": true - } - } - }, "acorn-jsx": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.1.tgz", "integrity": "sha512-K0Ptm/47OKfQRpNQ2J/oIN/3QYiK6FwW+eJbILhsdxh2WTLdl+30o8aGdTbm5JbffpFFAg/g+zi1E+jvJha5ng==", "dev": true }, - "acorn-walk": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.2.0.tgz", - "integrity": "sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA==", - "dev": true - }, "ajv": { "version": "6.12.2", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.2.tgz", @@ -3773,9 +3835,9 @@ "dev": true }, "aws4": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.10.0.tgz", - "integrity": "sha512-3YDiu347mtVtjpyV3u5kVqQLP242c06zwDOgpeRnybmXlYYsLbtTrUBUm8i8srONt+FWobl5aibnU1030PeeuA==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", + "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==", "dev": true }, "babel-jest": { @@ -4024,12 +4086,13 @@ "camelcase": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true }, "caniuse-lite": { - "version": "1.0.30001131", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001131.tgz", - "integrity": "sha512-4QYi6Mal4MMfQMSqGIRPGbKIbZygeN83QsWq1ixpUwvtfgAZot5BrCKzGygvZaV+CnELdTwD0S4cqUNozq7/Cw==" + "version": "1.0.30001220", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001220.tgz", + "integrity": "sha512-pjC2T4DIDyGAKTL4dMvGUQaMUHRmhvPpAgNNTa14jaBWHu+bLQgvpFqElxh9L4829Fdx0PlKiMp3wnYldRtECA==" }, "capture-exit": { "version": "2.0.0", @@ -4086,13 +4149,13 @@ } }, "cliui": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", - "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", "requires": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", - "wrap-ansi": "^6.2.0" + "wrap-ansi": "^7.0.0" } }, "co": { @@ -4273,17 +4336,6 @@ "assert-plus": "^1.0.0" } }, - "data-urls": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-1.1.0.tgz", - "integrity": "sha512-YTWYI9se1P55u58gL5GkQHW4P6VJBJ5iBT+B5a7i2Tjadhv52paJG0qHX4A0OR6/t52odI64KP2YvFpkDOi3eQ==", - "dev": true, - "requires": { - "abab": "^2.0.0", - "whatwg-mimetype": "^2.2.0", - "whatwg-url": "^7.0.0" - } - }, "debug": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", @@ -4295,7 +4347,8 @@ "decamelize": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true }, "decode-uri-component": { "version": "0.2.0", @@ -4399,15 +4452,6 @@ "esutils": "^2.0.2" } }, - "domexception": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/domexception/-/domexception-1.0.1.tgz", - "integrity": "sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug==", - "dev": true, - "requires": { - "webidl-conversions": "^4.0.2" - } - }, "ecc-jsbn": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", @@ -4512,19 +4556,6 @@ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, - "escodegen": { - "version": "1.14.3", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", - "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", - "dev": true, - "requires": { - "esprima": "^4.0.1", - "estraverse": "^4.2.0", - "esutils": "^2.0.2", - "optionator": "^0.8.1", - "source-map": "~0.6.1" - } - }, "eslint": { "version": "7.9.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.9.0.tgz", @@ -5049,6 +5080,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, "requires": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" @@ -5294,13 +5326,27 @@ "dev": true }, "har-validator": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", - "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", + "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", "dev": true, "requires": { - "ajv": "^6.5.5", + "ajv": "^6.12.3", "har-schema": "^2.0.0" + }, + "dependencies": { + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + } } }, "has": { @@ -5384,15 +5430,6 @@ "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==", "dev": true }, - "html-encoding-sniffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz", - "integrity": "sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw==", - "dev": true, - "requires": { - "whatwg-encoding": "^1.0.1" - } - }, "html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -5804,6 +5841,17 @@ "jest-cli": "^25.5.4" }, "dependencies": { + "cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, "graceful-fs": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", @@ -5830,6 +5878,54 @@ "prompts": "^2.0.1", "realpath-native": "^2.0.0", "yargs": "^15.3.1" + }, + "dependencies": { + "yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "dev": true, + "requires": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + } + } + } + }, + "wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "dev": true + }, + "yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" } } } @@ -6008,6 +6104,171 @@ "jest-mock": "^25.5.0", "jest-util": "^25.5.0", "jsdom": "^15.2.1" + }, + "dependencies": { + "acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true + }, + "acorn-globals": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-4.3.4.tgz", + "integrity": "sha512-clfQEh21R+D0leSbUdWf3OcfqyaCSAQ8Ryq00bofSekfr9W8u1jyYZo6ir0xu9Gtcf7BjcHJpnbZH7JOCpP60A==", + "dev": true, + "requires": { + "acorn": "^6.0.1", + "acorn-walk": "^6.0.1" + }, + "dependencies": { + "acorn": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz", + "integrity": "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==", + "dev": true + } + } + }, + "acorn-walk": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.2.0.tgz", + "integrity": "sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA==", + "dev": true + }, + "data-urls": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-1.1.0.tgz", + "integrity": "sha512-YTWYI9se1P55u58gL5GkQHW4P6VJBJ5iBT+B5a7i2Tjadhv52paJG0qHX4A0OR6/t52odI64KP2YvFpkDOi3eQ==", + "dev": true, + "requires": { + "abab": "^2.0.0", + "whatwg-mimetype": "^2.2.0", + "whatwg-url": "^7.0.0" + } + }, + "domexception": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-1.0.1.tgz", + "integrity": "sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug==", + "dev": true, + "requires": { + "webidl-conversions": "^4.0.2" + } + }, + "escodegen": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", + "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", + "dev": true, + "requires": { + "esprima": "^4.0.1", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1", + "source-map": "~0.6.1" + } + }, + "html-encoding-sniffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz", + "integrity": "sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw==", + "dev": true, + "requires": { + "whatwg-encoding": "^1.0.1" + } + }, + "jsdom": { + "version": "15.2.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-15.2.1.tgz", + "integrity": "sha512-fAl1W0/7T2G5vURSyxBzrJ1LSdQn6Tr5UX/xD4PXDx/PDgwygedfW6El/KIj3xJ7FU61TTYnc/l/B7P49Eqt6g==", + "dev": true, + "requires": { + "abab": "^2.0.0", + "acorn": "^7.1.0", + "acorn-globals": "^4.3.2", + "array-equal": "^1.0.0", + "cssom": "^0.4.1", + "cssstyle": "^2.0.0", + "data-urls": "^1.1.0", + "domexception": "^1.0.1", + "escodegen": "^1.11.1", + "html-encoding-sniffer": "^1.0.2", + "nwsapi": "^2.2.0", + "parse5": "5.1.0", + "pn": "^1.1.0", + "request": "^2.88.0", + "request-promise-native": "^1.0.7", + "saxes": "^3.1.9", + "symbol-tree": "^3.2.2", + "tough-cookie": "^3.0.1", + "w3c-hr-time": "^1.0.1", + "w3c-xmlserializer": "^1.1.2", + "webidl-conversions": "^4.0.2", + "whatwg-encoding": "^1.0.5", + "whatwg-mimetype": "^2.3.0", + "whatwg-url": "^7.0.0", + "ws": "^7.0.0", + "xml-name-validator": "^3.0.0" + } + }, + "saxes": { + "version": "3.1.11", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-3.1.11.tgz", + "integrity": "sha512-Ydydq3zC+WYDJK1+gRxRapLIED9PWeSuuS41wqyoRmzvhhh9nc+QQrVMKJYzJFULazeGhzSV0QleN2wD3boh2g==", + "dev": true, + "requires": { + "xmlchars": "^2.1.1" + } + }, + "tough-cookie": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-3.0.1.tgz", + "integrity": "sha512-yQyJ0u4pZsv9D4clxO69OEjLWYw+jbgspjTue4lTQZLfV0c5l1VmK2y1JK8E9ahdpltPOaAThPcp5nKPUgSnsg==", + "dev": true, + "requires": { + "ip-regex": "^2.1.0", + "psl": "^1.1.28", + "punycode": "^2.1.1" + } + }, + "tr46": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", + "integrity": "sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "w3c-xmlserializer": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-1.1.2.tgz", + "integrity": "sha512-p10l/ayESzrBMYWRID6xbuCKh2Fp77+sA0doRuGn4tTIMrrZVeqfpKjXHY+oDh3K4nLdPgNwMTVP6Vp4pvqbNg==", + "dev": true, + "requires": { + "domexception": "^1.0.1", + "webidl-conversions": "^4.0.2", + "xml-name-validator": "^3.0.0" + } + }, + "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==", + "dev": true + }, + "whatwg-url": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", + "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", + "dev": true, + "requires": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + } } }, "jest-environment-node": { @@ -6256,11 +6517,68 @@ "yargs": "^15.3.1" }, "dependencies": { + "cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, "graceful-fs": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", "dev": true + }, + "wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "dev": true + }, + "yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "dev": true, + "requires": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + } + }, + "yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } } } }, @@ -6392,40 +6710,6 @@ "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", "dev": true }, - "jsdom": { - "version": "15.2.1", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-15.2.1.tgz", - "integrity": "sha512-fAl1W0/7T2G5vURSyxBzrJ1LSdQn6Tr5UX/xD4PXDx/PDgwygedfW6El/KIj3xJ7FU61TTYnc/l/B7P49Eqt6g==", - "dev": true, - "requires": { - "abab": "^2.0.0", - "acorn": "^7.1.0", - "acorn-globals": "^4.3.2", - "array-equal": "^1.0.0", - "cssom": "^0.4.1", - "cssstyle": "^2.0.0", - "data-urls": "^1.1.0", - "domexception": "^1.0.1", - "escodegen": "^1.11.1", - "html-encoding-sniffer": "^1.0.2", - "nwsapi": "^2.2.0", - "parse5": "5.1.0", - "pn": "^1.1.0", - "request": "^2.88.0", - "request-promise-native": "^1.0.7", - "saxes": "^3.1.9", - "symbol-tree": "^3.2.2", - "tough-cookie": "^3.0.1", - "w3c-hr-time": "^1.0.1", - "w3c-xmlserializer": "^1.1.2", - "webidl-conversions": "^4.0.2", - "whatwg-encoding": "^1.0.5", - "whatwg-mimetype": "^2.3.0", - "whatwg-url": "^7.0.0", - "ws": "^7.0.0", - "xml-name-validator": "^3.0.0" - } - }, "jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -6533,6 +6817,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, "requires": { "p-locate": "^4.1.0" } @@ -6632,18 +6917,18 @@ } }, "mime-db": { - "version": "1.44.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", - "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==", + "version": "1.47.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.47.0.tgz", + "integrity": "sha512-QBmA/G2y+IfeS4oktet3qRZ+P5kPhCKRXxXnQEudYqUaEioAU1/Lq2us3D/t1Jfo4hE9REQPrbB7K5sOczJVIw==", "dev": true }, "mime-types": { - "version": "2.1.27", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz", - "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==", + "version": "2.1.30", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.30.tgz", + "integrity": "sha512-crmjA4bLtR8m9qLpHvgxSChT+XoSlZi8J4n/aIdn3z92e/U47Z0V/yl+Wh9W046GgFVAmoNR/fmdbZYcSSIUeg==", "dev": true, "requires": { - "mime-db": "1.44.0" + "mime-db": "1.47.0" } }, "mimic-fn": { @@ -6947,6 +7232,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, "requires": { "p-try": "^2.0.0" } @@ -6955,6 +7241,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, "requires": { "p-limit": "^2.2.0" } @@ -6962,7 +7249,8 @@ "p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true }, "parent-module": { "version": "1.0.1", @@ -6999,7 +7287,8 @@ "path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true }, "path-is-absolute": { "version": "1.0.1", @@ -7074,6 +7363,12 @@ "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", "dev": true }, + "prettier": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.2.1.tgz", + "integrity": "sha512-PqyhM2yCjg/oKkFPtTGUojv7gnZAoG80ttl45O6x2Ug/rMJw4wcc9k6aaf2hibP7BGVCCM33gZoGjyvt9mm16Q==", + "dev": true + }, "pretty-format": { "version": "25.5.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-25.5.0.tgz", @@ -7323,21 +7618,21 @@ } }, "request-promise-core": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.3.tgz", - "integrity": "sha512-QIs2+ArIGQVp5ZYbWD5ZLCY29D5CfWizP8eWnm8FoGD1TX61veauETVQbrV60662V0oFBkrDOuaBI8XgtuyYAQ==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.4.tgz", + "integrity": "sha512-TTbAfBBRdWD7aNNOoVOBH4pN/KigV6LyapYNNlAPA8JwbovRti1E88m3sYAwsLi5ryhPKsE9APwnjFTgdUjTpw==", "dev": true, "requires": { - "lodash": "^4.17.15" + "lodash": "^4.17.19" } }, "request-promise-native": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.8.tgz", - "integrity": "sha512-dapwLGqkHtwL5AEbfenuzjTYg35Jd6KPytsC2/TLkVMz8rm+tNt72MGUWT1RP/aYawMpN6HqbNGBQaRcBtjQMQ==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.9.tgz", + "integrity": "sha512-wcW+sIUiWnKgNY0dqCpOZkUbF/I+YPi+f09JZIDa39Ec+q82CpSYniDp+ISgTTbKmnpJWASeJBPZmoxH84wt3g==", "dev": true, "requires": { - "request-promise-core": "1.1.3", + "request-promise-core": "1.1.4", "stealthy-require": "^1.1.1", "tough-cookie": "^2.3.3" }, @@ -7362,7 +7657,8 @@ "require-main-filename": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "dev": true }, "resolve": { "version": "1.17.0", @@ -7584,15 +7880,6 @@ } } }, - "saxes": { - "version": "3.1.11", - "resolved": "https://registry.npmjs.org/saxes/-/saxes-3.1.11.tgz", - "integrity": "sha512-Ydydq3zC+WYDJK1+gRxRapLIED9PWeSuuS41wqyoRmzvhhh9nc+QQrVMKJYzJFULazeGhzSV0QleN2wD3boh2g==", - "dev": true, - "requires": { - "xmlchars": "^2.1.1" - } - }, "semver": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", @@ -7602,7 +7889,8 @@ "set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "dev": true }, "set-value": { "version": "2.0.1", @@ -7642,6 +7930,12 @@ "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", "dev": true }, + "shell-quote": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.2.tgz", + "integrity": "sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg==", + "dev": true + }, "shellwords": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz", @@ -7997,9 +8291,9 @@ } }, "string-width": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", - "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", + "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", "requires": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -8272,26 +8566,6 @@ "is-number": "^7.0.0" } }, - "tough-cookie": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-3.0.1.tgz", - "integrity": "sha512-yQyJ0u4pZsv9D4clxO69OEjLWYw+jbgspjTue4lTQZLfV0c5l1VmK2y1JK8E9ahdpltPOaAThPcp5nKPUgSnsg==", - "dev": true, - "requires": { - "ip-regex": "^2.1.0", - "psl": "^1.1.28", - "punycode": "^2.1.1" - } - }, - "tr46": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", - "integrity": "sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - }, "tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -8521,17 +8795,6 @@ "browser-process-hrtime": "^1.0.0" } }, - "w3c-xmlserializer": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-1.1.2.tgz", - "integrity": "sha512-p10l/ayESzrBMYWRID6xbuCKh2Fp77+sA0doRuGn4tTIMrrZVeqfpKjXHY+oDh3K4nLdPgNwMTVP6Vp4pvqbNg==", - "dev": true, - "requires": { - "domexception": "^1.0.1", - "webidl-conversions": "^4.0.2", - "xml-name-validator": "^3.0.0" - } - }, "walker": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.7.tgz", @@ -8541,12 +8804,6 @@ "makeerror": "1.0.x" } }, - "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==", - "dev": true - }, "whatwg-encoding": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz", @@ -8562,17 +8819,6 @@ "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==", "dev": true }, - "whatwg-url": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", - "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", - "dev": true, - "requires": { - "lodash.sortby": "^4.7.0", - "tr46": "^1.0.1", - "webidl-conversions": "^4.0.2" - } - }, "which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -8585,7 +8831,8 @@ "which-module": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", - "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" + "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", + "dev": true }, "word-wrap": { "version": "1.2.3", @@ -8599,9 +8846,9 @@ "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=" }, "wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "requires": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -8646,9 +8893,9 @@ } }, "ws": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.3.0.tgz", - "integrity": "sha512-iFtXzngZVXPGgpTlP1rBqsUK82p9tKqsWRPg5L56egiljujJT3vGAYnHANvFxBieXrTFavhzhxW52jnaWV+w2w==", + "version": "7.4.5", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.5.tgz", + "integrity": "sha512-xzyu3hFvomRfXKH8vOFMU3OguG6oOvhXMo3xsGy3xWExqaM2dxBbVxuD99O7m3ZUFMvvscsZDqxfgMaRr/Nr1g==", "dev": true }, "xml-name-validator": { @@ -8664,36 +8911,35 @@ "dev": true }, "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==" + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==" }, "yargs": { - "version": "15.4.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", - "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.0.0.tgz", + "integrity": "sha512-gbtedDPfBgG40iLbaRXhqYJycUYqFVZQLIxl1cG5Ez/xZL/47TetSYzPSIixkWa36GKHr9D/o/oSG1vHXF4zTw==", "requires": { - "cliui": "^6.0.0", - "decamelize": "^1.2.0", - "find-up": "^4.1.0", - "get-caller-file": "^2.0.1", + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", "string-width": "^4.2.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^18.1.2" + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "dependencies": { + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" + } } }, "yargs-parser": { - "version": "18.1.3", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", - "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } + "version": "20.2.7", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.7.tgz", + "integrity": "sha512-FiNkvbeHzB/syOjIUxFDCnhSfzAL8R5vs40MgLFBorXACCOAEaWu0gRZl14vG8MR9AOJIZbmkjhusqBYZ3HTHw==" } } } diff --git a/package.json b/package.json index 3edc9e46..48a8afa9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jambo", - "version": "1.10.4", + "version": "1.11.0", "description": "A JAMStack implementation using Handlebars", "main": "index.js", "scripts": { @@ -38,20 +38,32 @@ "merge-options": "^2.0.0", "prompts": "^2.3.1", "simple-git": "^1.131.0", - "yargs": "^15.1.0" + "yargs": "^17.0.0" }, "devDependencies": { "@babel/plugin-transform-object-assign": "^7.12.1", + "@types/jest": "^26.0.23", "eslint": "^7.8.1", - "jest": "^25.4.0" + "jest": "^25.4.0", + "prettier": "^2.2.1", + "shell-quote": "^1.7.2" }, "jest": { + "collectCoverageFrom": [ + "src/**" + ], "verbose": true, + "setupFilesAfterEnv": [ + "./tests/setup/setup.js" + ], "testMatch": [ - "**/tests/**/*.js" + "**/tests/**/*.js", + "!**/tests/acceptance/**/*.js", + "**/tests/acceptance/suites/**/*.js" ], "testPathIgnorePatterns": [ - "/tests/fixtures/" + "/tests/fixtures/", + "/tests/setup/" ] } } diff --git a/src/buildJamboCLI.js b/src/buildJamboCLI.js new file mode 100644 index 00000000..312f8400 --- /dev/null +++ b/src/buildJamboCLI.js @@ -0,0 +1,69 @@ +const fs = require('file-system'); +const path = require('path'); +const { parseJamboConfig } = require('./utils/jamboconfigutils'); +const CommandRegistry = require('./commands/commandregistry'); +const YargsFactory = require('./yargsfactory'); +const CommandImporter = require('./commands/commandimporter'); + +/** + * @param {string[]} argv the argv for the current process + * @returns {import('yargs').Argv} A fully built Jambo CLI instance. + */ +module.exports = function buildJamboCLI(argv) { + const jamboConfig = fs.existsSync('jambo.json') && parseJamboConfig(); + const commandRegistry = new CommandRegistry(); + + if (argv.length < 3) { + console.error('You must provide Jambo with a command.'); + return; + } + + const shouldParseCustomCommands = + shouldImportCustomCommands(argv[2], commandRegistry); + const canParseCustomCommands = + jamboConfig && jamboConfig.dirs && jamboConfig.dirs.output; + + if (shouldParseCustomCommands && canParseCustomCommands) { + importCustomCommands(jamboConfig, commandRegistry); + } + + const yargsFactory = new YargsFactory(commandRegistry, jamboConfig); + return yargsFactory.createCLI(); +} + +/** + * Determines if custom {@link Command}s should be imported and added to the CLI instance. + * + * @param {string} invokedCommand The Jambo command that was invoked from the + * command line. + * @param {CommandRegistry} commandRegistry The registry containing all built-in commands. + * @returns {boolean} If custom {@link Command}s need to be added to the CLI instance. + */ +function shouldImportCustomCommands(invokedCommand, commandRegistry) { + const isCustomCommand = + !commandRegistry.getAliases().includes(invokedCommand) && + !invokedCommand.startsWith('--'); + + return invokedCommand === '--help' || + invokedCommand === 'describe' || + isCustomCommand; +} + +/** + * Imports custom commands from the Theme and the top-level of the site repository. + * The imported commands are added to the provided {@link CommandRegistry}. + * + * @param {Object} jamboConfig The site's parsed Jambo configuration. + * @param {CommandRegistry} commandRegistry The existing registry of built-in commands. + */ +function importCustomCommands(jamboConfig, commandRegistry) { + const commandImporter = jamboConfig.defaultTheme ? + new CommandImporter( + jamboConfig.dirs.output, + path.join(jamboConfig.dirs.themes, jamboConfig.defaultTheme)) : + new CommandImporter(jamboConfig.dirs.output); + + commandImporter.import().forEach(customCommand => { + commandRegistry.addCommand(customCommand) + }); +} diff --git a/src/cli.js b/src/cli.js index 1746852b..6ac50131 100755 --- a/src/cli.js +++ b/src/cli.js @@ -1,13 +1,6 @@ #!/usr/bin/env node - -const fs = require('file-system'); -const path = require('path'); - -const { parseJamboConfig } = require('./utils/jamboconfigutils'); const { exitWithError } = require('./utils/errorutils'); -const CommandRegistry = require('./commands/commandregistry'); -const YargsFactory = require('./yargsfactory'); -const CommandImporter = require('./commands/commandimporter'); +const buildJamboCLI = require('./buildJamboCLI'); // Exit with a non-zero exit code for unhandled rejections and uncaught exceptions process.on('unhandledRejection', err => { @@ -17,67 +10,5 @@ process.on('uncaughtException', err => { exitWithError(err); }); -buildJamboCLI(); - -/** - * @returns {Object} A fully built Jambo CLI instance. - */ -function buildJamboCLI() { - const jamboConfig = fs.existsSync('jambo.json') && parseJamboConfig(); - const commandRegistry = new CommandRegistry(); - - if (process.argv.length < 3) { - console.error('You must provide Jambo with a command.'); - return; - } - - const shouldParseCustomCommands = - shouldImportCustomCommands(process.argv[2], commandRegistry); - const canParseCustomCommands = - jamboConfig && jamboConfig.dirs && jamboConfig.dirs.output; - - if (shouldParseCustomCommands && canParseCustomCommands) { - importCustomCommands(jamboConfig, commandRegistry); - } - - const yargsFactory = new YargsFactory(commandRegistry, jamboConfig); - const options = yargsFactory.createCLI(); - return options.argv; -} - -/** - * Determines if custom {@link Command}s should be imported and added to the CLI instance. - * - * @param {string} invokedCommand The Jambo command that was invoked from the - * command line. - * @param {CommandRegistry} commandRegistry The registry containing all built-in commands. - * @returns {boolean} If custom {@link Command}s need to be added to the CLI instance. - */ -function shouldImportCustomCommands(invokedCommand, commandRegistry) { - const isCustomCommand = - !commandRegistry.getAliases().includes(invokedCommand) && - !invokedCommand.startsWith('--'); - - return invokedCommand === '--help' || - invokedCommand === 'describe' || - isCustomCommand; -} - -/** - * Imports custom commands from the Theme and the top-level of the site repository. - * The imported commands are added to the provided {@link CommandRegistry}. - * - * @param {Object} jamboConfig The site's parsed Jambo configuration. - * @param {CommandRegistry} commandRegistry The existing registry of built-in commands. - */ -function importCustomCommands(jamboConfig, commandRegistry) { - const commandImporter = jamboConfig.defaultTheme ? - new CommandImporter( - jamboConfig.dirs.output, - path.join(jamboConfig.dirs.themes, jamboConfig.defaultTheme)) : - new CommandImporter(jamboConfig.dirs.output); - - commandImporter.import().forEach(customCommand => { - commandRegistry.addCommand(customCommand) - }); -} +const jambo = buildJamboCLI(process.argv); +jambo && jambo.parse(); diff --git a/src/commands/build/buildcommand.js b/src/commands/build/buildcommand.js index b46f2c4c..451dfc4e 100644 --- a/src/commands/build/buildcommand.js +++ b/src/commands/build/buildcommand.js @@ -36,9 +36,9 @@ class BuildCommand { } } - execute(args) { + async execute(args) { try { - this.sitesGenerator.generate(args.jsonEnvVars); + await this.sitesGenerator.generate(args.jsonEnvVars); } catch (err) { if (isCustomError(err)) { throw err; diff --git a/src/commands/import/themeimporter.js b/src/commands/import/themeimporter.js index c1fe3c40..be50c13c 100644 --- a/src/commands/import/themeimporter.js +++ b/src/commands/import/themeimporter.js @@ -2,7 +2,8 @@ const path = require('path'); const simpleGit = require('simple-git/promise'); const git = simpleGit(); const { ThemeShadower } = require('../override/themeshadower'); -const { getRepoForTheme } = require('../../utils/gitutils'); +const ThemeManager = require('../../utils/thememanager'); +const { getRepoNameFromURL } = require('../../utils/gitutils'); const SystemError = require('../../errors/systemerror'); const UserError = require('../../errors/usererror'); const { isCustomError } = require('../../utils/errorutils'); @@ -10,6 +11,8 @@ const { ArgumentMetadata, ArgumentType } = require('../../models/commands/argume const { CustomCommand } = require('../../utils/customcommands/command'); const { CustomCommandExecuter } = require('../../utils/customcommands/commandexecuter'); const { searchDirectoryIgnoringExtensions } = require('../../utils/fileutils'); +const fsExtra = require('fs-extra'); +const process = require('process'); /** * ThemeImporter imports a specified theme into the themes directory. @@ -31,48 +34,46 @@ class ThemeImporter { static args() { return { + themeUrl: new ArgumentMetadata({ + type: ArgumentType.STRING, + description: 'url of the theme\'s git repo', + }), theme: new ArgumentMetadata({ type: ArgumentType.STRING, - description: 'theme to import', - isRequired: true + description: '(deprecated: specify the themeUrl instead)' + + ' the name of the theme to import', }), - addAsSubmodule: new ArgumentMetadata({ + useSubmodules: new ArgumentMetadata({ type: ArgumentType.BOOLEAN, - description: 'import the theme as a submodule', - defaultValue: true + description: 'import the theme as a submodule' }), } } static describe() { - const importableThemes = this._getImportableThemes(); + const importableThemes = ThemeManager.getKnownThemes(); return { displayName: 'Import Theme', params: { + themeUrl: { + displayName: 'URL', + type: 'string', + }, theme: { displayName: 'Theme', type: 'singleoption', - required: true, options: importableThemes }, - addAsSubmodule: { - displayName: 'Add as Submodule', - type: 'boolean', - default: true + useSubmodules: { + displayName: 'Use Submodules', + type: 'boolean' } } } } - /** - * @returns {Array} the names of the available themes to be imported - */ - static _getImportableThemes() { - return ['answers-hitchhiker-theme']; - } - - execute(args) { - this.import(args.theme, args.addAsSubmodule) + async execute(args) { + await this.import(args.themeUrl, args.theme, args.useSubmodules) .then(console.log); } @@ -80,24 +81,31 @@ class ThemeImporter { * Imports the requested theme into Jambo's Themes directory. Note that the theme can * either be cloned directly into this directory or added there as a submodule. * - * @param {string} themeName The name of the theme - * @param {boolean} addAsSubmodule If the theme should be imported as a submodule. + * @param {string} themeUrl The URL of the theme to import. Takes precedence over the + * 'themeName' param. + * @param {string} themeName The name of a known theme. + * @param {boolean} useSubmodules If the theme should be imported as a submodule. * @returns {Promise} If the addition of the submodule was successful, a Promise * containing the new submodule's local path. If the addition * failed, a Promise containing the error. */ - async import(themeName, addAsSubmodule) { + async import(themeUrl, themeName, useSubmodules) { if (!this.config) { throw new UserError('No jambo.json found. Did you `jambo init` yet?'); } + if (!themeUrl && !themeName) { + throw new UserError('A URL or a theme must be specifed for an import'); + } try { - const themeRepo = getRepoForTheme(themeName); - const themePath = path.join(this.config.dirs.themes, themeName); - - if (addAsSubmodule) { + const themeRepo = themeUrl || ThemeManager.getRepoForTheme(themeName); + const themeRepoName = themeUrl ? getRepoNameFromURL(themeUrl) : themeName; + const themePath = path.join(this.config.dirs.themes, themeRepoName); + await git.cwd(process.cwd()); + if (useSubmodules) { await git.submoduleAdd(themeRepo, themePath); } else { await git.clone(themeRepo, themePath); + this._removeGitFolder(themePath); } this._postImport(themePath); @@ -110,6 +118,15 @@ class ThemeImporter { } } + /** + * Removes the .git folder from the theme. + * + * @param {string} themePath + */ + _removeGitFolder(themePath) { + fsExtra.removeSync(path.join(themePath, '.git')); + } + /** * Run the post import hook, if one exists. * diff --git a/src/commands/init/initcommand.js b/src/commands/init/initcommand.js index 1e833aba..3c6b9a45 100644 --- a/src/commands/init/initcommand.js +++ b/src/commands/init/initcommand.js @@ -1,5 +1,6 @@ const { RepositorySettings, RepositoryScaffolder } = require('./repositoryscaffolder'); const { ArgumentMetadata, ArgumentType } = require('../../models/commands/argumentmetadata'); +const ThemeManager = require('../../utils/thememanager'); /** * InitCommand initializes the current directory as a Jambo repository. @@ -15,49 +16,49 @@ class InitCommand { static args() { return { + themeUrl: new ArgumentMetadata({ + type: ArgumentType.STRING, + description: 'url of a theme\'s git repo to import during the init', + }), theme: new ArgumentMetadata({ type: ArgumentType.STRING, - description: 'a starter theme', + description: '(deprecated: specify the themeUrl instead)' + + ' the name of a theme to import during the init', isRequired: false }), - addThemeAsSubmodule: new ArgumentMetadata({ + useSubmodules: new ArgumentMetadata({ type: ArgumentType.BOOLEAN, - description: 'if starter theme should be imported as submodule', - defaultValue: true + description: 'if starter theme should be imported as submodule' }), } } static describe() { - const importableThemes = this._getImportableThemes(); + const importableThemes = ThemeManager.getKnownThemes(); return { displayName: 'Initialize Jambo', params: { + themeUrl: { + displayName: 'URL', + type: 'string', + }, theme: { displayName: 'Theme', type: 'singleoption', options: importableThemes }, - addThemeAsSubmodule: { - displayName: 'Add Theme as Submodule', - type: 'boolean', - default: true + useSubmodules: { + displayName: 'Use Submodules', + type: 'boolean' } } } } - /** - * @returns {Array} the names of the available themes to be imported - */ - static _getImportableThemes() { - return ['answers-hitchhiker-theme']; - } - - execute(args) { + async execute(args) { const repositorySettings = new RepositorySettings(args); const repositoryScaffolder = new RepositoryScaffolder(); - repositoryScaffolder.create(repositorySettings); + await repositoryScaffolder.create(repositorySettings); } } diff --git a/src/commands/init/repositoryscaffolder.js b/src/commands/init/repositoryscaffolder.js index e7470d0f..1c1a252e 100644 --- a/src/commands/init/repositoryscaffolder.js +++ b/src/commands/init/repositoryscaffolder.js @@ -1,28 +1,34 @@ const ThemeImporter = require('../import/themeimporter'); const fs = require('file-system'); +const process = require('process'); const simpleGit = require('simple-git/promise'); const SystemError = require('../../errors/systemerror'); const git = simpleGit(); /** * RepositorySettings contains the information needed by Jambo to scaffold a new site - * repository. Currently, these settings include an optional theme and whether or not - * the theme should be imported as a submodule. + * repository. Currently, these settings include an optional themeUrl, theme name, and + * whether or not the theme should be imported as a submodule. */ exports.RepositorySettings = class { - constructor({ theme, addThemeAsSubmodule, includeTranslations }) { + constructor({ themeUrl, theme, useSubmodules, includeTranslations }) { + this._themeUrl = themeUrl; this._theme = theme; - this._addThemeAsSubmodule = addThemeAsSubmodule; + this._useSubmodules = useSubmodules; this._includeTranslations = includeTranslations; } + getThemeUrl() { + return this._themeUrl; + } + getTheme() { return this._theme; } - shouldAddThemeAsSubmodule() { - return this._addThemeAsSubmodule; + shouldUseSubmodules() { + return this._useSubmodules; } shouldIncludeTranslations() { @@ -41,6 +47,8 @@ exports.RepositoryScaffolder = class { */ async create(repositorySettings) { try { + const cwd = process.cwd(); + await git.cwd(cwd); await git.init(); fs.writeFileSync('.gitignore', 'public/\nnode_modules/\n'); @@ -49,12 +57,14 @@ exports.RepositoryScaffolder = class { this._createDirectorySkeleton(includeTranslations); const jamboConfig = this._createJamboConfig(includeTranslations); + const themeUrl = repositorySettings.getThemeUrl(); const theme = repositorySettings.getTheme(); - if (theme) { + if (themeUrl || theme) { const themeImporter = new ThemeImporter(jamboConfig); await themeImporter.import( + themeUrl, theme, - repositorySettings.shouldAddThemeAsSubmodule()); + repositorySettings.shouldUseSubmodules()); } } catch (err) { throw new SystemError(err.message, err.stack); diff --git a/src/commands/upgrade/themeupgrader.js b/src/commands/upgrade/themeupgrader.js index c842470b..96474710 100644 --- a/src/commands/upgrade/themeupgrader.js +++ b/src/commands/upgrade/themeupgrader.js @@ -2,7 +2,7 @@ const fs = require('fs-extra'); const path = require('path'); const simpleGit = require('simple-git/promise'); -const { getRepoForTheme } = require('../../utils/gitutils'); +const ThemeManager = require('../../utils/thememanager'); const { CustomCommand } = require('../../utils/customcommands/command'); const { CustomCommandExecuter } = require('../../utils/customcommands/commandexecuter'); const { ArgumentMetadata, ArgumentType } = require('../../models/commands/argumentmetadata'); @@ -10,6 +10,7 @@ const SystemError = require('../../errors/systemerror'); const UserError = require('../../errors/systemerror'); const { isCustomError } = require('../../utils/errorutils'); const { searchDirectoryIgnoringExtensions } = require('../../utils/fileutils'); +const fsExtra = require('fs-extra'); const git = simpleGit(); @@ -102,9 +103,13 @@ class ThemeUpgrader { throw new UserError( `Theme "${themeName}" not found within the "${this._themesDir}" folder`); } - await this._isGitSubmodule(themePath) - ? await this._upgradeSubmodule(themePath, branch) - : await this._recloneTheme(themeName, themePath, branch); + + if (await this._isGitSubmodule(themePath)) { + await this._upgradeSubmodule(themePath, branch) + } else { + await this._recloneTheme(themeName, themePath, branch); + this._removeGitFolder(themePath); + } if (!disableScript) { this._executePostUpgradeScript(themePath, isLegacy); } @@ -119,6 +124,15 @@ class ThemeUpgrader { } } + /** + * Removes the .git folder from the theme. + * + * @param {string} themePath + */ + _removeGitFolder(themePath) { + fsExtra.removeSync(path.join(themePath, '.git')); + } + /** * Executes the upgrade script, and outputs its stdout and stderr. * @param {string} themePath path to the default theme @@ -159,7 +173,7 @@ class ThemeUpgrader { */ async _recloneTheme(themeName, themePath, branch) { await fs.remove(themePath); - const themeRepoURL = getRepoForTheme(themeName); + const themeRepoURL = ThemeManager.getRepoForTheme(themeName); const updateBranch = branch || 'master'; await git.clone(themeRepoURL, themePath, ['--branch', updateBranch]); } diff --git a/src/handlebars/handlebarspreprocessor.js b/src/handlebars/handlebarspreprocessor.js index 7a938963..0712a3d7 100644 --- a/src/handlebars/handlebarspreprocessor.js +++ b/src/handlebars/handlebarspreprocessor.js @@ -1,5 +1,6 @@ const TranslateInvocation = require('./models/translateinvocation'); -const Handlebars = require('handlebars'); +const InvocationTranspiler = require('./invocationtranspiler'); +const HbsHelperParser = require('./hbshelperparser'); /** * This class performs preprocessing on Handlebars content before it is registered @@ -8,7 +9,7 @@ const Handlebars = require('handlebars'); */ class HandlebarsPreprocessor { constructor(translator) { - this._translator = translator; + this._invocationTranspiler = new InvocationTranspiler(translator); } /** @@ -21,170 +22,23 @@ class HandlebarsPreprocessor { */ process(handlebarsContent) { let processedHandlebarsContent = handlebarsContent; - const translateHelperCalls = - processedHandlebarsContent.match(/\{\{\s?translate(JS)?\s(.+?)\}\}/g) || []; - - translateHelperCalls.forEach(call => { - const translateInvocation = TranslateInvocation.from(call); - const transpiledCall = this._handleTranslateInvocation(translateInvocation); - processedHandlebarsContent = processedHandlebarsContent.replace( - call, transpiledCall); - }); - - return processedHandlebarsContent; - } - - /** - * Transpiles a usage of the 'translate' or 'translateJS' helper. If the - * translation can be resolved at compile-time (no pluralization or interpolation), - * the usage will be transpiled to it. Otherwise, it will be transpiled to the - * appropriate call to the SDK's run-time translation method. - * - * @param {TranslateInvocation} invocation The {@link TranslateInvocation} - * representaiton of the usage. - * @returns {string} The transpiled result. - */ - _handleTranslateInvocation(invocation) { - let translatorResult; - const translationContext = invocation.getContext(); - if (invocation.isUsingPluralization()) { - translatorResult = translationContext ? - this._translator.translatePluralWithContext( - invocation.getPhrase(), - invocation.getPluralForm(), - translationContext): - this._translator.translatePlural( - invocation.getPhrase(), - invocation.getPluralForm()); - } else { - translatorResult = translationContext ? - this._translator.translateWithContext( - invocation.getPhrase(), translationContext) : - this._translator.translate(invocation.getPhrase()); - } - - if (invocation.canBeTranslatedStatically()) { - if (invocation.getInvokedHelper() === 'translateJS' ) { - const escapedTranslatorResult = this._escapeSingleQuotes(translatorResult); - return `'${escapedTranslatorResult}'`; - } - - if (invocation.shouldEscapeHTML()) { - return Handlebars.Utils.escapeExpression(translatorResult); - } - - return translatorResult; - } - const interpParams = invocation.getInterpolationParams(); - - return invocation.getInvokedHelper() === 'translateJS' ? - this._createRuntimeCallForJS( - translatorResult, - interpParams, - invocation.isUsingPluralization()) : - this._createRuntimeCallForHBS( - translatorResult, - interpParams, - invocation.isUsingPluralization(), - invocation.shouldEscapeHTML()); - } - - /** - * Constructs a call to the SDK's Javascript method for run-time translation - * processing. This call is constructed using the translation(s) for a phrase and any - * interpolation parameters. - * - * @param {Object|string} translatorResult The translation(s) for the phrase. - * @param {Object} interpolationParams The needed interpolation parameters - * (including 'count'). - * @param {boolean} needsPluralization If pluralization is required when translating. - * @returns {string} The call to ANSWERS.processTranslation. - */ - _createRuntimeCallForJS(translatorResult, interpolationParams, needsPluralization) { - let parsedParams = JSON.stringify(interpolationParams); - parsedParams = parsedParams.replace(/[\'\"]/g, ''); - - if (needsPluralization) { - const count = interpolationParams.count; - const pluralForms = this._getFormattedPluralForms(translatorResult); - - return `ANSWERS.processTranslation(${pluralForms}, ${parsedParams}, ${count})`; + try { + const translateHelperCalls = + new HbsHelperParser(['translate', 'translateJS']).parse(handlebarsContent); + + translateHelperCalls.forEach(call => { + const translateInvocation = TranslateInvocation.from(call); + const transpiledCall = this._invocationTranspiler.transpile(translateInvocation); + processedHandlebarsContent = processedHandlebarsContent.replace( + call, transpiledCall); + }); + } catch(err) { + // If we run into a file type that the Handlebars parser cannot understand, like a + // .woff file, fail silently. This is to maintain backwards compatibility with + // usages of older versions of Jambo, where we were using a regex to identify + // translateHelperCalls instead of the Handlebars parser. } - const escapedTranslatorResult = this._escapeSingleQuotes(translatorResult); - - return `ANSWERS.processTranslation('${escapedTranslatorResult}', ${parsedParams})`; - } - - /** - * Constructs a string representation of a translatorResult Object. This output is - * similar to JSON.stringiy(), however keys are not surrounded by quotes, and values - * are surrounded by single quotes. - * - * @param {Object} translatorResult - * @returns {string} - */ - _getFormattedPluralForms(translatorResult) { - const pluralFormPairs = Object.entries(translatorResult) - .reduce((params, [pluralFormIndex, pluralForm], index, array) => { - const escapedPluralForm = this._escapeSingleQuotes(pluralForm); - const accumulatedParams = params + `${pluralFormIndex}:'${escapedPluralForm}'`; - const isLastParam = (index === array.length-1); - - return isLastParam ? - accumulatedParams : - accumulatedParams + ','; - }, ''); - - return '{' + pluralFormPairs + '}'; - } - - /** - * Constructs a call to the SDK's Handlebars helper for run-time translation - * processing. This call is constructed using the translation(s) for a phrase and any - * interpolation parameters. - * - * @param {Object|string} translatorResult The translation(s) for the phrase. - * @param {Object} interpolationValues The needed interpolation parameters - * (including 'count'). - * @param {boolean} needsPluralization If pluralization is required when translating. - * @param {boolean} shouldEscapeHTML If HTML should be escaped. If false, wrap the call - * in triple curly braces. If true, wrap in in double - * double curly braces. - * @returns {string} The call to the 'processTranslation' helper. - */ - _createRuntimeCallForHBS( - translatorResult, - interpolationValues, - needsPluralization, - shouldEscapeHTML) - { - const translationParams = needsPluralization ? - Object.entries(translatorResult) - .reduce((params, [paramName, paramValue]) => { - paramValue = this._escapeSingleQuotes(paramValue); - return params + `pluralForm${paramName}='${paramValue}' `; - }, '') : - `phrase='${this._escapeSingleQuotes(translatorResult)}'`; - - const interpolationParams = Object.entries(interpolationValues) - .reduce((params, [paramName, paramValue]) => { - return params + `${paramName}=${paramValue} `; - }, ''); - - return shouldEscapeHTML ? - `{{ processTranslation ${translationParams} ${interpolationParams}}}` : - `{{{ processTranslation ${translationParams} ${interpolationParams}}}}` - } - - /** - * Escape single quotes in the string - * @param {string} str - * - * @returns {string} - */ - _escapeSingleQuotes(str) { - const regex = new RegExp('\'', 'g'); - return str.replace(regex, '\\\''); + return processedHandlebarsContent; } } module.exports = HandlebarsPreprocessor; \ No newline at end of file diff --git a/src/handlebars/hbshelperparser.js b/src/handlebars/hbshelperparser.js new file mode 100644 index 00000000..85780fab --- /dev/null +++ b/src/handlebars/hbshelperparser.js @@ -0,0 +1,104 @@ +const Handlebars = require('handlebars'); + +/** + * InvocationExtractor takes a handlebars template, and an array of + * requested hbs helpers, and parses out an array of all + * hbs helpers found of the requested types. + */ +class HbsHelperParser { + constructor(helpersToParse) { + /** + * The helpers that should be parsed out. + * @type {string[]} + */ + this.helpersToParse = helpersToParse; + + /** + * @type {Handlebars.Visitor} + */ + this.vistor = new Handlebars.Visitor(); + + /** + * _handleMustacheStatement() is dispatched on all MustacheStatement nodes. + * @type {Function} + */ + this.vistor.MustacheStatement = this._handleMustacheStatement.bind(this); + + /** + * Helper statements found in a template. + * @type {hbs.AST.MustacheStatement[]} + */ + this.helperStatements = []; + } + + /** + * Parses requested hbs helpers from a handlebars template. + * + * @param {string} template a handlebars template + * @return {string[]} + */ + parse(template) { + this.helperStatements = []; + const ast = Handlebars.parse(template); + this.vistor.accept(ast); + const lineEndIndices = this._getLineEndIndices(template); + return this.helperStatements.map(statement => + this._getOriginalHelperCall(statement, lineEndIndices, template)); + } + + /** + * _handleMustacheStatement() is called on any MustacheStatement nodes by this.visitor. + * For MustacheStatements that are recognized as requested helpers, push them onto + * the helper statement accumulator for later usage. + * + * @param {hbs.AST.MustacheStatement} statement + */ + _handleMustacheStatement(statement) { + const isRequestedHelper = this.helpersToParse.includes(statement.path.original); + if (!isRequestedHelper) { + return; + } + this.helperStatements.push(statement); + } + + /** + * Returns the end index of every line in the template. + * For example, a template like "hi\n hello\n bye" would return + * [3, 10, 14], which are the indices corresponding to the end + * of each line. + * + * @param {string} template + * @returns {number[]} + */ + _getLineEndIndices(template) { + const splitTemplate = template.split('\n'); + const indices = []; + splitTemplate.forEach((line, i) => { + const previousIndex = i == 0 ? 0 : indices[i - 1]; + const isLastLine = i === splitTemplate.length - 1; + const currentLineLength = isLastLine ? line.length : line.length + 1; + indices.push(previousIndex + currentLineLength); + }); + return indices; + } + + /** + * Gets the original value for a given MustacheStatement. + * + * @param {hbs.AST.MustacheStatement} statement the statement to get the original of + * @param {number[]} lineEndIndices the line end indices of the original template + * @param {string} template the original template + */ + _getOriginalHelperCall(statement, lineEndIndices, template) { + const getIndex = ({ line, column }) => { + const lineStart = line === 1 ? 0 : lineEndIndices[line - 2]; + return lineStart + column; + } + const { start, end } = statement.loc; + const startIndex = getIndex(start); + const endIndex = getIndex(end); + return template.substring(startIndex, endIndex); + } +} + +module.exports = HbsHelperParser; diff --git a/src/handlebars/invocationtranspiler.js b/src/handlebars/invocationtranspiler.js new file mode 100644 index 00000000..761a134d --- /dev/null +++ b/src/handlebars/invocationtranspiler.js @@ -0,0 +1,170 @@ +const Handlebars = require('handlebars'); + +/** + * InvocationTranspiler is responsible for taking an instance of + * {@link TranslateInvocation}, and changing it into a static translation, + * if possible, or a runtime translation helper if not. + */ +class InvocationTranspiler { + constructor(translator) { + /** + * @type {Translator} + */ + this._translator = translator; + } + + /** + * Transpiles a usage of the 'translate' or 'translateJS' helper. If the + * translation can be resolved at compile-time (no pluralization or interpolation), + * the usage will be transpiled to it. Otherwise, it will be transpiled to the + * appropriate call to the SDK's run-time translation method. + * + * @param {TranslateInvocation} invocation The {@link TranslateInvocation} + * representaiton of the usage. + * @returns {string} The transpiled result. + */ + transpile(invocation) { + let translatorResult; + const translationContext = invocation.getContext(); + if (invocation.isUsingPluralization()) { + translatorResult = translationContext ? + this._translator.translatePluralWithContext( + invocation.getPhrase(), + invocation.getPluralForm(), + translationContext) : + this._translator.translatePlural( + invocation.getPhrase(), + invocation.getPluralForm()); + } else { + translatorResult = translationContext ? + this._translator.translateWithContext( + invocation.getPhrase(), translationContext) : + this._translator.translate(invocation.getPhrase()); + } + + if (invocation.canBeTranslatedStatically()) { + if (invocation.getInvokedHelper() === 'translateJS') { + const escapedTranslatorResult = this._escapeSingleQuotes(translatorResult); + return `'${escapedTranslatorResult}'`; + } + + if (invocation.shouldEscapeHTML()) { + return Handlebars.Utils.escapeExpression(translatorResult); + } + + return translatorResult; + } + const interpParams = invocation.getInterpolationParams(); + + return invocation.getInvokedHelper() === 'translateJS' ? + this._createRuntimeCallForJS( + translatorResult, + interpParams, + invocation.isUsingPluralization()) : + this._createRuntimeCallForHBS( + translatorResult, + interpParams, + invocation.isUsingPluralization(), + invocation.shouldEscapeHTML()); + } + + /** + * Constructs a call to the SDK's Javascript method for run-time translation + * processing. This call is constructed using the translation(s) for a phrase and any + * interpolation parameters. + * + * @param {Object|string} translatorResult The translation(s) for the phrase. + * @param {Object} interpolationParams The needed interpolation parameters + * (including 'count'). + * @param {boolean} needsPluralization If pluralization is required when translating. + * @returns {string} The call to ANSWERS.processTranslation. + */ + _createRuntimeCallForJS(translatorResult, interpolationParams, needsPluralization) { + let parsedParams = JSON.stringify(interpolationParams); + parsedParams = parsedParams.replace(/[\'\"]/g, ''); + + if (needsPluralization) { + const count = interpolationParams.count; + const pluralForms = this._getFormattedPluralForms(translatorResult); + + return `ANSWERS.processTranslation(${pluralForms}, ${parsedParams}, ${count})`; + } + const escapedTranslatorResult = this._escapeSingleQuotes(translatorResult); + + return `ANSWERS.processTranslation('${escapedTranslatorResult}', ${parsedParams})`; + } + + + /** + * Constructs a string representation of a translatorResult Object. This output is + * similar to JSON.stringiy(), however keys are not surrounded by quotes, and values + * are surrounded by single quotes. + * + * @param {Object} translatorResult + * @returns {string} + */ + _getFormattedPluralForms(translatorResult) { + const pluralFormPairs = Object.entries(translatorResult) + .reduce((params, [pluralFormIndex, pluralForm], index, array) => { + const escapedPluralForm = this._escapeSingleQuotes(pluralForm); + const accumulatedParams = params + `${pluralFormIndex}:'${escapedPluralForm}'`; + const isLastParam = (index === array.length-1); + + return isLastParam ? + accumulatedParams : + accumulatedParams + ','; + }, ''); + + return '{' + pluralFormPairs + '}'; + } + + /** + * Constructs a call to the SDK's Handlebars helper for run-time translation + * processing. This call is constructed using the translation(s) for a phrase and any + * interpolation parameters. + * + * @param {Object|string} translatorResult The translation(s) for the phrase. + * @param {Object} interpolationValues The needed interpolation parameters + * (including 'count'). + * @param {boolean} needsPluralization If pluralization is required when translating. + * @param {boolean} shouldEscapeHTML If HTML should be escaped. If false, wrap the call + * in triple curly braces. If true, wrap in in double + * double curly braces. + * @returns {string} The call to the 'processTranslation' helper. + */ + _createRuntimeCallForHBS( + translatorResult, + interpolationValues, + needsPluralization, + shouldEscapeHTML) + { + const translationParams = needsPluralization ? + Object.entries(translatorResult) + .reduce((params, [paramName, paramValue]) => { + paramValue = this._escapeSingleQuotes(paramValue); + return params + `pluralForm${paramName}='${paramValue}' `; + }, '') : + `phrase='${this._escapeSingleQuotes(translatorResult)}'`; + + const interpolationParams = Object.entries(interpolationValues) + .reduce((params, [paramName, paramValue]) => { + return params + `${paramName}=${paramValue} `; + }, ''); + + return shouldEscapeHTML ? + `{{ processTranslation ${translationParams} ${interpolationParams}}}` : + `{{{ processTranslation ${translationParams} ${interpolationParams}}}}` + } + + /** + * Escape single quotes in the string + * @param {string} str + * + * @returns {string} + */ + _escapeSingleQuotes(str) { + const regex = new RegExp('\'', 'g'); + return str.replace(regex, '\\\''); + } +} +module.exports = InvocationTranspiler; \ No newline at end of file diff --git a/src/utils/fileutils.js b/src/utils/fileutils.js index c28dc253..04e4f951 100644 --- a/src/utils/fileutils.js +++ b/src/utils/fileutils.js @@ -1,5 +1,6 @@ const fs = require('fs'); const path = require('path'); +const process = require('process'); /** * Returns the given filename without its extension @@ -43,7 +44,13 @@ exports.isValidFile = isValidFile; * @returns {boolean} */ isValidPartialPath = function(path) { - return path && !path.startsWith('node_modules/') && !path.includes('/node_modules/'); + if (!path) { + return false; + } + const invalidPaths = ['node_modules', '.git']; + return invalidPaths.every(invalidPath => { + return !path.startsWith(`${invalidPath}/`) && !path.includes(`/${invalidPath}/`); + }); } exports.isValidPartialPath = isValidPartialPath; @@ -61,7 +68,7 @@ searchDirectoryIgnoringExtensions = function(desiredFile, directoryPath) { const dirEntries = fs.readdirSync(directoryPath); for (const dirEntry of dirEntries) { if (desiredFile === stripExtension(dirEntry)) { - const filePath = path.resolve(directoryPath, dirEntry); + const filePath = path.resolve(process.cwd(), directoryPath, dirEntry); if (fs.lstatSync(filePath).isFile()) { return dirEntry; } diff --git a/src/utils/gitutils.js b/src/utils/gitutils.js index 482f021e..400e3a22 100644 --- a/src/utils/gitutils.js +++ b/src/utils/gitutils.js @@ -1,13 +1,9 @@ -const UserError = require('../errors/usererror') +const path = require('path'); + /** - * Gets the git repo URL for the given theme name. - * @param {string} themeName + * Gets the repo name from a git repo URL + * @param {string} repoURL */ -exports.getRepoForTheme = function(themeName) { - switch (themeName) { - case 'answers-hitchhiker-theme': - return 'https://github.com/yext/answers-hitchhiker-theme.git'; - default: - throw new UserError('Unrecognized theme'); - } +exports.getRepoNameFromURL = function(repoURL) { + return path.basename(repoURL, '.git'); } diff --git a/src/utils/thememanager.js b/src/utils/thememanager.js new file mode 100644 index 00000000..87e227c8 --- /dev/null +++ b/src/utils/thememanager.js @@ -0,0 +1,43 @@ +const UserError = require('../errors/usererror') + +const ThemeRepos = { + 'answers-hitchhiker-theme': 'https://github.com/yext/answers-hitchhiker-theme.git' +} + +/** + * Responsible for providing information about themes known by Jambo + */ +class ThemeManager { + /** + * Returns true if a theme name is known by Jambo + * + * @param {string} themeName + * @returns {boolean} + */ + static isThemeKnown(themeName) { + return (themeName in ThemeRepos); + } + + /** + * Returns an array of known themes + * @returns {string[]} + */ + static getKnownThemes() { + return Object.keys(ThemeRepos); + } + + /** + * Gets the repo for a given theme name + * + * @param {string} themeName The URL to a theme, or the name of a known theme. + * @returns + */ + static getRepoForTheme(themeName) { + if (!this.isThemeKnown(themeName)) { + throw new UserError(`The theme ${themeName} is not known by Jambo.`); + } + return ThemeRepos[themeName]; + } +} + +module.exports = ThemeManager; \ No newline at end of file diff --git a/src/yargsfactory.js b/src/yargsfactory.js index b79064e7..e8019959 100644 --- a/src/yargsfactory.js +++ b/src/yargsfactory.js @@ -3,7 +3,6 @@ const PageScaffolder = require('./commands/page/add/pagescaffolder'); const SitesGenerator = require('./commands/build/sitesgenerator'); const { ArgumentMetadata, ArgumentType } = require('./models/commands/argumentmetadata'); - /** * Creates the {@link yargs} instance that powers the Jambo CLI. */ @@ -16,6 +15,8 @@ class YargsFactory { /** * Generates a {@link yargs} instance with all of the built-in and custom * commands known to Jambo. + * + * @returns {import('yargs').Argv} */ createCLI() { const cli = yargs.usage('Usage: $0 [options]'); @@ -23,7 +24,7 @@ class YargsFactory { this._commandRegistry.getCommands().forEach(commandClass => { cli.command(this._createCommandModule(commandClass)); }); - cli.strict() + cli.strict(); return cli; } @@ -52,9 +53,9 @@ class YargsFactory { } }); }, - handler: argv => { + handler: async argv => { const commandInstance = this._createCommandInstance(commandClass); - commandInstance.execute(argv); + await commandInstance.execute(argv); } } } @@ -65,7 +66,7 @@ class YargsFactory { * * @param {string} name The name of the option. * @param {ArgumentMetadata} metadata The option's {@link ArgumentMetadata}. - * @param {Object} yargs The Yargs instance to modify. + * @param {import('yargs').Argv} yargs The Yargs instance to modify. */ _addListOption(name, metadata, yargs) { yargs.array(name); diff --git a/tests/acceptance/fixtures/basic-flow/index.html b/tests/acceptance/fixtures/basic-flow/index.html new file mode 100644 index 00000000..9577330d --- /dev/null +++ b/tests/acceptance/fixtures/basic-flow/index.html @@ -0,0 +1,10 @@ + +
+
\ No newline at end of file diff --git a/tests/acceptance/setup/TestInstance.js b/tests/acceptance/setup/TestInstance.js new file mode 100644 index 00000000..4d0ff8dc --- /dev/null +++ b/tests/acceptance/setup/TestInstance.js @@ -0,0 +1,22 @@ +const buildJamboCLI = require('../../../src/buildJamboCLI'); +const { parse } = require('shell-quote'); + +/** + * TestInstance gives Jambo acceptance tests different ways + * to interact with the testing playground. + */ +module.exports = class TestInstance { + async jambo(command) { + const commandArgs = parse(command); + const argv = [process.argv[0], process.argv[1], ...commandArgs]; + + return buildJamboCLI(argv) + .scriptName('jambo') + .fail(function(msg, err) { + console.error('Error running command:', command); + if (err) throw err + }) + .exitProcess(false) + .parseAsync(commandArgs); + } +} \ No newline at end of file diff --git a/tests/acceptance/setup/playground.js b/tests/acceptance/setup/playground.js new file mode 100644 index 00000000..c8601fbd --- /dev/null +++ b/tests/acceptance/setup/playground.js @@ -0,0 +1,71 @@ +const path = require('path'); +const fs = require('fs'); +const fsExtra = require('fs-extra'); +const { chdir, cwd } = require('process'); +const TestInstance = require('./TestInstance'); +const simpleGit = require('simple-git/promise'); + +/** + * Transform all theme folders under test-themes/ into git repos. + */ +async function setupTestThemes() { + async function initGitRepo(dir) { + const originalDir = cwd(); + chdir(dir); + const git = simpleGit(dir) + await git.init(); + await git.add('-A'); + await git.commit('init test theme'); + chdir(originalDir); + } + const testThemesDir = path.resolve(__dirname, '../test-themes'); + const testThemes = fs.readdirSync(testThemesDir); + for (const themeName of testThemes) { + await initGitRepo(path.resolve(testThemesDir, themeName)); + } +} + +/** + * Transform all theme folders under test-themes back into regular folders, + * so they can be tracked properly by jambo's git repo. + */ +function cleanupTestThemes() { + const testThemesDir = path.resolve(__dirname, '../test-themes'); + const testThemes = fs.readdirSync(testThemesDir); + testThemes.forEach(themeName => { + const themeGitFolder = path.resolve(testThemesDir, themeName, '.git'); + fsExtra.removeSync(themeGitFolder); + }); +} + +/** + * Runs a test suite in a playground-[id] folder. + * + * @param {Function} testFunction + */ +exports.runInPlayground = async function(testFunction) { + const originalDir = cwd(); + const id = parseInt(Math.random() * 99999999); + const playgroundDir = path.resolve(__dirname, '../playground-' + id ) + + async function setup() { + fsExtra.mkdirpSync(playgroundDir); + await setupTestThemes(); + chdir(playgroundDir); + } + + function cleanup() { + chdir(originalDir); + cleanupTestThemes(); + fsExtra.removeSync(playgroundDir); + } + + try { + await setup(); + await testFunction(new TestInstance()); + } catch (err) { + throw err; + } finally { + cleanup(); + } +} \ No newline at end of file diff --git a/tests/acceptance/suites/basic-flow.js b/tests/acceptance/suites/basic-flow.js new file mode 100644 index 00000000..7432252e --- /dev/null +++ b/tests/acceptance/suites/basic-flow.js @@ -0,0 +1,18 @@ +const fs = require('fs'); +const path = require('path'); + +const { runInPlayground } = require('../setup/playground'); + +// silence jambo's noisy output +console.log = jest.fn(); + +it('can init -> import -> create page -> build', () => runInPlayground(async t => { + await t.jambo('init'); + await t.jambo('import --themeUrl ../test-themes/basic-flow'); + await t.jambo('page --name index --template universal-standard'); + await t.jambo('build'); + const actualPage = fs.readFileSync('public/index.html', 'utf-8'); + const expectedPage = fs.readFileSync( + '../fixtures/basic-flow/index.html', 'utf-8'); + expect(actualPage).toEqualHtml(expectedPage); +})); \ No newline at end of file diff --git a/tests/acceptance/test-themes/basic-flow/postimport.js b/tests/acceptance/test-themes/basic-flow/postimport.js new file mode 100755 index 00000000..4f5bd399 --- /dev/null +++ b/tests/acceptance/test-themes/basic-flow/postimport.js @@ -0,0 +1,21 @@ +#!/usr/bin/env node +const fs = require('fs'); +const fsExtra = require('fs-extra'); +const { assign, stringify } = require('comment-json'); + +/** + * Updates the defaultTheme field in the jambo.json. + * + * @param {string} themeName + */ +function updateDefaultTheme(themeName) { + const jamboConfig = JSON.parse(fs.readFileSync('jambo.json', 'utf-8')); + if (jamboConfig.defaultTheme !== themeName) { + const updatedConfig = assign({ defaultTheme: themeName }, jamboConfig); + fs.writeFileSync('jambo.json', stringify(updatedConfig, null, 2)); + } +} + +updateDefaultTheme('basic-flow'); +fs.writeFileSync('config/global_config.json', '{}'); +fsExtra.copySync('themes/basic-flow/static', 'static'); \ No newline at end of file diff --git a/tests/acceptance/test-themes/basic-flow/script/core.hbs b/tests/acceptance/test-themes/basic-flow/script/core.hbs new file mode 100644 index 00000000..a88e95aa --- /dev/null +++ b/tests/acceptance/test-themes/basic-flow/script/core.hbs @@ -0,0 +1,3 @@ + diff --git a/tests/acceptance/test-themes/basic-flow/static/.gitkeep b/tests/acceptance/test-themes/basic-flow/static/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/tests/acceptance/test-themes/basic-flow/templates/universal-standard/markup/directanswer.hbs b/tests/acceptance/test-themes/basic-flow/templates/universal-standard/markup/directanswer.hbs new file mode 100644 index 00000000..5f000c6a --- /dev/null +++ b/tests/acceptance/test-themes/basic-flow/templates/universal-standard/markup/directanswer.hbs @@ -0,0 +1 @@ +
\ No newline at end of file diff --git a/tests/acceptance/test-themes/basic-flow/templates/universal-standard/page-config.json b/tests/acceptance/test-themes/basic-flow/templates/universal-standard/page-config.json new file mode 100644 index 00000000..a6f21011 --- /dev/null +++ b/tests/acceptance/test-themes/basic-flow/templates/universal-standard/page-config.json @@ -0,0 +1,14 @@ +{ + "pageTitle": "universal page", + "componentSettings": { + /** + "QASubmission": { + "entityId": "", // Set the ID of the entity to use for Q&A submissions, must be of entity type "Organization" + "privacyPolicyUrl": "" // The fully qualified URL to the privacy policy + }, + **/ + "DirectAnswer": { + "dummyConfig": "dummy config" + } + } +} diff --git a/tests/acceptance/test-themes/basic-flow/templates/universal-standard/page.html.hbs b/tests/acceptance/test-themes/basic-flow/templates/universal-standard/page.html.hbs new file mode 100644 index 00000000..b0f2bd9b --- /dev/null +++ b/tests/acceptance/test-themes/basic-flow/templates/universal-standard/page.html.hbs @@ -0,0 +1,5 @@ +{{#> script/core }} + {{> templates/universal-standard/script/directanswer }} +{{/script/core }} +{{> templates/universal-standard/markup/directanswer }} + diff --git a/tests/acceptance/test-themes/basic-flow/templates/universal-standard/script/directanswer.hbs b/tests/acceptance/test-themes/basic-flow/templates/universal-standard/script/directanswer.hbs new file mode 100644 index 00000000..4853ed74 --- /dev/null +++ b/tests/acceptance/test-themes/basic-flow/templates/universal-standard/script/directanswer.hbs @@ -0,0 +1,3 @@ +ANSWERS.addComponent("DirectAnswer", Object.assign({}, { + container: "#js-answersDirectAnswer" +}, {{{ json componentSettings.DirectAnswer }}})); \ No newline at end of file diff --git a/tests/commands/describe/describecommand.js b/tests/commands/describe/describecommand.js index 02cc64e3..6cc71ef8 100644 --- a/tests/commands/describe/describecommand.js +++ b/tests/commands/describe/describecommand.js @@ -15,10 +15,9 @@ const mockInitCommand = { type: 'singleoption', options: ['answers-hitchhiker-theme'] }, - addThemeAsSubmodule: { - displayName: 'Add Theme as Submodule', - type: 'boolean', - default: true + useSubmodules: { + displayName: 'Use Submodules', + type: 'boolean' } } } @@ -54,10 +53,9 @@ describe('DescribeCommand works correctly', () => { type: 'singleoption', options: ['answers-hitchhiker-theme'] }, - addThemeAsSubmodule: { - displayName: 'Add Theme as Submodule', - type: 'boolean', - default: true + useSubmodules: { + displayName: 'Use Submodules', + type: 'boolean' } } }); diff --git a/tests/fixtures/handlebars/opensans-regular-webfont.woff b/tests/fixtures/handlebars/opensans-regular-webfont.woff new file mode 100644 index 00000000..18334c80 Binary files /dev/null and b/tests/fixtures/handlebars/opensans-regular-webfont.woff differ diff --git a/tests/fixtures/handlebars/processedcomponent.js b/tests/fixtures/handlebars/processedcomponent.js index 730d60f9..0e842a0e 100644 --- a/tests/fixtures/handlebars/processedcomponent.js +++ b/tests/fixtures/handlebars/processedcomponent.js @@ -1,3 +1,4 @@ +{{!-- {{ translate phrase="handlebars comments should be ignored"}} --}} {{> cards/card_component componentName='standard' }} class standardCardComponent extends BaseCard['standard'] { diff --git a/tests/fixtures/handlebars/processedtemplate.hbs b/tests/fixtures/handlebars/processedtemplate.hbs index 5893f002..3fbf9d15 100644 --- a/tests/fixtures/handlebars/processedtemplate.hbs +++ b/tests/fixtures/handlebars/processedtemplate.hbs @@ -14,7 +14,8 @@ {{ processTranslation pluralForm0='La [[count]] femme a fait une promenade' pluralForm1='Les [[count]] femmes fait une promenade' count=myCount }} - {{!-- {{ processTranslation pluralForm0='singular' pluralForm1='plural' count=mycount }} --}} + + {{!-- {{ translate phrase="handlebars comments should be ignored"}} --}} L'homme L'homme <span class="yext">L'os du chien</span> diff --git a/tests/fixtures/handlebars/rawcomponent.js b/tests/fixtures/handlebars/rawcomponent.js index 1278c656..a6ed8601 100644 --- a/tests/fixtures/handlebars/rawcomponent.js +++ b/tests/fixtures/handlebars/rawcomponent.js @@ -1,3 +1,4 @@ +{{!-- {{ translate phrase="handlebars comments should be ignored"}} --}} {{> cards/card_component componentName='standard' }} class standardCardComponent extends BaseCard['standard'] { @@ -18,7 +19,12 @@ class standardCardComponent extends BaseCard['standard'] { target: '_top', // If the title's URL should open in a new tab, etc. titleEventOptions: this.addDefaultEventOptions(), details: {{ translateJS phrase='Some item [[name]]' pluralForm='Some items [[name]]' name=profile.name count=profile.count }}, // The text in the body of the card - intermixed: {{ translateJS phrase='View our website [[name]]' pluralForm='View our websites [[name]]' count=2 name=name}}, + intermixed: {{ translateJS + phrase='View our website [[name]]' + pluralForm='View our websites [[name]]' + count=2 + name=name + }}, singleQuote: {{ translateJS phrase='The dog\'s bone' }}, pluralizedSingleQuote: {{ translateJS phrase='The person' pluralForm='The people' context='male' count=myCount}}, showMoreDetails: { @@ -27,7 +33,11 @@ class standardCardComponent extends BaseCard['standard'] { showLessText: 'Show less' // Label when toggle will hide truncated text }, CTA1: { - label: {{translateJS phrase='Mail now [[id1]]' context='Mail is a verb' id1=profile.name}}, // The CTA's label + label: {{translateJS + phrase='Mail now [[id1]]' + context='Mail is a verb' + id1=profile.name + }}, // The CTA's label label2: {{translateJS phrase='[[name]]\'s mail' name=myName}}, iconName: 'chevron', // The icon to use for the CTA url: Formatter.generateCTAFieldTypeLink(profile.c_primaryCTA), // The URL a user will be directed to when clicking diff --git a/tests/fixtures/handlebars/rawtemplate.hbs b/tests/fixtures/handlebars/rawtemplate.hbs index 34bc408d..97e7f60d 100644 --- a/tests/fixtures/handlebars/rawtemplate.hbs +++ b/tests/fixtures/handlebars/rawtemplate.hbs @@ -14,7 +14,8 @@ {{ translate phrase='The [[count]] person went on a walk' pluralForm='The [[count]] people went on a walk' context='female' count=myCount}} - {{!-- {{ translate phrase='singular' pluralForm='plural' count=mycount}} --}} + + {{!-- {{ translate phrase="handlebars comments should be ignored"}} --}} {{ translate phrase='Person' context="male" escapeHTML=true}} {{ translate phrase='Person' context="male" escapeHTML=false}} {{ translate phrase='The dog\'s bone' escapeHTML=true}} @@ -25,5 +26,10 @@ {{ translate phrase='View our website [[name]]' name=name}} {{ translate phrase='View our website [[name]]' pluralForm='View our websites [[name]]' count=2 name=name escapeHTML=false}} {{ translate phrase='View our website [[name]]' pluralForm='View our websites [[name]]' count=2 name=name}} - {{ translate phrase='View our website [[name]]' pluralForm='View our websites [[name]]' count=2 name=name context='internet web, not spider web' escapeHTML=false}} + {{ translate + phrase='View our website [[name]]' + pluralForm='View our websites [[name]]' + count=2 name=name context='internet web, not spider web' + escapeHTML=false + }} diff --git a/tests/handlebars/handlebarspreprocessor.js b/tests/handlebars/handlebarspreprocessor.js index d96fb26b..61c6c23a 100644 --- a/tests/handlebars/handlebarspreprocessor.js +++ b/tests/handlebars/handlebarspreprocessor.js @@ -108,4 +108,12 @@ describe('HandlebarsPreprocessor works correctly', () => { expect(handlebarsPreprocessor.process(rawHbsHandlebarsContent)) .toEqual(processedHbsHandlebarsContent); }); + + it('fails gracefully when trying to process .woff files', () => { + const woffPath = + path.join(__dirname, '../fixtures/handlebars/opensans-regular-webfont.woff'); + const woffContent = readFileSync(woffPath, 'utf8'); + expect(handlebarsPreprocessor.process(woffContent)) + .toEqual(woffContent); + }); }); diff --git a/tests/handlebars/hbshelperparser.js b/tests/handlebars/hbshelperparser.js new file mode 100644 index 00000000..e4922950 --- /dev/null +++ b/tests/handlebars/hbshelperparser.js @@ -0,0 +1,33 @@ +const HbsHelperParser = require('../../src/handlebars/hbshelperparser'); + +it('can parse a simple helper', () => { + const parser = new HbsHelperParser(['simpleBoy']); + const parsedHelpers = parser.parse('{{simpleBoy phrase=\'hi\'}}'); + expect(parsedHelpers).toEqual(['{{simpleBoy phrase=\'hi\'}}']); +}); + +it('will ignore helpers that are not requested for', () => { + const parser = new HbsHelperParser(['translate']); + const parsedHelpers = parser.parse( + '{{simpleBoy phrase=\'hi\'}} {{translate phrase=\'mimimi\'}}'); + expect(parsedHelpers).toEqual(['{{translate phrase=\'mimimi\'}}']); +}); + +it('can parse from a multi-line file', () => { + const parser = new HbsHelperParser(['translate']); + const parsedHelpers = parser.parse( + '{{translate}}\n\n\n{{translate phrase="mimimi"}}'); + expect(parsedHelpers).toEqual([ + '{{translate}}', + '{{translate phrase="mimimi"}}' + ]); +}); + +it('can parse a multi-line helper', () => { + const parser = new HbsHelperParser(['translate']); + const parsedHelpers = parser.parse( + 'asdfasd\n\n{{ translate\nphrase=\'hi\'\ncontext=\'some context!!!\'\n }}'); + expect(parsedHelpers).toEqual([ + '{{ translate\nphrase=\'hi\'\ncontext=\'some context!!!\'\n }}' + ]); +}); \ No newline at end of file diff --git a/tests/setup/setup.js b/tests/setup/setup.js new file mode 100644 index 00000000..84391a3d --- /dev/null +++ b/tests/setup/setup.js @@ -0,0 +1,21 @@ +const prettier = require('prettier'); + +expect.extend({ + toEqualHtml: function(received, expected) { + const normalize = html => { + return prettier.format(html, { parser: 'html' }); + }; + const pass = normalize(received) === normalize(expected) + if (pass) { + return { + message: () => `expected ${received} to not equal ${expected}`, + pass: true, + }; + } else { + return { + message: () => `expected ${received} to equal ${expected}`, + pass: false, + }; + } + } +}); diff --git a/tests/utils/fileutils.js b/tests/utils/fileutils.js index c2600cab..24beb562 100644 --- a/tests/utils/fileutils.js +++ b/tests/utils/fileutils.js @@ -56,22 +56,26 @@ describe('isValidFile properly determines if files are valid', () => { }); describe('isValidPartialPath properly determines if paths are valid', () => { - it('returns true when a path does not contain /node_modules/', () => { - let path = '../answers-hitchhiker-theme/partials/index.hbs'; - let isValid = isValidPartialPath(path); - expect(isValid).toEqual(true); - }); - - it('returns false when a path contains /node_modules/', () => { - let path = '../../answers-hitchhiker-theme/test-site/node_modules/yargs/index.js'; - let isValid = isValidPartialPath(path); - expect(isValid).toEqual(false); - }); - - it('returns false when a path starts with node_modules/', () => { - let path = 'node_modules/handlebars/index.js'; - let isValid = isValidPartialPath(path); - expect(isValid).toEqual(false); + const blacklistedPaths = ['.git', 'node_modules']; + blacklistedPaths.forEach(blacklistedPath => { + it(`returns true when a path does not contain /${blacklistedPath}/`, () => { + let path = '../answers-hitchhiker-theme/partials/index.hbs'; + let isValid = isValidPartialPath(path); + expect(isValid).toEqual(true); + }); + + it(`returns false when a path contains /${blacklistedPath}/`, () => { + let path = + `../../answers-hitchhiker-theme/test-site/${blacklistedPath}/yargs/index.js`; + let isValid = isValidPartialPath(path); + expect(isValid).toEqual(false); + }); + + it(`returns false when a path starts with ${blacklistedPath}/`, () => { + let path = `${blacklistedPath}/handlebars/index.js`; + let isValid = isValidPartialPath(path); + expect(isValid).toEqual(false); + }); }); });