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
+
+
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);
+ });
});
});